/* ============================================================
   a360 — CMS block styles.

   Loaded after style.css on CMS-rendered pages (page.html) and inside the
   editor's preview iframe. It deliberately adds as little as possible: the
   blocks render with the same .tile, .card, .price-card, .faq, .btn and
   .section-head__title classes the hand-written pages use, so a page built in
   the CMS is indistinguishable from one written by hand.

   What lives here is only the part the site had no vocabulary for: the outer
   section wrapper whose spacing, width, background and alignment an editor can
   change. Every one of those is a custom property set per block, so nothing
   here has to know which values exist.

   Specificity note: the site's own .section / .section-head / .features rules
   carry outer padding and margins meant for hand-written markup. Inside a
   block those would fight the block's own spacing, so the resets below are
   written as .cmsb <descendant> (0,2,0) to beat the single-class originals.
   ============================================================ */

/* ---------- Holding the fold while the body is in flight ----------
   page.html ships an empty #cms-root and fetches the page, but js/layout.js
   fills in the shared CTA and footer straight away — so for as long as the
   request takes, the only things on screen are the nav, the call to action and
   the footer, which then get shoved down when the content lands. Reserving a
   screen's worth of space keeps them below the fold, so the page fills in from
   the top instead of reshuffling.

   Scoped as tightly as the problem: a prerendered page has its body in the HTML
   and never matches :empty, the reservation ends the moment cms-render sets
   body[data-cms-state] (including the not-found and error states, which want to
   sit at their natural height), and the editor's preview iframe is exempt —
   it draws into this same root from a different source. */
html:not([data-cms-preview]) body:not([data-cms-state]) #cms-root:empty { min-height: 100vh; }

/* ---------- Block shell ---------- */
.cmsb {
  --cmsb-pt: var(--section-y);
  --cmsb-pb: var(--section-y);
  --cmsb-max: 1440px;
  --cmsb-gap: clamp(28px, 4vw, 56px);
  position: relative;
  padding-block: var(--cmsb-pt) var(--cmsb-pb);
  /* Left and right default to the site gutter and are overridable per section,
     which is what "full bleed on one side" needs. */
  padding-inline: var(--cmsb-pl, var(--gutter)) var(--cmsb-pr, var(--gutter));
}
.cmsb__inner {
  max-width: var(--cmsb-max);
  min-height: var(--cmsb-minh, 0);
  margin-inline: auto;
  width: 100%;
}

/* Container widths. `full` drops the gutter too — for edge-to-edge media. */
.cmsb--w-narrow  { --cmsb-max: 760px; }
.cmsb--w-content { --cmsb-max: 1080px; }
.cmsb--w-wide    { --cmsb-max: 1440px; }
.cmsb--w-full    { --cmsb-max: none; padding-inline: 0; }

/* Backgrounds. Dark and brand invert the type colours, including the heading
   colour, which is otherwise --brand-dark and would vanish on a dark panel. */
.cmsb--bg-tint  { background: var(--pill); }
.cmsb--bg-panel { background: var(--panel); }
.cmsb--bg-brand { background: var(--brand-dark); }
.cmsb--bg-panel,
.cmsb--bg-brand { color: #fff; --heading: #fff; --ink: #fff; --muted: rgba(255, 255, 255, 0.66); --line: rgba(255, 255, 255, 0.16); }
.cmsb--bg-panel h1, .cmsb--bg-panel h2, .cmsb--bg-panel h3,
.cmsb--bg-brand h1, .cmsb--bg-brand h2, .cmsb--bg-brand h3 { color: #fff; }
.cmsb--bg-panel .tile, .cmsb--bg-brand .tile,
.cmsb--bg-panel .card, .cmsb--bg-brand .card,
.cmsb--bg-panel .price-card, .cmsb--bg-brand .price-card {
  background: rgba(255, 255, 255, 0.05);
  border-color: rgba(255, 255, 255, 0.16);
}
/* .tile:hover (css/style.css) switches to var(--pill), which these blocks
   never redefine — landing an opaque light-gray card on a dark panel instead
   of the highlighted-glass look the rest of the tile takes here. Brightening
   the same translucent white keeps the hover on-theme while still reading as
   a clear lift, the way .approw__item:hover reads against --panel. */
.cmsb--bg-panel .tile:hover, .cmsb--bg-brand .tile:hover {
  background: rgba(255, 255, 255, 0.09);
}

/* Company products / Companies / Child pages default to centered — the full
   version of this lives in css/style.css beside .cmsb--pricing's identical
   fix (text-align alone doesn't move .section-head__title/__lead, which are
   block-level boxes with their own max-width). Kept here as a pointer since
   every other block-type default in this file lives in this one place. */

.cmsb--align-center { text-align: center; }
.cmsb--align-center .cmsb__actions { justify-content: center; }
.cmsb--align-center .eyebrow { justify-content: center; }
/* The half the Alignment control was missing. .section-head__title and
   .section-head__lead carry their own max-width (18ch and 54ch, css/style.css)
   — so `text-align: center` centred the words inside a box that was itself
   still hard against the left edge, and a heading shorter than its measure
   looked untouched by the control. Three block types already worked around
   this one at a time in css/style.css by naming themselves; this is the same
   fix applied where the class is actually declared, so every block whose
   alignment is set to centre gets it. Blocks that opted in by name keep their
   !important versions, which land on the same values. */
.cmsb--align-center .section-head__title,
.cmsb--align-center .section-head__lead { margin-inline: auto; }
.cmsb--align-right .section-head__title,
.cmsb--align-right .section-head__lead { margin-inline: auto 0; }

/* "Right" was never given its half of this pair — the class reached the DOM
   from the inspector's Alignment control, but nothing in the stylesheet ever
   matched it, so the option looked broken however you got there. */
.cmsb--align-right { text-align: right; }
.cmsb--align-right .cmsb__actions { justify-content: flex-end; }
.cmsb--align-right .eyebrow { justify-content: flex-end; }

/* ---------- Resets for reused site containers ---------- */
.cmsb .section-head {
  max-width: none;
  margin-inline: 0;
  padding-inline: 0;
  margin-bottom: var(--cmsb-gap);
}
.cmsb .section-head:last-child { margin-bottom: 0; }
.cmsb .features { padding: 0; gap: clamp(72px, 11vh, 150px); }
.cmsb .feature { max-width: none; }
/* Feature showcase's "Picture on the left" toggle. Scoped to .cmsb rather
   than added to .feature--flip in css/style.css: the homepage's own Academy
   pillar already carries that class with no matching rule (a no-op there
   today), and giving it real effect site-wide would silently swap that
   section's layout as a side effect of this block gaining a flip toggle.
   `order` alone was the first attempt, and was wrong: .feature's own two
   grid tracks are different widths (minmax(280px,420px) then 1fr in
   style.css; see the featureShowcase override just below, which swaps
   which of the two is the wide one) — a grid item's `order` only changes
   *placement* order, not which of those two tracks it lands in, so
   reordering put the picture into the track sized for copy and shrank it.
   `direction: rtl` on the grid instead mirrors which *side* each track
   renders on while every item keeps its own track's size, wherever they
   sit — then resets to ltr on the children so their own text and image
   aren't mirrored too. */
.cmsb .feature--flip { direction: rtl; }
.cmsb .feature--flip > * { direction: ltr; }
/* Bounds the picture by bounding the *column* it fills, rather than by
   capping the picture inside a column wider than it.
   style.css sizes the media track as a bare 1fr because its own user, the
   homepage's .card, fills whatever width that gives it (via aspect-ratio)
   — so on a wide screen that track runs past 900px and an uploaded
   screenshot, which has no such self-limit, renders however tall its own
   proportions make it at that width. Capping the picture itself was tried
   first and is what caused the alignment trouble: any cap narrower than
   the column leaves space inside it with nothing to fill, and wherever
   that leftover is placed, the picture's edge stops matching the column's.
   Capping the track instead leaves the picture at width:100% of it, so
   its edges still land exactly on the column's — flush to the same 1440px
   frame as every other section, on both sides, flipped or not — while a
   narrower column brings the rendered height down proportionally; the gap
   between the two is untouched.

   Sized in fr rather than a fixed px cap on the media track: the block
   shell already caps the whole row at 1440px (--cmsb-max), so a ratio is
   bounded on a large screen anyway — and a fixed cap misbehaves in the
   middle of the range, where the media track holds its full width and
   leaves the copy whatever is left (at a ~1100px viewport that was 197px
   of copy against 700px of picture). A ratio keeps both sides shrinking
   together instead. */
section[data-block-type="featureShowcase"] .feature {
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr);
}
/* One measure for the whole copy column, so the heading and the paragraph
   below it end on the same line rather than the heading running the full
   column while the body stops at its own 40ch — which is what made the
   copy read ragged once the column got wider. The cap lands on .feature__copy
   so every part of the copy (eyebrow, heading, body, checklist, button)
   shares it, and .feature__desc's own narrower max-width is cleared so it
   fills that shared measure instead of setting a second, shorter one. */
section[data-block-type="featureShowcase"] .feature__copy {
  max-width: 520px;
}
section[data-block-type="featureShowcase"] .feature__desc {
  max-width: none;
}
@media (max-width: 900px) {
  /* style.css already stacks .feature to one column here, but that rule is
     a single class and this override is not — without a matching one, this
     would keep forcing two columns onto a phone. */
  section[data-block-type="featureShowcase"] .feature {
    grid-template-columns: 1fr;
  }
  /* The measure cap is only there to stop the copy outrunning the picture
     in a side-by-side row. Stacked, the two sit one above the other in the
     same single column, so holding the copy 38px short of the picture's
     edge just reads as the text being misaligned with it. */
  section[data-block-type="featureShowcase"] .feature__copy {
    max-width: none;
  }
}
/* .cmsb__actions already carries its own top margin; .feature__desc's own
   margin-bottom would normally collapse against it to the same effect, but
   trimmed explicitly rather than relying on that — same as .cohero's. */
.feature__copy .cmsb__actions { margin-top: 0; }
/* .price-card__list carries no bottom margin of its own (its other use, the
   pricing cards, always ends with a .btn that has its own top margin
   instead) — without one here, the button sits flush against the last
   checklist line. Scoped to .feature__copy so pricing cards keep their own
   spacing untouched. */
.feature__copy .price-card__list { margin-bottom: 26px; }

/* ---------- Shared bits ---------- */
.cmsb__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  margin-top: 28px;
}
.cmsb__actions:first-child { margin-top: 0; }
.cmsb__lead { color: var(--muted); font-size: clamp(16px, 1.15vw, 18.5px); line-height: 1.62; }

/* ---------- Hero ---------- */
/* Reuses .hero/.hero--page wholesale; the block shell only sets the canvas up
   and stops its own padding doubling the hero's.
   min-height: auto used to live here too, meant to cancel .hero's own
   min-height: 100vh (the homepage's full-height variant) now that a CMS hero
   is never that tall. But every CMS hero also always carries .hero--page
   (see the render function in js/cms-render.js), which already has its own,
   smaller clamp(560px, 78vh, 760px) — and ".cmsb--hero .hero" is more
   specific than ".hero--page", so this was silently overriding that clamp
   too, not just the 100vh it was meant to cancel. The effect: three pages
   with three different amounts of hero copy (a page with a button, one
   without) each got a *different* box height, hugged to whatever content
   they happened to have — headline sitting at a different distance from
   the nav on every page, and a WebGL backdrop with noticeably less canvas
   to fill on the shorter ones, reading as "cropped". Deleting the override
   lets .hero--page's own clamp apply — the same fixed height on all three,
   generous enough that the backdrop scenes (built for that size) fill it
   properly, and .hero__content's default centering (no extra top padding
   needed once the box isn't hugging content) clears the nav on its own. */
.cmsb--hero { padding: 0; }
/* Always edge-to-edge, not just whatever the block's own Width style says —
   a hero's backdrop is the one thing on the page that should never be
   capped at the 1440px content width (.cmsb--w-wide's default for every
   block, since no hero instance has ever set its own Width). Below 1440px
   viewports this was invisible (nothing to cap); on a bigger monitor it
   quietly left the canvas short of the actual screen, with the page
   background showing at both edges instead of the pattern reaching them. */
.cmsb--hero .cmsb__inner { max-width: none; }
/* .hero__content's shared 120px/60px top/bottom padding is asymmetric on
   purpose for the homepage's own full-height hero (100vh, so it needed more
   push-down from a taller fixed nav area above the fold). A CMS hero is a
   shorter, fixed box that centers its content via grid place-items — with
   that same lopsided padding, the content box's own centre of mass sits
   above the box's true centre, so the centred grid places the *text*
   visibly above centre too. Matching the top to the existing 60px bottom
   is what actually centers it, now that the box height is fixed rather
   than hugging content (see the min-height fix above). */
.hero--page .hero__content { padding-top: 60px; }

/* ---------- Rich text ---------- */
.cmsb .prose > * + * { margin-top: 1.1em; }
.cmsb .prose h2, .cmsb .prose h3 { margin-top: 1.6em; font-family: var(--display); color: var(--heading); }
.cmsb .prose ul, .cmsb .prose ol { padding-left: 1.25em; }
.cmsb .prose li + li { margin-top: 0.5em; }
.cmsb .prose a { color: var(--brand); text-decoration: underline; text-underline-offset: 3px; }

/* ---------- List block ---------- */
.cmslist { display: grid; gap: 14px; list-style: none; }
.cmslist--cols { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 14px 40px; }
.cmslist__i {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 14px;
  align-items: start;
  line-height: 1.55;
}
.cmslist__m {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  flex: none;
  border-radius: 8px;
  background: color-mix(in srgb, var(--brand) 14%, transparent);
  color: var(--brand);
  font-family: var(--mono);
  font-size: 11.5px;
  font-weight: 500;
  margin-top: 2px;
}
.cmslist__m svg { width: 14px; height: 14px; }
.cmslist__t { font-weight: 600; }
.cmslist__d { color: var(--muted); font-size: 15px; margin-top: 3px; }

/* ---------- Testimonials ---------- */
.cmsquotes { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: clamp(16px, 2vw, 26px); }
.cmsquote {
  display: flex;
  flex-direction: column;
  gap: 22px;
  border: 1px solid var(--line);
  border-radius: 22px;
  padding: clamp(24px, 3vw, 34px);
  background: var(--paper);
}
.cmsquote__mark { width: 26px; height: 20px; color: var(--brand); opacity: 0.85; }
.cmsquote__text { font-size: clamp(16px, 1.2vw, 19px); line-height: 1.55; flex: 1; }
.cmsquote__who { display: flex; align-items: center; gap: 12px; }
.cmsquote__pic { width: 40px; height: 40px; border-radius: 50%; object-fit: cover; flex: none; }
.cmsquote__name { font-weight: 600; font-size: 14.5px; }
.cmsquote__role { color: var(--muted); font-size: 13px; }

/* ---------- Logo rail ---------- */
.cmslogos { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: clamp(26px, 4vw, 64px); }
.cmslogos img { height: clamp(24px, 2.4vw, 34px); width: auto; opacity: 0.55; filter: grayscale(1); transition: opacity 0.3s, filter 0.3s; }
.cmslogos a:hover img, .cmslogos img:hover { opacity: 1; filter: none; }

/* ---------- Image block ---------- */
.cmsfig { margin: 0; }
.cmsfig img { width: 100%; height: auto; border-radius: var(--cmsfig-radius, 22px); }
/* display: block — a <video> defaults to inline, which leaves a few pixels of
   baseline gap under it that <img> (already block here) doesn't have; object-fit
   so a video shot at a different aspect ratio than its <img> counterpart still
   fills the same frame instead of letterboxing. */
.cmsfig video { width: 100%; height: auto; display: block; border-radius: var(--cmsfig-radius, 22px); object-fit: cover; }
.cmsfig figcaption { margin-top: 12px; color: var(--muted); font-size: 13.5px; }

/* ---------- Feature showcase picture: cursor tilt + glow ----------
   Scoped by the section's own data-block-type (every block already carries
   one) so this never touches an image anywhere else on the site. */
section[data-block-type="featureShowcase"] .cmsfig {
  position: relative;
  /* Scopes the ::after's mix-blend-mode to this box alone, so the glow
     blends with the picture underneath it instead of reaching past .cmsfig
     into whatever the page paints behind the whole section. */
  isolation: isolate;
  /* Matches the <img>'s own radius and clips to it, so ::after (a plain
     rectangle) never has a corner poking past the picture's rounded edge —
     that mismatch, not the blend mode, was the actual source of the
     "boundary" on dark panels. With this, the glow's only visible edge is
     the picture's real edge. */
  border-radius: var(--cmsfig-radius, 22px);
  overflow: hidden;
  will-change: transform;
}
section[data-block-type="featureShowcase"] .cmsfig::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: radial-gradient(circle at var(--tilt-mx, 50%) var(--tilt-my, 50%), rgba(255, 255, 255, 0.9), transparent 60%);
  /* Blended instead of laid on top as a flat tint: soft-light modulates the
     picture's own pixels by how much alpha the gradient has at each point,
     so it thins out to nothing right along with the gradient's own fade —
     no flat-opacity rectangle edge left to show past the picture's rounded
     corners, which is what made it visible on dark panels. */
  mix-blend-mode: soft-light;
  opacity: 0;
  transition: opacity 0.3s ease;
  pointer-events: none;
}
/* Both feature showcase's and companyHero's cursor-follow glow stay off in
   both themes now — left out of the `.is-tilting::after` rule entirely rather
   than overridden per theme, so the ::after (still there for the tilt's
   isolation/clip setup above) just never leaves opacity: 0. The tilt itself —
   the picture rotating with the pointer — is untouched, only the highlight
   that used to chase the cursor across it is gone. companyHero is the picture
   beside the heading on a product page's opening hero. */
section[data-block-type="companyHero"] .cmsfig {
  position: relative;
  will-change: transform;
}
section[data-block-type="companyHero"] .cmsfig::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: radial-gradient(circle at var(--tilt-mx, 50%) var(--tilt-my, 50%), rgba(255, 255, 255, 0.28), transparent 60%);
  opacity: 0;
  transition: opacity 0.3s ease;
  pointer-events: none;
}
/* Feature tabs' picture: same tilt as above, no glow — matches companyHero's
   plainer treatment rather than featureShowcase's, since a picture that's
   about to be swapped out for the next item doesn't need its own highlight
   layer clipped and isolated for one. Positioning context only. */
section[data-block-type="featureTabs"] .cmsfig {
  position: relative;
  will-change: transform;
}

/* ---------- Company hero ----------
   An open, on-page hero (no forced panel) — it reads off the block's own
   background/heading/muted tokens, so the "Style" background picker (paper,
   tint, panel, brand) still works exactly as it does on every other block
   instead of being overridden. Copy beside a picture, split evenly and
   centered, same responsive shape as .feature. min-width:280px matches the
   floor .cmsn's grow children wrap at, for the same reason: a flex child
   with no floor just keeps shrinking instead of ever stacking on a narrow
   screen. */
.cohero {
  display: flex;
  gap: clamp(32px, 5vw, 64px);
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
}
.cohero--flip {
  flex-direction: row-reverse;
}
.cohero__copy,
.cohero__media {
  flex: 1 1 0%;
  min-width: 280px;
  max-width: 640px;
}
/* The company's own logo + name, as a small pill badge above the headline —
   the "New" / "Watch us on Zoom" tag slot in the layout this follows. */
.cohero__brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-size: 13.5px;
  font-weight: 600;
  color: var(--ink);
  background: var(--pill);
  border: 1px solid var(--line);
  padding: 6px 18px 6px 6px;
  border-radius: 999px;
  margin-bottom: 26px;
}
.cohero__logo {
  height: 26px;
  width: 26px;
  flex: none;
  object-fit: contain;
  box-sizing: border-box;
  /* Uploaded logos are drawn for a light background — a fixed white disc
     behind every one keeps it legible on the badge regardless of theme,
     instead of forcing a white silhouette that erased its real colours. */
  background: #fff;
  border-radius: 50%;
  padding: 4px;
}
.cohero__title {
  font-family: var(--display);
  font-size: clamp(34px, 4vw, 56px);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.06;
  margin-bottom: 20px;
  color: var(--heading);
}
.cohero__desc { font-size: 16.5px; line-height: 1.65; color: var(--muted); margin-bottom: 32px; max-width: 46ch; }
/* .cmsb__actions already provides gap/wrap; only the top margin needs
   trimming here since .cohero__desc already carries that spacing. */
.cohero .cmsb__actions { margin-top: 0; }
/* Second button reads as a plain link (no pill) beside the solid primary
   one — "Contact our sales" in the layout this follows — rather than two
   competing solid buttons. Scoped to .cohero so .btn--ghost keeps its usual
   filled look everywhere else on the site. */
.cohero .cmsb__actions .btn--ghost {
  background: transparent;
  padding-inline: 2px;
  color: var(--ink);
}
.cohero .cmsb__actions .btn--ghost::after {
  content: "→";
  transition: transform 0.25s cubic-bezier(0.3, 0.7, 0, 1);
}
.cohero .cmsb__actions .btn--ghost:hover {
  background: transparent;
  transform: none;
  color: var(--brand);
}
.cohero .cmsb__actions .btn--ghost:hover::after { transform: translateX(4px); }
/* Defensive: older content saved with the "light" (solid white) variant —
   right on a dark panel before this hero opened up to any background — gets
   a hairline border so it doesn't vanish on a paper/tint background. */
.cohero .cmsb__actions .btn--light { border: 1px solid var(--line); }
/* The picture floats with a soft brand-coloured glow behind it — a real
   uploaded image (screenshot, product shot), not a drawn illustration;
   same figure/tilt plumbing as Feature showcase. No white mat around it —
   the image's own edges are the card. */
.cohero__media {
  position: relative;
}
.cohero__media::before {
  content: "";
  position: absolute;
  inset: clamp(-28px, -6vw, -10px) clamp(6px, 3vw, 32px) clamp(6px, 3vw, 32px) clamp(-28px, -6vw, -10px);
  z-index: 0;
  border-radius: 40px;
  background:
    radial-gradient(46% 50% at 20% 18%, rgba(14, 143, 200, 0.28), transparent 70%),
    radial-gradient(40% 46% at 84% 78%, rgba(11, 56, 79, 0.22), transparent 70%);
  filter: blur(40px);
}
.cohero__media .cmsfig {
  position: relative;
  z-index: 1;
  margin: 0;
  border-radius: 20px;
  overflow: hidden;
  box-shadow: 0 30px 60px -30px rgba(11, 56, 79, 0.35), 0 10px 30px -14px rgba(11, 56, 79, 0.18);
}
.cohero__media .cmsfig img { border-radius: 20px; display: block; width: 100%; height: auto; }
.cohero__media .cmsfig video { border-radius: 20px; display: block; width: 100%; height: auto; object-fit: cover; }

/* ---------- Feature showcase brand lockup ----------
   The software's logo beside the brand label. .eyebrow already carries the
   bottom margin that separates this row from the heading, so it moves up to
   the row itself — leaving it on the eyebrow would space the two children
   against each other instead of the block below. */
.feature__brand {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 18px;
}
.feature__brand .eyebrow { margin-bottom: 0; }
/* Bare mark, no plate behind it — unlike .cohero__logo, which sits on a pill
   badge and keeps a white tile so a dark logo stays legible. Here the logo
   sits directly on the section background, so a tile would read as a white
   box drawn around the mark rather than as part of it. The trade-off is that
   a logo drawn in near-black for light backgrounds has nothing to stand
   against on a dark panel; upload a light or full-colour version for those. */
.feature__logo {
  height: 26px;
  width: 26px;
  flex: none;
  object-fit: contain;
}
/* Editor only — the empty state, so the slot is discoverable on the canvas
   rather than only from the inspector's Logo field. */
.feature__logoslot {
  height: 26px;
  width: 26px;
  flex: none;
  display: grid;
  place-items: center;
  padding: 0;
  border: 1px dashed var(--line);
  border-radius: 6px;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  opacity: 0.7;
  transition: opacity 0.2s, border-color 0.2s;
}
.feature__logoslot:hover { opacity: 1; border-color: var(--brand); color: var(--brand); }
.feature__logoslot svg { width: 14px; height: 14px; }
/* ---------- App row ----------
   This page's child pages as square cards, centred. Hovering one opens it: the
   logo lifts, the name grows to full size and the line under it fades in.

   One card, however many lines. A line shares its width between the apps on
   it, and the hovered one takes what it needs off the cards beside it, which
   part around it to make the room; all that changes past a single line is how
   many lines the apps are dealt into. The plain grid here is the base, and
   everything the card does is layered on further down — see the comment above
   the media query for why that order and not the reverse. */
.approw {
  --approw-card: 200px;
  --approw-col: 120px;
  /* The logo's box and the label's measure, named because three other numbers
     are derived from them: the label's `top` (which has to clear the logo),
     and the resting scale (which has to keep the label inside its column).
     Hard-coding those meant a change to one silently broke the others. */
  --approw-ic: 42px;
  --approw-label: 136px;
  /* Shrinks the label to --approw-label * this. Sized against the *tightest*
     column the row ever produces, which is not the resting one: while a card
     is open its neighbours have each given up a share of its width, so they
     are narrower than they ever are at rest. A label wider than the column it
     sits on runs into the one beside it. */
  --approw-rest: 0.65;
  /* The daylight a label needs beside its neighbour for the row to still be
     worth running as a row. Less a style knob than the threshold
     js/cms-render.js sizes a line against, which is why it lives here with the
     numbers it is measured against rather than in the script. Set
     from what the seven-app row already ships with at 981px — the narrowest
     width the row form runs at, where its labels clear each other by 14.5px —
     so no row that reads as a row today is turned into a grid by the check. */
  --approw-gap-min: 12px;
  /* How far the card's whole stack — logo and label — sits below the padding
     it is laid out from, so that at rest it reads as centred in the square
     rather than pinned to the top of it. Derived: the square, less what the
     stack actually occupies at rest (the logo box, the 12px under it, and one
     line of the label at its resting scale — 17px x 1.2, the two numbers
     .approw__name sets further down), halved, then less the 28px of top
     padding it is measured from. Comes out around 38px.

     One number for every card in the block rather than one per card. A label
     that wraps to two lines therefore rides a few pixels high of true centre,
     which is the trade worth making: centring each card on its own content
     would step the logos up and down across a line that has to read as one. */
  --approw-rest-shift: calc(
    (var(--approw-card) - (var(--approw-ic) + 12px + 17px * 1.2 * var(--approw-rest))) / 2 - 28px
  );
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
  gap: 30px 12px;
}
/* The lines applyAppRowShapes() deals the apps into — see there for why they
   have to be spelled out rather than left to the browser to wrap. They exist
   for the card, and the card only exists below; here they are inert, and
   display:contents drops them out of the box tree so the apps fall through
   into the grid above exactly as though the wrappers had never been added. */
.approw__line { display: contents; }
.approw__item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  text-align: center;
  text-decoration: none;
  color: inherit;
}
.approw__ic {
  width: var(--approw-ic);
  height: var(--approw-ic);
  flex: none;
  display: grid;
  place-items: center;
  /* The label below is positioned to clear exactly this box, so a logo that
     rendered any taller than it would be sitting under its own name. The image
     is handed this box's size as a length (below), so it cannot ask for more
     than this whatever the uploaded file's dimensions or aspect; the clip stays
     as the backstop for anything else that ends up in here. */
  overflow: hidden;
}
.approw__ic img {
  /* This box's size, not 100% of it. The box centres its content, which leaves
     its row sized to that content — so a percentage here is measured against a
     row whose height is waiting on this image, and CSS resolves that circle to
     `auto`. A portrait logo then took its intrinsic height (a 435x696 one came
     out 42 wide by 67 tall), the max-* pair collapsed the same way for the same
     reason, and the clip above took the difference off the bottom of the logo.
     A length can't go circular, so object-fit gets a real box to letterbox
     into and a tall logo is fitted rather than cropped. */
  width: var(--approw-ic);
  height: var(--approw-ic);
  object-fit: contain;
  display: block;
}
.approw__ic svg { width: 34px; height: 34px; color: var(--brand); }
/* Both take .tile's type — the same treatment the Company products tiles give
   a name and the line under it, so a page carrying both reads as one design. */
.approw__name {
  display: block;
  font-family: var(--display);
  font-size: 14px;
  font-weight: 600;
  letter-spacing: -0.015em;
  line-height: 1.3;
  color: var(--heading);
}
/* Bounded rather than left to run: this line falls back to the page's own
   search description when nobody has written a short one, and those are
   sentences. Three lines keeps one long description from setting the height of
   every row in the grid. */
.approw__cap {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  overflow: hidden;
  margin-top: 4px;
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--muted);
}
/* The card, and the two shapes it lays out in.

   Needs a pointer to open a card with and the room to draw one (the query
   below) — without either, the plain grid above is the whole block, with every
   caption already showing and nothing hidden behind a hover, which is the only
   honest thing to do where a hover never happens.

   With both, every app is a square card whatever the shape: the *card* is
   shared, and only how the cards are placed differs.

   Every card is narrower than its square at rest and grows to the full square
   on hover, taking that width off the cards beside it on its own line, which
   part around it. Only the number of lines differs:

     - One line (no `.approw--grid`): the block itself is that line. How many
       apps still fit is settled against the real width after mount — see
       approwPerLine() in js/cms-render.js.

     - Several (`.approw--grid`): each line is a `.approw__line` of its own,
       written into the DOM rather than left to the browser to wrap. A card can
       only take width off the cards beside it if the browser agrees which
       those are: left to wrap on its own, a card that grew would push its
       line's last app onto the next line and reflow every line under it.
       Spelled out, each line shares its width exactly as a single line does,
       and nothing below the open card moves.

   Written the other way up from how it reads on a desktop, with the plain grid
   as the base and all of this layered on top. That order is not a style
   preference: the first cut had the row as the base and the grid as the
   override, and a 900px window (narrow enough for the grid, still a real
   pointer) kept firing the hover rules inside it, drawing half a card around a
   grid cell. Anything scoped here simply does not exist outside it, so there
   is nothing to undo and no way to miss a piece. */
@media (min-width: 981px) and (hover: hover) {
  /* ---- the card itself, identical in both shapes ---- */
  .approw__item {
    position: relative;
    /* Square is the one hard constraint: --approw-card is both the width a
       card opens to and the height it always has, so there is no pair of
       numbers that can drift apart. The height is set at rest too — a card
       that grew taller on hover would shunt the rest of the page down. */
    height: var(--approw-card);
    gap: 0;
    /* Where the stack is laid out from. Sized so the logo and the text below
       it sit centred in the square once the card is *open* — see
       .approw__text's `top`, which is this plus the logo plus the gap between
       them — with --approw-shift below carrying the difference between that
       and where the shorter resting stack has to sit to look centred too. */
    padding: 28px 12px 0;
    /* Read by the logo and the label, which are the two things that move: one
       is in the flow and the other is positioned, so there is no single box to
       shift and no wrapper to add without changing the markup. Setting it here
       and letting both transforms pick it up means the two can never disagree
       about where the stack is. */
    --approw-shift: var(--approw-rest-shift);
    border-radius: 22px;
    transition: flex-basis 0.28s ease, flex-shrink 0.28s ease, background-color 0.28s ease,
      box-shadow 0.28s ease;
  }
  .approw__item:hover,
  .approw__item:focus-visible {
    /* An open card has the caption to fit as well, and 28px is the padding
       that was sized for exactly that, so the stack rises back to it — which
       is the same movement that opens the room the line fades into. */
    --approw-shift: 0px;
    background: var(--paper);
    /* The hairline is what keeps the card readable in dark mode, where --paper
       is nearly the page colour and a shadow alone shows almost nothing. */
    box-shadow: 0 20px 44px -20px rgba(11, 56, 79, 0.3), 0 2px 10px -4px rgba(11, 56, 79, 0.12),
      inset 0 0 0 1px var(--line);
    /* Over its neighbours, so the shadow falls on them rather than under. */
    z-index: 2;
  }
  /* In dark mode --paper is the page colour itself, so a card painted with it
     is distinguished only by its hairline — a shadow cast on an almost-black
     background shows next to nothing. --pill is the one step up from the page
     the palette already defines, which is exactly the job here. */
  :root[data-theme="dark"] .approw__item:hover,
  :root[data-theme="dark"] .approw__item:focus-visible { background: var(--pill); }
  /* An app with no line under it has nothing to make room for, so its stack
     stays put through the hover rather than rising to open a gap that is never
     filled. :has() is the whole test: no caption element is rendered at all
     when there is neither text nor a prompt to show for it (see appRowItem in
     js/cms-render.js). A browser without :has() keeps the lift, which is the
     behaviour this block already shipped with and not a broken one. */
  .approw__item:not(:has(.approw__cap)):hover,
  .approw__item:not(:has(.approw__cap)):focus-visible { --approw-shift: var(--approw-rest-shift); }

  .approw__ic {
    transform: translateY(var(--approw-shift));
    transition: transform 0.28s ease;
  }
  .approw__item:hover .approw__ic { transform: translateY(var(--approw-shift)) scale(1.1); }

  /* Out of the item's flow, and as wide as the card's inside whatever the item
     is currently doing — so the name wraps the same way in both states and the
     caption never has to re-wrap as the card opens. In the row at rest it is
     wider than its own column and overhangs into the gaps beside it, which is
     what lets a long name like "Gemini Notebook" stay on one line at that
     density. */
  .approw__text {
    position: absolute;
    /* Derived, not measured off a screenshot: the item's own top padding, plus
       the logo box, plus the gap under it. Change the logo size and the label
       still clears it. This is where the label sits with the card open; at rest
       --approw-shift takes it down the square with the logo. */
    top: calc(28px + var(--approw-ic) + 12px);
    left: 50%;
    transform: translate(-50%, var(--approw-shift)) scale(var(--approw-rest));
    transform-origin: top center;
    width: var(--approw-label);
    transition: transform 0.28s ease;
  }
  .approw__item:hover .approw__text,
  .approw__item:focus-visible .approw__text { transform: translate(-50%, var(--approw-shift)) scale(1); }
  /* The resting label is the card's own type scaled down, not a smaller type.
     Animating font-size between two sizes re-wraps the text partway through
     the transition — a name that fits on one line small takes two lines big,
     and the reflow lands as a snap no easing can smooth over. A transform
     never reflows: the wrap is computed once, at full size, and both states
     show the same lines. It also means one measure to reason about instead of
     two, and the shrink is what keeps a 23px display heading from being the
     row rather than a label on it. `top center` so the label's top edge stays
     put while it grows, rather than drifting up out of its own position. */
  .approw__name {
    font-family: var(--display);
    /* A flat size rather than .tile h3's clamp(19px, 1.5vw, 23px). Two words of
       display caps at 23px in a 200px card is a headline where a card title was
       wanted, and it is the same type scaled down that has to read as a label
       at rest. Same family, weight and tracking as the tile — one step down in
       size, because this box is a third of a tile's width. */
    font-size: 17px;
    font-weight: 600;
    letter-spacing: -0.015em;
    line-height: 1.2;
    color: var(--muted);
    /* Splits a two-line name evenly instead of leaving one word stranded
       under a full line. Ignored by browsers that don't have it, which just
       get the ordinary greedy wrap. */
    text-wrap: balance;
    transition: color 0.28s ease;
  }
  .approw__item:hover .approw__name,
  .approw__item:focus-visible .approw__name { color: var(--heading); }
  /* Two lines here, not the three the plain grid allows: the card is a fixed
     square and the name above it may already be taking two of its own, so this
     is what fits under the worst case without the last line sliding out through
     the bottom edge. A description longer than that ends in an ellipsis —
     this line is a label for the app, and the field's hint says so. */
  .approw__cap {
    -webkit-line-clamp: 2;
    line-clamp: 2;
    margin-top: 10px;
    font-size: 15px;
    line-height: 1.62;
    opacity: 0;
    transition: opacity 0.2s ease;
  }
  .approw__item:hover .approw__cap,
  .approw__item:focus-visible .approw__cap { opacity: 1; }
  /* The editor-only "Add a line" placeholder (see appRowItem in
     js/cms-render.js) rides the same reveal as a real caption rather than
     .tile__cap-empty's flat 0.45 — that rule is written for a plain grid tile,
     where the caption is always on screen, and it would otherwise leave the
     prompt showing at rest on every app while the real captions stay hidden.
     Restated on hover because .tile__cap-empty is declared further down this
     file and wins the tie at equal specificity. */
  .approw__cap.tile__cap-empty { opacity: 0; }
  .approw__item:hover .approw__cap.tile__cap-empty,
  .approw__item:focus-visible .approw__cap.tile__cap-empty { opacity: 0.45; }

  /* ---- the line, whether it is the block itself or one of several ---- */
  .approw:not(.approw--grid),
  .approw__line {
    display: flex;
    /* Never wrap. A line holds what applyAppRowShapes() put on it and no more,
       which is the whole reason the lines are spelled out: a card growing on a
       line the browser wrapped by itself would spill its last app onto the
       next line and reflow everything under it. */
    flex-wrap: nowrap;
    justify-content: center;
    align-items: center;
    gap: 0;
    height: var(--approw-card);
    /* The card is wider than the column it grows from, so the outermost one
       reaches half that difference past the row's own edge. */
    padding-inline: calc((var(--approw-card) - var(--approw-col)) / 2);
  }
  .approw:not(.approw--grid) > .approw__item,
  .approw__line > .approw__item {
    flex: 0 1 var(--approw-col);
    min-width: 0;
  }
  .approw:not(.approw--grid) > .approw__item:hover,
  .approw:not(.approw--grid) > .approw__item:focus-visible,
  .approw__line > .approw__item:hover,
  .approw__line > .approw__item:focus-visible {
    /* `0 0` — grow *and* shrink both off. The row is normally over-subscribed,
       so every item is being shrunk by the flex algorithm; leaving the hovered
       one shrinkable meant it took its share of that too and settled a couple
       of dozen pixels under its own basis, which is a card that is not square.
       With shrink off the whole shortfall lands on its neighbours, which is
       where it was always meant to — and is what makes the row part around the
       card instead of the card floating over it. */
    flex: 0 0 var(--approw-card);
  }

  /* ---- several of them: a plain stack, since each line lays itself out ---- */
  /* Set only once applyAppRowShapes() has actually dealt the apps into lines.
     Until then — and in the HTML that ships prerendered — the block is still
     the plain grid above, which is a fine thing to paint for one frame and a
     far better one than 25 apps stacked one to a row. */
  .approw[data-approw-lines] { display: block; }
  /* Between lines only. Side by side the cards butt together, so an open card
     casts its shadow over its neighbours the way it always has; down the page
     they need the daylight, or a card opening on the second line paints
     straight onto the one above it. */
  .approw__line + .approw__line { margin-top: 14px; }
}

/* ---------- Offer card (Course card / Service card) ----------
   One picture, one column of copy, and the software marked inside the picture
   rather than listed in the copy — see offerCard() in js/cms-render.js for why.
   Both block types render the same markup, so everything here is written
   against .ocard rather than against either data-block-type. */
.ocard {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
  gap: clamp(28px, 4vw, 60px);
  /* Both columns start at the top of the row rather than centring on each
     other — the picture's sticky travel below is measured from the top of the
     row, and a centred figure would begin its journey already halfway down
     whatever room it had. */
  align-items: start;
}
/* The figure column is the one that moves; the copy stays put and simply
   fills whichever track is left, so nothing inside it has to know. The rail
   swaps sides with it, so the count always sits on the outer edge of the row
   rather than getting pinched into the gap between the picture and the copy. */
.ocard--flip .ocard__figure { order: 2; }
.ocard--flip .ocard__rail { order: 2; }

/* ---- picture side ---- */
/* The picture rides the scroll: it holds its place on screen while the copy
   column travels past it, and comes to rest the moment the copy runs out —
   so it moves down through the whole of the right-hand column and stops at
   its end, then walks back up again on the way out.

   Sticky rather than a scroll-driven tween. A grid item's sticky containing
   block is its own row track, which is exactly as tall as the taller of the
   two columns, so "stop at the end of the copy" is the browser's default
   behaviour here rather than a height this file has to measure and keep
   measuring. It also costs no JavaScript, tracks the scroll in both
   directions for free, and cannot end up stranded mid-travel if GSAP fails to
   load. This is the same pattern .feature__copy already uses on hand-written
   pages (css/style.css), mirrored — there the copy sticks and the picture
   scrolls.

   `top` matches .feature__copy's 18vh so a page mixing the two doesn't have
   two different resting heights. Nothing is set for prefers-reduced-motion:
   sticky isn't animation, it's a resting position — the picture never moves
   relative to the reader's eye, which is the opposite of what that setting
   guards against. */
.ocard__figure {
  display: flex;
  align-items: stretch;
  gap: clamp(12px, 1.4vw, 20px);
  min-width: 0;
  position: sticky;
  top: 18vh;
  align-self: start;
}
.ocard__rail {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding-top: 4px;
}
.ocard__statv { font-family: var(--display); font-size: 21px; font-weight: 600; color: var(--heading); line-height: 1.1; }
.ocard__statl { font-size: 12.5px; color: var(--muted); margin-bottom: 12px; }
.ocard__faces { display: flex; flex-direction: column; }
/* Overlapped rather than spaced: a stack of portraits reads as "a group of
   people" at a glance, where a neat column of circles reads as a list. */
.ocard__face + .ocard__face { margin-top: -12px; }
.ocard__face {
  width: 46px;
  height: 46px;
  border-radius: 50%;
  overflow: hidden;
  border: 2px solid var(--paper);
  background: var(--pill);
  box-shadow: 0 6px 14px -8px rgba(11, 56, 79, 0.5);
}
.ocard__face img { width: 100%; height: 100%; object-fit: cover; display: block; }

.ocard__media {
  position: relative;
  flex: 1;
  min-width: 0;
  --cmsfig-radius: 24px;
}
.ocard__media .cmsfig {
  border-radius: var(--cmsfig-radius);
  overflow: hidden;
  box-shadow: 0 30px 60px -34px rgba(11, 56, 79, 0.45);
}

/* The software marks. A sibling of .cmsfig rather than a child, so a picture
   swapped for a video (or for the empty pick-a-picture slot) leaves them
   exactly where they were. */
.ocard__apps {
  position: absolute;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  max-width: calc(100% - 28px);
  z-index: 1;
}
.ocard__apps--tl { top: 14px; left: 14px; }
.ocard__apps--tr { top: 14px; right: 14px; justify-content: flex-end; }
.ocard__apps--bl { bottom: 14px; left: 14px; }
.ocard__apps--br { bottom: 14px; right: 14px; justify-content: flex-end; }
/* A light plate under every mark, in both themes: these sit on a photograph,
   not on the page, so there is no page background for them to inherit
   contrast from — and most software logos are drawn for a light backing. */
.ocard__app {
  width: 38px;
  height: 38px;
  flex: none;
  display: grid;
  place-items: center;
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.92);
  box-shadow: 0 6px 18px -8px rgba(11, 56, 79, 0.6), inset 0 0 0 1px rgba(255, 255, 255, 0.7);
  color: var(--brand-dark);
  transition: transform 0.2s ease;
}
.ocard__app img { width: 24px; height: 24px; object-fit: contain; display: block; }
.ocard__app svg { width: 20px; height: 20px; }
a.ocard__app:hover { transform: translateY(-2px); }

/* ---- copy side ---- */
.ocard__copy { min-width: 0; display: flex; flex-direction: column; align-items: stretch; }
.ocard__head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
.ocard__title {
  font-family: var(--display);
  font-size: clamp(24px, 2.2vw, 34px);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.18;
  color: var(--heading);
  margin: 0;
}
.ocard__badge {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 13px;
  border: 1px solid var(--line);
  border-radius: 999px;
  font-size: 13.5px;
  font-weight: 600;
  color: var(--heading);
  white-space: nowrap;
}
.ocard__badge svg { width: 15px; height: 15px; color: var(--brand); }

.ocard__person { display: flex; align-items: center; gap: 12px; margin-top: 20px; }
.ocard__avatar { width: 46px; height: 46px; border-radius: 14px; object-fit: cover; flex: none; background: var(--pill); }
.ocard__ptext { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.ocard__pname { font-weight: 600; font-size: 15px; color: var(--heading); }
.ocard__prole { font-size: 13px; color: var(--muted); }

.ocard__desc { margin-top: 22px; font-size: 15.5px; line-height: 1.65; color: var(--muted); }
/* .price-card__list carries no bottom margin of its own — same reason
   .feature__copy sets one above, and the same value, so the two blocks space
   a checklist against what follows it identically. */
.ocard__copy .price-card__list { margin-top: 22px; margin-bottom: 0; }

.ocard__meta {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(155px, 1fr));
  gap: 12px;
  margin-top: 26px;
}
.ocard__chip {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 13px 16px;
  border: 1px solid var(--line);
  border-radius: 16px;
}
.ocard__chipk { font-size: 13.5px; color: var(--muted); }
.ocard__chipv { font-size: 14.5px; font-weight: 600; color: var(--heading); }

.ocard__buy {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 14px clamp(16px, 2vw, 28px);
  margin-top: 26px;
  padding: 14px 16px 14px clamp(18px, 2vw, 26px);
  border-radius: 22px;
  background: var(--brand);
  color: #fff;
}
.ocard__prices { display: flex; align-items: baseline; gap: 12px; }
.ocard__was { font-size: 15px; opacity: 0.66; }
.ocard__now { font-family: var(--display); font-size: clamp(22px, 2vw, 30px); font-weight: 600; letter-spacing: -0.02em; }
.ocard__acts { display: flex; flex-wrap: wrap; gap: 10px; }
/* The bar sets its own background, so the button variants have to be
   re-pointed against it rather than against the page: .btn--dark is the brand
   blue this bar already is, and .btn--ghost is a near-white fill with a --line
   hairline, which on brand blue is a white button that only looks like a
   mistake. Both land on the white-on-brand pair a solid/outline choice is
   asking for anywhere else — filled and outlined, in that order.
   `background` and `border-color` are both restated on the ghost: its own
   rule sets a fill, so recolouring only the border would leave white text on
   an almost-white button.

   Each one is paired with a :root[data-theme="dark"] twin because that is
   what css/style.css uses to re-point the same two variants for dark mode —
   a one-class-heavier selector that wins over a plain .ocard__buy .btn rule
   however far down the cascade it sits, which is what put the brand-blue
   button back on the brand-blue bar. This bar is the same colour in both
   themes, so both twins want the same values. */
.ocard__buy .btn--dark,
:root[data-theme="dark"] .ocard__buy .btn--dark {
  background: #fff;
  border-color: #fff;
  color: var(--brand-dark);
}
.ocard__buy .btn--dark:hover,
:root[data-theme="dark"] .ocard__buy .btn--dark:hover {
  background: #eaf6fc;
  border-color: #eaf6fc;
  box-shadow: none;
}
.ocard__buy .btn--ghost,
:root[data-theme="dark"] .ocard__buy .btn--ghost {
  background: transparent;
  border-color: rgba(255, 255, 255, 0.55);
  color: #fff;
}
.ocard__buy .btn--ghost:hover,
:root[data-theme="dark"] .ocard__buy .btn--ghost:hover {
  background: rgba(255, 255, 255, 0.16);
  border-color: #fff;
}

@media (max-width: 980px) {
  .ocard { grid-template-columns: 1fr; }
  /* Stacked, the picture leads — an offer is looked at before it is read —
     so the flip only has a job while the two sit side by side. */
  .ocard--flip .ocard__figure { order: 0; }
  /* One column means the picture's row track is its own height, so there is
     nothing to travel through — sticky would only pin it to the top of the
     viewport for the length of its own box, which on a phone is the picture
     covering the copy someone is trying to read. */
  .ocard__figure { position: static; }
}
@media (max-width: 620px) {
  /* The rail turns into a row above the picture: 46px portraits down the side
     of a phone-width picture would take a third of it. */
  .ocard__figure { flex-direction: column; }
  .ocard__rail { flex-direction: row; align-items: center; gap: 10px; padding-top: 0; }
  .ocard__statl { margin-bottom: 0; }
  .ocard__faces { flex-direction: row; }
  .ocard__face + .ocard__face { margin-top: 0; margin-left: -12px; }
  .ocard__head { flex-direction: column; }
  .ocard__buy { align-items: stretch; }
  .ocard__acts .btn { width: 100%; justify-content: center; }
}

/* ---------- Auto-populated tile caption ----------
   Only ever rendered in the editor (see companyProductTile in
   js/cms-render.js): a tile whose caption nobody has written yet still needs
   something to click, since clicking it is how the field is reached. Dimmed
   so it reads as a prompt rather than as copy someone left behind — it never
   reaches the live page, so this styling is for the canvas alone. */
.tile__cap-empty { opacity: 0.45; font-style: italic; }

/* ---------- Divider ---------- */
.cmsrule { border: 0; border-top: 1px solid var(--line); }
.cmsrule--dots { border: 0; height: 4px; background: radial-gradient(circle, var(--line) 1.6px, transparent 1.7px) 0 0/14px 4px repeat-x; }

/* ---------- Company products grid — editor canvas only. A product with no
   published page yet still shows, so the admin can see it's coming, marked
   so it reads as "not live" rather than as a broken tile. ---------- */
.tag--draft { background: var(--pill); color: var(--muted); border: 1px solid var(--line); margin-top: 12px; }

/* ---------- Embedded HTML ---------- */
.cmsembed :is(iframe, video, img) { max-width: 100%; }

/* ============================================================
   The editable canvas — preview only.

   Everything below is scoped to [data-cms-preview], which js/cms-render.js sets
   on <html> only when the page is running inside the editor. A visitor's page
   never matches any of it.

   The governing rule: nothing here may change layout. Outlines, not borders;
   overlays, not inserted elements in the flow. A canvas that reflows when you
   point at it stops being a preview of anything.
   ============================================================ */

[data-cms-preview] .cmsb::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  outline: 0 solid transparent;
  transition: outline-color 0.12s, outline-width 0.12s;
  z-index: 4;
}
[data-cms-preview] .cmsb:hover::after { outline: 1.5px solid color-mix(in srgb, var(--brand) 45%, transparent); }
[data-cms-preview] .cmsb.is-selected::after { outline: 2px solid var(--brand); }
[data-cms-preview] .cmsb.is-selected { z-index: 3; }
[data-cms-preview] .cmsb.is-dragging { opacity: 0.4; }

/* Hidden sections are still shown in the canvas, greyed, because you have to be
   able to find one to unhide it. Visitors don't get the block at all. */
[data-cms-preview] .cmsb.is-hidden { opacity: 0.42; filter: saturate(0.4); }

/* Locked sections render at full strength — nothing about how the page looks
   should change — but get a standing dashed outline so "why won't this
   select" has an answer right on the canvas, not just in the layers panel. */
[data-cms-preview] .cmsb.is-locked::after {
  outline: 1.5px dashed color-mix(in srgb, var(--ink-3, #7d8d9c) 60%, transparent);
}

/* On a linked copy, the section this page added itself. Every other section on
   that canvas belongs to the original, so this is the one worth marking — a
   soft brand edge and a corner label, at full strength like the lock above,
   because the point is to say whose it is without changing how it looks. */
[data-cms-preview] .cmsb.is-copyown::after {
  outline: 1.5px solid color-mix(in srgb, var(--brand, #0e8fc8) 45%, transparent);
  outline-offset: -2px;
}

[data-cms-preview] .cmsb.is-copyown > .cmsb__inner::before {
  content: "Only on this page";
  position: absolute;
  top: 0;
  left: 12px;
  z-index: 3;
  padding: 2px 8px;
  border-radius: 0 0 6px 6px;
  font: 500 11px/1.6 var(--font-ui, system-ui, sans-serif);
  letter-spacing: 0.02em;
  color: #fff;
  background: var(--brand, #0e8fc8);
  pointer-events: none;
}

/* ---------- Section toolbar ----------
   Rendered inside the section it controls, so it tracks the section's own
   position with no coordinate maths and no scroll syncing. */
.cmsb__bar {
  position: absolute;
  top: 0;
  right: 12px;
  z-index: 6;
  display: flex;
  align-items: center;
  gap: 1px;
  padding: 3px;
  border-radius: 0 0 10px 10px;
  background: #0b2230;
  box-shadow: 0 8px 22px -10px rgba(6, 18, 26, 0.7);
  opacity: 0;
  transform: translateY(-4px);
  transition: opacity 0.14s ease, transform 0.14s ease;
  pointer-events: none;
  font-family: var(--body);
}
[data-cms-preview] .cmsb:hover > .cmsb__bar,
[data-cms-preview] .cmsb.is-selected > .cmsb__bar,
[data-cms-preview] .cmsb:focus-within > .cmsb__bar {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}
/* A component nested in a frame carries none of a standalone section's own
   top padding (see the `nested` padding rule in js/cms-render.js) — there is
   no padding zone left for the bar to sit in above the content the way there
   is on a real page section, so at top:0 it sat directly over whatever the
   component draws first. For something small there — the "Add a picture"
   prompt on an empty image node, say — the bar covered it completely and ate
   every click meant for the button underneath. Floating it above the
   section's own box instead, in the frame's own gap, keeps it clear of
   content regardless of how little (or how much) padding that content has. */
.cmsn--component > .cmsb > .cmsb__bar {
  top: -38px;
  border-radius: 10px;
}
/* While dragging, only the section being moved keeps its toolbar — the others
   lighting up under the cursor is noise. */
.is-cms-dragging .cmsb:not(.is-dragging) > .cmsb__bar { opacity: 0 !important; pointer-events: none !important; }

.cmsb__bar button {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 0;
  border-radius: 6px;
  background: none;
  color: rgba(219, 230, 238, 0.85);
  cursor: pointer;
  transition: background 0.12s ease, color 0.12s ease;
}
.cmsb__bar button svg { width: 14px; height: 14px; display: block; }
.cmsb__bar button:hover:not(:disabled) { background: rgba(255, 255, 255, 0.12); color: #fff; }
.cmsb__bar button:disabled { opacity: 0.3; cursor: default; }
.cmsb__bar button:focus-visible { outline: 2px solid var(--brand); outline-offset: -1px; }
.cmsb__bardanger:hover:not(:disabled) { background: #c0362c !important; color: #fff !important; }
.cmsb__baradd:hover:not(:disabled) { background: var(--brand) !important; color: #fff !important; }

.cmsb__grip { cursor: grab !important; }
.cmsb__grip:active { cursor: grabbing !important; }
.is-cms-dragging, .is-cms-dragging * { cursor: grabbing !important; user-select: none !important; }

.cmsb__barname {
  padding: 0 8px 0 4px;
  color: rgba(219, 230, 238, 0.75);
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
}
.cmsb__barsep { width: 1px; height: 16px; margin: 0 3px; background: rgba(255, 255, 255, 0.14); }

/* ---------- Editable text ----------
   A dotted underline rather than a box: it says "this is text you can change"
   without drawing a form field over a designed page. */
[data-cms-preview] [data-cms-field] {
  border-radius: 3px;
  outline-offset: 2px;
  transition: background 0.12s ease, box-shadow 0.12s ease;
  /* Live CSS turns pointer-events off on some wrappers on purpose — the Hero's
     .hero__content (css/style.css) does it so clicks reach the WebGL backdrop
     underneath it, restoring it only for the <a> tags inside. That left the
     Hero's eyebrow and sub-heading with no way in: they're text/textarea
     fields that hide their sidebar input once they hold a value (see
     FieldRenderer's "field--oncanvas" case) and rely on being clicked
     in-place, so an ancestor with pointer-events: none makes them silently
     uneditable rather than merely unstyled. Forcing it back on here, once,
     for every field marker, is the fix for this case and for the next block
     that turns pointer-events off on a wrapper for its own reasons.
   */
  pointer-events: auto;
}
/* A button's own label is a data-cms-field too — the CTA text is what's
   editable — so without this, the rule above's border-radius: 3px (two
   attribute selectors) outranks .btn's 999px (one class) and every
   CMS-generated button loses its rounding, but only inside the canvas: the
   live site never carries [data-cms-preview] at all. One class higher than
   the rule above is enough to win regardless of source order. */
[data-cms-preview] [data-cms-field].btn {
  border-radius: 999px;
}
[data-cms-preview] [data-cms-field]:hover {
  box-shadow: inset 0 -1px 0 0 color-mix(in srgb, var(--brand) 45%, transparent);
}
[data-cms-preview] [data-cms-field]:focus {
  outline: 2px solid var(--brand);
  background: color-mix(in srgb, var(--brand) 7%, transparent);
  box-shadow: none;
}
/* An emptied field would otherwise collapse to a zero-width target nobody can
   click back into. Reserve enough of a box to aim at. */
[data-cms-preview] [data-cms-field]:empty {
  display: inline-block;
  min-width: 5ch;
  min-height: 1em;
  box-shadow: inset 0 -1px 0 0 color-mix(in srgb, var(--brand) 45%, transparent);
}

/* ---------- Editable media ---------- */
[data-cms-preview] [data-cms-image] { cursor: pointer; transition: outline-color 0.12s ease; outline: 2px solid transparent; outline-offset: 3px; }
[data-cms-preview] [data-cms-image]:hover { outline-color: var(--brand); }

/* ---------- Individual elements ----------
   A card, a plan, a FAQ row, a button. Their outline is quieter than a
   section's — you are inside something now, and two identical highlights would
   not tell you which. */
[data-cms-preview] [data-cms-part] {
  position: relative;
  outline: 1px dashed transparent;
  outline-offset: 3px;
  transition: outline-color 0.12s ease;
  /* Same reasoning as [data-cms-field] above: an ancestor like .hero__content
     turning pointer-events off for the live site would otherwise make
     clicking to select a part (the Hero's title, its button row) silently
     do nothing inside a block like that. */
  pointer-events: auto;
}
[data-cms-preview] [data-cms-part]:hover {
  outline-color: color-mix(in srgb, var(--brand) 55%, transparent);
}
[data-cms-preview] [data-cms-part].is-part-selected {
  outline: 2px solid var(--brand);
  outline-offset: 3px;
}
[data-cms-preview] [data-cms-part].is-dragging { opacity: 0.35; }

/* Positioned against the selected element by script, but parented to the block
   — putting it inside the element would inject markup into whatever text is
   being edited. left/top are set inline. */
.cmspartbar {
  position: absolute;
  z-index: 7;
  display: flex;
  align-items: center;
  gap: 1px;
  padding: 2px;
  border-radius: 8px;
  background: var(--brand);
  box-shadow: 0 6px 16px -6px rgba(6, 18, 26, 0.6);
  font-family: var(--body);
}
.cmspartbar button {
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 0;
  border-radius: 5px;
  background: none;
  color: rgba(255, 255, 255, 0.88);
  cursor: pointer;
  transition: background 0.12s ease, color 0.12s ease;
}
.cmspartbar button svg { width: 12px; height: 12px; display: block; }
.cmspartbar button[hidden] { display: none; }
.cmspartbar button:hover { background: rgba(255, 255, 255, 0.22); color: #fff; }
/* The element's own path, so it is always obvious what is selected — "cards.1"
   is more use than "Card" when there are three grids on the page. */
.cmspartbar__name {
  padding: 0 7px 0 3px;
  color: rgba(255, 255, 255, 0.9);
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.02em;
  white-space: nowrap;
}
.cmspartbar__grip { cursor: grab !important; }
.cmspartbar__grip:active { cursor: grabbing !important; }
.cmspartbar__danger:hover { background: #c0362c !important; }

/* ---------- Empty picture / icon slot ---------- */
.cmsslot {
  display: flex;
  gap: 6px;
  margin-bottom: 14px;
}
.cmsslot button {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 6px 10px;
  border: 1px dashed var(--line);
  border-radius: 10px;
  background: none;
  color: var(--muted);
  font-family: var(--body);
  font-size: 12.5px;
  font-weight: 600;
  cursor: pointer;
  transition: border-color 0.14s ease, color 0.14s ease, background 0.14s ease;
}
.cmsslot button svg { width: 14px; height: 14px; }
.cmsslot button:hover {
  border-color: var(--brand);
  color: var(--brand);
  background: color-mix(in srgb, var(--brand) 8%, transparent);
}

/* ---------- Drop indicator ---------- */
.cmsdrop {
  position: relative;
  height: 3px;
  margin: -1.5px 0;
  border-radius: 3px;
  background: var(--brand);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--brand) 18%, transparent);
  z-index: 8;
}
.cmsdrop::before {
  content: "";
  position: absolute;
  left: -5px;
  top: 50%;
  width: 11px;
  height: 11px;
  transform: translateY(-50%);
  border-radius: 50%;
  background: var(--brand);
}
/* Elements laid out side by side get a vertical bar instead — the indicator has
   to point the way the row runs or it reads as the wrong gap. */
.cmsdrop--v {
  width: 3px;
  height: auto;
  min-height: 40px;
  align-self: stretch;
  margin: 0 -1.5px;
}
.cmsdrop--v::before {
  left: 50%;
  top: -5px;
  transform: translateX(-50%);
}

/* ============================================================
   Direct manipulation — js/cms-edit.js's overlay.

   One fixed layer above the page holding handles, guides, distances and the
   size badge. Fixed rather than absolute because getBoundingClientRect and a
   fixed layer already share a coordinate space, so nothing here has to know
   about scroll; and *above* the content rather than inside it because half of
   these elements are contenteditable and a div dropped into a heading becomes
   part of the heading.

   Nothing in here participates in layout — the canvas must not reflow because
   something was selected.
   ============================================================ */
/* With the overlay in charge, the element's own rings have to go.
   `.is-part-selected` draws a 2px outline at a 3px offset and a focused text
   field draws another — stacked under the overlay's box that becomes three
   rectangles around one element, none of which the handles sit on. The overlay
   box is the selection now; the caret and the brand tint still say which text
   is being typed into.

   Scoped to `.is-cms-direct`, which js/cms-edit.js sets on itself: if that file
   ever fails to load, the outlines below are the only thing showing what is
   selected and must stay exactly as they were. */
[data-cms-preview].is-cms-direct [data-cms-part].is-part-selected,
[data-cms-preview].is-cms-direct [data-cms-part].is-part-selected:hover {
  outline-color: transparent;
}
/* The section keeps its own ring only while *it* is what is selected. Once the
   selection has moved to something inside it, the overlay is drawing that
   thing's box, and a second full-width rectangle around the whole section is
   just another border to work out the meaning of. */
[data-cms-preview].is-cms-direct .cmsb.is-selected.has-part::after {
  outline-color: transparent;
}
[data-cms-preview].is-cms-direct [data-cms-field]:focus {
  outline: none;
}
/* Grabbable, and unmistakably so — this is the affordance that says the thing
   can be dragged, which is the whole gesture the canvas was missing.

   Text included. It only shows a caret once you are actually inside it, which
   is what the second click is for; until then it is a layer like any other and
   has to say so. `:not([contenteditable])` is what lets it say so: without it,
   this rule and the `cursor: text` one below both match a field that is
   currently being edited, and this one's higher specificity was silently
   winning — so the caret never actually showed once you were typing. */
[data-cms-preview].is-cms-direct [data-cms-part].is-part-selected:not([contenteditable]),
[data-cms-preview].is-cms-direct .cmsn.is-selected:not([contenteditable]),
[data-cms-preview].is-cms-direct .cmsb.is-selected > .cmsb__inner {
  cursor: move;
}
[data-cms-preview] [data-cms-field][contenteditable] { cursor: text; }
/* Mid-edit, the rest of the page must not look draggable — but not the field
   being typed into: same tie as above, equal specificity to the `cursor:
   text` rule, and this one came later so it was winning there too, right
   where hovering the caret back over your own words most wants to say so. */
.is-cms-typing [data-cms-part].is-part-selected:not([contenteditable]) { cursor: default; }
.is-cms-moving, .is-cms-moving * { user-select: none !important; cursor: move !important; }
.is-cms-moving [data-cms-part].is-moving { opacity: 0.85; z-index: 5; }

/* Auto-layout, while dragging a node: the neighbours slide out of the way to
   open the slot the node would drop into, so the gap you are aiming at is the
   gap you get. Safe because js/cms-edit.js decides the slot from geometry it
   captured *before* any of this ran — the movement here can never feed back
   into the answer and walk the target out from under the pointer.

   The dragged element is excluded: it is being written a fresh transform every
   pointermove, and a transition on that is a node that lags the hand. */
.is-cms-moving [data-frame] > .cmsn,
.is-cms-moving #cms-root > [data-block-id] {
  transition: transform 0.16s cubic-bezier(0.2, 0, 0, 1);
}
.is-cms-moving .is-moving { transition: none !important; }
@media (prefers-reduced-motion: reduce) {
  .is-cms-moving [data-frame] > .cmsn,
  .is-cms-moving #cms-root > [data-block-id] { transition: none; }
}

.cmsedit {
  position: fixed;
  inset: 0;
  z-index: 2147483000;
  pointer-events: none;
  display: none;
  font-family: var(--body);
  --cmsedit-ink: var(--brand);
  --cmsedit-measure: #e5484d;
}
.cmsedit.is-on { display: block; }

/* Every child positions itself with a transform from the top-left corner, so
   one rule covers all of them and no child needs its own left/top. */
.cmsedit > *,
.cmsedit__ghosts > *,
.cmsedit__guides > *,
.cmsedit__measure > * {
  position: absolute;
  left: 0;
  top: 0;
}
.cmsedit__ghosts,
.cmsedit__guides,
.cmsedit__measure {
  position: absolute;
  inset: 0;
}

/* ---------- The selected element's box ----------
   The only ring around a selected element, so it can afford to be definite. */
.cmsedit__box {
  box-shadow: 0 0 0 2px var(--cmsedit-ink);
  border-radius: 2px;
}
.cmsedit__ghost {
  box-shadow: 0 0 0 1.5px color-mix(in srgb, var(--cmsedit-ink) 45%, transparent);
  border-radius: 2px;
}

/* ---------- Handles ----------
   Corners set a width ceiling; sides set padding, or margin with Alt. The hit
   target is deliberately larger than the dot you can see: eight of these on a
   small card is already a crowded 20 pixels. */
.cmsedit__h {
  width: 14px;
  height: 14px;
  margin: -7px 0 0 -7px;
  pointer-events: auto;
  touch-action: none;
}
.cmsedit__h::after {
  content: "";
  position: absolute;
  inset: 4px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 0 0 1.5px var(--cmsedit-ink), 0 1px 3px rgba(6, 18, 26, 0.4);
  transition: inset 0.1s ease;
}
.cmsedit__h:hover::after,
.is-cms-handling .cmsedit__h::after { inset: 2px; }
/* The side handles are bars, not dots — they run along the edge they adjust,
   which is what says "this one is about that whole side". */
.cmsedit__h--n::after,
.cmsedit__h--s::after { inset: 5px 3px; border-radius: 3px; }
.cmsedit__h--e::after,
.cmsedit__h--w::after { inset: 3px 5px; border-radius: 3px; }
.cmsedit__h[hidden] { display: none; }

/* ---------- Size badge ---------- */
.cmsedit__hud {
  /* The `translate` property, not `transform`: script owns `transform` here, and
     the two compose (translate is applied first) instead of overwriting each
     other. A percentage resolves against this element's own box, which is the
     whole point — a margin percentage would resolve against the viewport. */
  translate: -50% 0;
  padding: 2px 7px;
  border-radius: 5px;
  background: var(--cmsedit-ink);
  color: #fff;
  font-size: 11px;
  font-weight: 600;
  line-height: 1.5;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  transform-origin: top left;
}
.cmsedit__hud[hidden] { display: none; }

/* ---------- The number under the cursor while dragging ---------- */
.cmsedit__readout {
  padding: 3px 8px;
  border-radius: 6px;
  background: #0b2230;
  color: #fff;
  font-size: 11.5px;
  font-weight: 600;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  box-shadow: 0 6px 18px -8px rgba(6, 18, 26, 0.7);
}
.cmsedit__readout.is-snapped { background: var(--cmsedit-ink); }
.cmsedit__readout[hidden] { display: none; }

/* ---------- Where a dragged element would land ----------
   Drawn in the overlay rather than inserted between the siblings — an element
   in the flow would change the very geometry the slot is computed from. The
   siblings do now slide aside to open that slot (see .is-cms-moving above), but
   they are translated, which costs no layout and is measured from geometry
   captured before the drag started. */
.cmsedit__insert {
  background: var(--cmsedit-ink);
  border-radius: 3px;
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--cmsedit-ink) 20%, transparent);
  /* Matches the peer transition above so the line arrives with the gap it is
     marking rather than snapping ahead of it — a marker that jumps while the
     siblings it is between are still gliding reads as two contradictory
     answers to "where does this land". */
  transition: transform 0.16s cubic-bezier(0.2, 0, 0, 1), width 0.16s, height 0.16s;
}
.cmsedit__insert[hidden] { transition: none; }
@media (prefers-reduced-motion: reduce) {
  .cmsedit__insert { transition: none; }
}

/* ---------- Alignment guides ---------- */
.cmsedit__guide {
  width: 100%;
  height: 1px;
  background: var(--cmsedit-measure);
}
.cmsedit__guide--v { width: 1px; height: 100%; }

/* ---------- Distances, shown while Alt is held ---------- */
.cmsedit__span { background: var(--cmsedit-measure); }
.cmsedit__span--h { height: 1px; }
.cmsedit__span--v { width: 1px; }
.cmsedit__dist {
  translate: -50% 0;
  min-width: 28px;
  padding: 1px 5px;
  border-radius: 4px;
  background: var(--cmsedit-measure);
  color: #fff;
  font-size: 10.5px;
  font-weight: 700;
  line-height: 1.6;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

/* While measuring, the page stops reacting to the pointer — you are reading it,
   not editing it, and a hover state firing under the cursor is a distraction. */
.is-cms-measuring [data-cms-part] { outline-color: transparent !important; }
.is-cms-panning, .is-cms-panning * { cursor: grab !important; user-select: none !important; }
/* A drag in progress must not be interrupted by the text caret the pointer
   would otherwise be putting into whatever it passes over. */
.is-cms-handling, .is-cms-handling * { user-select: none !important; }

/* ============================================================
   Frames and nodes

   A frame is a container an editor made, rather than one a section type
   dictated. Everything positional is emitted inline by the renderer — this
   file only supplies what every node of a kind has in common, so that the
   inline style stays readable and stays about *this* node.
   ============================================================ */
.cmsframe { min-width: 0; }
.cmsframe--free > .cmsn { margin: 0; }

.cmsn { min-width: 0; }
.cmsn--text .cmsn__t {
  margin: 0;
  /* The value is one string and keeps its own line breaks — the editable
     region is the whole paragraph, so the breaks have to survive the round
     trip through it. */
  white-space: pre-wrap;
}
.cmsn--text h1, .cmsn--text h2, .cmsn--text h3, .cmsn--text h4 {
  font-family: var(--display);
  color: var(--heading);
  line-height: 1.15;
}
.cmsn__i { width: 100%; height: 100%; max-width: 48px; max-height: 48px; color: var(--brand); }
.cmsn__img { display: block; width: 100%; height: 100%; object-fit: cover; border-radius: inherit; }
.cmsn--component > .cmsb { padding: 0; }

/* A frame with nothing in it has no size of its own to be seen at, and an
   invisible container is one nobody can drop anything into. Preview only. */
[data-cms-preview] .cmsframe.is-empty {
  min-height: 96px;
  border: 1px dashed var(--line);
  border-radius: 12px;
  display: grid;
  place-items: center;
}
.cmsframe__hint { color: var(--muted); font-size: 13px; }

/* The frame a dragged node would land in. Outlined rather than filled: what is
   already inside it has to stay readable while you decide. */
[data-cms-preview] .is-drop-target {
  outline: 2px solid var(--brand);
  outline-offset: -2px;
  border-radius: 12px;
  background: color-mix(in srgb, var(--brand) 7%, transparent);
}

/* An empty picture node, same idea. */
.cmsn__ph {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  height: 100%;
  min-height: 64px;
  justify-content: center;
  padding: 10px;
  border: 1px dashed var(--line);
  border-radius: 10px;
  background: none;
  color: var(--muted);
  font-family: var(--body);
  font-size: 12.5px;
  font-weight: 600;
  cursor: pointer;
}
.cmsn__ph svg { width: 16px; height: 16px; }
.cmsn__ph:hover { border-color: var(--brand); color: var(--brand); }

/* ---------- A height that was set by hand ----------
   Not preview-only: the height is part of the page, so it has to mean the same
   thing to a visitor as it does to the person who dragged it.

   A picture keeps its own aspect ratio. Give its container a height it did not
   ask for and the container obeys while the picture does not, so the extra
   height becomes a band of dead space underneath — inside the selection box,
   which makes it read as the resize having gone wrong rather than as the
   picture declining to stretch. Filling the box is what dragging a box's edge
   means; the crop is what `cover` is for. */
[data-sized~="h"] {
  display: flex;
  flex-direction: column;
}
[data-sized~="h"] > img,
[data-sized~="h"] > picture,
[data-sized~="h"] > picture > img,
[data-sized~="h"] > a > img {
  flex: 1 1 auto;
  /* Without this a flex item refuses to shrink below its content size, and an
     image that is taller than the box would push the box back open. */
  min-height: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* When the sized thing *is* the picture, min-height already gives it the box
   and only the crop is missing. */
img[data-sized~="h"] { object-fit: cover; }

/* Empty page / empty block placeholders, preview only. */
.cmsempty {
  display: grid;
  place-items: center;
  gap: 10px;
  min-height: 320px;
  padding: 60px 24px;
  text-align: center;
  color: var(--muted);
}
.cmsempty__t { font-family: var(--display); font-size: 20px; color: var(--heading); }
.cmsempty__d { font-size: 14.5px; max-width: 38ch; line-height: 1.6; }

/* Unknown block type — a page saved with a section type this deploy doesn't
   know about must still render everything else, and must say so out loud in the
   editor rather than silently dropping content. */
.cmsunknown {
  border: 1px dashed var(--line);
  border-radius: 14px;
  padding: 20px;
  color: var(--muted);
  font-family: var(--mono);
  font-size: 13px;
  text-align: center;
}

/* ---------- Under construction ----------
   The holding page a live-but-unfinished page shows instead of its sections
   (js/cms-render.js, renderConstruction). Not preview-only: this is what a
   visitor gets, and the editor's canvas draws the identical thing.

   It is the whole page, so like the breadcrumb it has to clear the fixed nav
   when nothing else is above it — hence the :first-child rule rather than a
   blanket padding, which would leave a hole under a breadcrumb.

   Everything here is drawn from the site's own tokens: the illustration is a
   wireframe of a page mid-build, and the tape across it is the one piece of
   deliberate signage. Nothing spins or slides under prefers-reduced-motion. */
.cmswip {
  padding: clamp(36px, 5vh, 64px) var(--gutter) clamp(72px, 10vh, 128px);
  text-align: center;
}
.cmswip:first-child { padding-top: calc(72px + clamp(28px, 5vh, 64px)); }
.cmswip__inner {
  display: grid;
  justify-items: center;
  gap: 14px;
  max-width: 620px;
  margin-inline: auto;
}

/* The illustration. Sized in ch-free absolutes so the tape angle reads the same
   on every screen; the SVG itself scales inside it. */
.cmswip__art {
  position: relative;
  width: min(420px, 100%);
  margin-bottom: clamp(10px, 2vh, 22px);
  color: var(--ink);
}
.cmswip__scene { display: block; width: 100%; height: auto; overflow: visible; }
.cmswip__paper { fill: var(--paper); stroke: var(--line); stroke-width: 2; }
.cmswip__rule { stroke: var(--line); stroke-width: 2; }
.cmswip__dotm { fill: currentColor; opacity: 0.18; }
.cmswip__bar { fill: currentColor; opacity: 0.22; }
.cmswip__bar--faint { opacity: 0.14; }
.cmswip__bar--head { fill: var(--brand); opacity: 0.85; }
.cmswip__bar--btn { fill: var(--brand); opacity: 0.28; }

/* The section that isn't there yet: marching ants around an empty slot. */
.cmswip__slot {
  fill: none;
  stroke: var(--brand);
  stroke-width: 2;
  stroke-dasharray: 6 6;
  opacity: 0.55;
  animation: cmswip-ants 1.4s linear infinite;
}
.cmswip__plus { stroke: var(--brand); stroke-width: 2.4; stroke-linecap: round; opacity: 0.55; }

.cmswip__gear {
  transform-box: fill-box;
  transform-origin: center;
  animation: cmswip-spin 9s linear infinite;
}
.cmswip__gear--slow { animation-duration: 6s; animation-direction: reverse; }
.cmswip__gear-ring { fill: none; stroke: currentColor; stroke-width: 4; opacity: 0.24; }
.cmswip__gear-teeth { stroke: currentColor; stroke-width: 3.4; stroke-linecap: round; opacity: 0.34; }
.cmswip__gear-hub { fill: var(--brand); opacity: 0.85; }

/* Tape across the page: brand stripes rather than hazard yellow, which would be
   the one thing on the site that isn't in the palette. */
.cmswip__tape {
  position: absolute;
  left: -4%;
  right: -4%;
  top: 56%;
  height: clamp(18px, 5.5%, 26px);
  transform: rotate(-7deg);
  border-block: 1px solid color-mix(in srgb, var(--brand) 45%, transparent);
  background:
    repeating-linear-gradient(
      115deg,
      color-mix(in srgb, var(--brand) 85%, transparent) 0 11px,
      color-mix(in srgb, var(--brand) 22%, var(--paper)) 11px 22px
    );
  background-size: 44px 100%;
  box-shadow: 0 10px 22px -14px rgba(11, 56, 79, 0.75);
  animation: cmswip-tape 1.6s linear infinite;
}

.cmswip__eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  padding: 6px 14px 6px 10px;
  border-radius: 999px;
  background: var(--pill);
  color: var(--brand);
  font-family: var(--body);
  font-size: 12.5px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.cmswip__pulse {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--brand);
  animation: cmswip-pulse 2s ease-in-out infinite;
}
.cmswip__title {
  margin: 0;
  font-family: var(--display);
  font-size: clamp(28px, 4.4vw, 44px);
  line-height: 1.1;
  color: var(--heading);
}
.cmswip__note {
  margin: 0;
  max-width: 46ch;
  color: var(--muted);
  font-size: clamp(15px, 1.6vw, 17px);
  line-height: 1.65;
}
.cmswip__acts {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 12px;
  margin-top: 10px;
}
/* ---------- The canvas banner (editor only) ----------
   A page under construction still gets built, so the canvas keeps drawing and
   editing the real sections; this is what says so, and what switches between
   the two things the canvas can show. It is the first child of #cms-root when
   it is there, so it carries the fixed nav's clearance and the breadcrumb
   behind it gives that clearance up. */
.cmswipbar {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  margin: calc(72px + clamp(18px, 2.6vh, 34px)) var(--gutter) 0;
  padding: 12px 14px;
  border: 1px dashed color-mix(in srgb, var(--brand) 45%, transparent);
  border-radius: 14px;
  background: color-mix(in srgb, var(--brand) 8%, var(--paper));
  color: var(--ink);
  font-family: var(--body);
  font-size: 14px;
  line-height: 1.5;
}
.cmswipbar__ic {
  display: inline-flex;
  flex: none;
  color: var(--brand);
}
.cmswipbar__ic svg { width: 20px; height: 20px; }
.cmswipbar__t { flex: 1 1 320px; }
.cmswipbar__b {
  flex: none;
  padding: 7px 13px;
  border: 1px solid color-mix(in srgb, var(--brand) 45%, transparent);
  border-radius: 999px;
  background: var(--paper);
  color: var(--brand);
  font-family: var(--body);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition: background-color 0.2s, color 0.2s;
}
.cmswipbar__b:hover { background: var(--brand); color: #fff; }
.cmswipbar + .cmscrumb { padding-top: clamp(14px, 2vh, 24px); }

@keyframes cmswip-ants { to { stroke-dashoffset: -24; } }
@keyframes cmswip-spin { to { transform: rotate(360deg); } }
@keyframes cmswip-tape { to { background-position: 44px 0; } }
@keyframes cmswip-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.35; transform: scale(0.72); }
}

@media (prefers-reduced-motion: reduce) {
  .cmswip__slot,
  .cmswip__gear,
  .cmswip__tape,
  .cmswip__pulse { animation: none; }
}

/* ---------- Breadcrumb ----------
   Sits above the first section on any page below a section landing page.

   The site nav is position: fixed, so anything at the very top of the document
   would sit underneath it. The strip therefore carries the nav's own height as
   top padding: 14px of .nav__inner padding above and below the tallest thing in
   it, which is the 44px control cluster, not the 30px logo — measured, because
   guessing from the logo puts the first breadcrumb 14px underneath the nav. It
   is repeated rather than shared because style.css has no token for it, and
   inventing one there would mean editing a file this feature has no other
   business in.

   The nav does not shrink on a phone, so there is no breakpoint on this.

   On a phone a three-level trail can outrun the screen. It scrolls sideways
   rather than wrapping or truncating: every step stays reachable, and the
   overflow is the hint that there is more. */
.cmscrumb {
  --cmscrumb-nav: 72px;
  padding: calc(var(--cmscrumb-nav) + clamp(18px, 2.6vh, 34px)) var(--gutter) 0;
}
.cmscrumb__list {
  display: flex;
  align-items: center;
  gap: 2px;
  max-width: 1440px;
  margin-inline: auto;
  padding: 0;
  list-style: none;
  font-size: 13.5px;
  line-height: 1;
  overflow-x: auto;
  scrollbar-width: none;
}
.cmscrumb__list::-webkit-scrollbar { display: none; }
.cmscrumb__i {
  display: flex;
  align-items: center;
  gap: 2px;
  white-space: nowrap;
}
.cmscrumb__l,
.cmscrumb__c {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 5px 8px;
  border-radius: 8px;
  color: var(--muted);
  text-decoration: none;
  transition: color 0.2s, background-color 0.2s;
}
.cmscrumb__l:hover { color: var(--brand); background: var(--pill); }
.cmscrumb__c { color: var(--heading); font-weight: 600; }
.cmscrumb__ic { width: 15px; height: 15px; flex: none; opacity: 0.9; object-fit: contain; border-radius: 2px; }
.cmscrumb__sep {
  display: inline-flex;
  color: var(--muted);
  opacity: 0.5;
}
.cmscrumb__sep svg { width: 13px; height: 13px; }

/* ---------- Responsive ---------- */
@media (max-width: 900px) {
  .cmsb { --cmsb-pt: clamp(48px, 9vh, 96px); --cmsb-pb: clamp(48px, 9vh, 96px); }
  .cmslist--cols { grid-template-columns: 1fr; }
}

@media (prefers-reduced-motion: reduce) {
  .cmslogos img { transition: none; }
}
