/* ==========================================================================
   CapInteract — site styles
   Mobile-first (§9). Type switches at 767px via tokens.css.

   FIVE breakpoints, and they deliberately do NOT coincide. Each answers a
   different question, so collapsing them onto one number breaks something:
     767   type scale                                            (tokens.css)
     768   horizontal insets 16 -> 24
           EVERY diagram asset swaps to desktop — rows via <picture>,
           hero via .hero__figure/.herodiagram
     900   footer top row goes two-column
    1024   horizontal insets 24 -> 48
    1200   capability rows go two-column
    1344   hero two-column for a 496 figure  (.hero--fig-496, Ci and Guardian)
    1376   hero two-column for a 539 figure  (.hero--fig-539, GP and LP)

   ASSET swaps and LAYOUT swaps are separate questions and separate numbers.
   Every asset switches at 768, because that is where the desktop file first fits
   the column. The layouts split later, at 1200 and 1344, because that is where
   two columns first fit side by side. Between them a page is stacked while
   already serving desktop artwork, which is correct — tying the two together is
   what once rendered a diagram at 56% and another at 67%.

   §9 has the measured tables.

   Every colour comes from tokens.css. No hex literals here.

   The box model, UA margin removal, the baseline focus ring and the motion
   floor now live in css/reset.css, which loads first.
   ========================================================================== */

body {
  font-family: var(--font-body);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  font-weight: var(--weight-body);
  color: var(--text-primary);
  background: var(--surface-default);
}

/* ---- Shared section scaffolding ---------------------------------------- */

.hero, .herodiagram, .value, .caps, .more, .fits, .cta,
.trust, .showcase, .notfound, .thanks, .reading, .offices {
  padding-block: var(--section-pad-block);
}

/* Horizontal insets, three steps, verified in a browser at 375 / 768 / 810 /
   820 / 1024 / 1080 / 1180 / 1200 / 1280 / 1366 / 1440.

   Element selectors rather than a class list: every band on every page is a
   <section> and the footer is a <footer>, so a new section picks the ladder up
   without anyone remembering to add it to a selector list. This is why the class
   rule above sets padding-BLOCK only — a class would outrank `section` and the
   ladder would never apply.

   NOTE the nav does not follow this ladder. It stays on --section-pad-inline,
   which steps 16 -> 48 at 767, so between 768 and 1023 the nav is inset 48 while
   the sections are inset 24. See §8 on why the nav has its own value, and the
   report for why that band is worth a look. */
:root { --pad-inline: 16px; }
@media (min-width: 768px)  { :root { --pad-inline: 24px; } }
@media (min-width: 1024px) { :root { --pad-inline: 48px; } }
section, footer { padding-inline: var(--pad-inline); }
.hero__inner, .herodiagram__inner, .value__inner,
.caps__inner, .more__inner, .fits__inner, .cta__inner,
.trust__inner, .showcase__inner, .notfound__inner, .thanks__inner,
.pagehead__inner, .formband__inner, .legalband__inner, .offices__inner {
  max-width: var(--inner-max);
  margin-inline: auto;
}

/* textCase: UPPER in Figma. No visual change on the section eyebrows today,
   because the strings read from Figma are already uppercase — THE CORE,
   CAPABILITIES. It is here so a future title-case string cannot render wrong.
   Presentational only: the DOM keeps whatever case the copy has, so assistive
   tech reads the word rather than spelling out capitals. */
.eyebrow {
  font-size: var(--fs-eyebrow);
  line-height: var(--lh-eyebrow);
  font-weight: var(--weight-body-strong);
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--text-accent);
}
.eyebrow--on-dark { color: var(--brand-accent); }

/* 64 flat, NOT --feature-row-gap. The row gap is 96/32 and this clearance is 64
   at both breakpoints on all four product pages — read off Figma's Section Header
   paddingBottom on eight artboards, not inferred. Borrowing the row-gap token made
   the header 32 too loose on desktop and 32 too tight on mobile, on every product
   page, since the day .caps was built. Same shape as D6 on the homepage: a shared
   spacing token doing a job the design specifies as a constant. Register D14.
   .more overrides this to 48 below, which IS Figma's value for that header. */
.section-head { text-align: center; margin-bottom: var(--gap-after-section-head); }
.section-head__title {
  font-family: var(--font-display);
  font-weight: var(--weight-display);
  font-size: var(--fs-section-title);
  line-height: var(--lh-section-title);
  color: var(--text-primary);
  margin-top: 0;
}

/* The eyebrow gap belongs BETWEEN an eyebrow and a title, so it is expressed as
   one. It used to sit unconditionally on .section-head__title, and three More
   headers have no eyebrow — GP's, Guardian's and LP's — so there the 12px had
   nothing above it, collapsed through .section-head's top edge and pushed the
   whole header down, making each More section 12px taller than Figma's Section
   Heading Wrap. Ci is unaffected (it uses .more__title) and so is the homepage
   (its section-head does carry an eyebrow), which is why this went unseen.
   Register D16. */
.eyebrow + .section-head__title { margin-top: var(--gap-after-eyebrow); }

/* ---- Buttons ----------------------------------------------------------- */

/* 48, NOT 44 — O15, fixed 2026-08-11. This is the site-wide `CTA −4` this file
   carried as an unexplained constant for three sessions.

   IT IS A HEIGHT RATHER THAN PADDING, AND THAT IS A TRANSCRIPTION, NOT A TASTE.
   Every variant of the library Button 27:195 (file ZfEMeKnC2KGMpmhaTquefu) has
   paddingTop and paddingBottom of ZERO and counterAxisSizingMode FIXED: Small is
   32, Medium 40, Large 48, and the height is declared, not derived. `.btn` is the
   Large variant — 16/24 text, 48 tall. Bumping padding to 12px would have hit the
   same number by inventing a vertical padding Figma does not have, and the next
   person to compare the two would have found a difference that is not in the file.

   min-height rather than height, because a wrapped label must still be allowed to
   grow. align-items: center was already here and is what centres the 24px line in
   the 48px box.

   HORIZONTAL PADDING IS DELIBERATELY LEFT AT 20 AND FIGMA SAYS 24. That is a real
   divergence and it is O22, not an oversight: it changes button WIDTHS, which is a
   different blast radius from this one and was not measured for here. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 48px;
  padding: 10px 20px;
  border-radius: var(--radius-card);
  font-size: var(--fs-button);
  line-height: var(--lh-button);
}
.btn--sm { min-height: 44px; padding: 6px 12px; font-size: 14px; line-height: 20px; }
.btn--accent { background: var(--action-accent);  color: var(--text-on-accent); }
.btn--navy   { background: var(--action-primary); color: var(--text-on-dark); }
/* No .btn:focus-visible for the ring — reset.css gives every focusable element
   the same two-tone one, and the old .btn rule was identical to it. */

/* ---- Interactive states (§11) -----------------------------------------
   Read from the library Button set 27:195, State: Default | Hover | Pressed |
   Disabled. Hover binds to action/accent-hover and action/primary-hover, which
   is what these use.

   Colour only, 150ms, and never transform — a button that moves under the
   cursor is harder to hit, not more responsive. No reduced-motion rule is
   needed here: the motion floor in reset.css already forces
   transition-duration to 0.01ms, which removes the fade and keeps the colour
   change. That is the right split, because the colour is information and the
   fade is decoration.

   :focus-visible is on every state selector, not just :hover. Without it a
   keyboard user gets the focus ring but no state change at all. */
.btn { transition: background-color 150ms ease, color 150ms ease; }

.btn--accent:hover, .btn--accent:focus-visible { background: var(--action-accent-hover); }
.btn--navy:hover,   .btn--navy:focus-visible   { background: var(--action-primary-hover); }

/* PRESS — a decision, not a transcription. Figma's Pressed variants bind to the
   same -hover variables as Hover, so a literal reading gives no press feedback.
   Returning to the base colour is a real change with no new value invented and
   no motion: both hovers are LIGHTER than their base, so a press reads as a
   darkening. Do not replace this with a transform. */
.btn--accent:active { background: var(--action-accent); }
.btn--navy:active   { background: var(--action-primary); }

/* TEXT LINKS — also a decision. Figma models no link hover anywhere: Nav
   Desktop and Nav Mobile carry State: Default|Scrolled and Collapsed|Menu Open,
   the footer sets carry Theme only, and no component in either file has a hover
   variant or a prototype reaction. So there was nothing to transcribe.

   Underline rather than a colour shift, because it is the one signal that works
   unchanged on navy and on light and cannot fail a contrast check — it inherits
   whatever colour the link already has. */
.nav__sublink:hover,  .nav__sublink:focus-visible,
.footer__link:hover,  .footer__link:focus-visible,
.footer__social:hover, .footer__social:focus-visible { text-decoration: underline; text-underline-offset: 3px; }

/* ==========================================================================
   Nav (§8)
   ========================================================================== */

/* Visually hidden but announced. Used for the new-tab warning on external
   links (WCAG 2.2 SC 3.2.5 Change on Request). */
.u-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  padding: 12px 16px;
  background: var(--surface-default);
  color: var(--text-primary);
}
.skip-link:focus { left: 0; }

.nav {
  position: sticky;
  top: 0;
  z-index: 50;
  will-change: transform;
  transition: transform 200ms ease, opacity 200ms ease;
}

/* ONE exception, and only at mobile without JS. There the panel renders in flow, so
   the header is ~376px tall — sticky, that would permanently cover more than half a
   667px phone screen. Desktop keeps sticky with or without JS, since CSS delivers it
   unaided and nothing there grows. */
@media (max-width: 899px) {
  html:not(.js) .nav { position: static; }
}
/* Figma 249:305 distributes THREE children with space-between — the logo, the
   link list, and the CTA group. The build had the last two grouped together on
   the right, which put the links at the wrong x entirely.

   The nav is also NOT capped at --inner-max. Figma spans it edge to edge with a
   48px inset, so at 1440 the content box is 1344 wide (48 -> 1392), not the
   1280 the page sections use. The cap is removed to match, which is what makes
   the measured coordinates reproduce.

   ABOVE 1440 — SETTLED 2026-08-10 by Zoran's rule, applied here 2026-08-11
   (O3 -> D25): "design >1440 stays as is; only the background color expands."
   So the INNER is capped at the design width and centred, and .nav keeps its
   full-bleed background. Logo, links and CTA freeze at the positions they hold
   at 1440 rather than tracking the viewport edge.

   CAPPED AT 1440, NOT AT 1376, AND THAT IS THE WHOLE POINT. 1376 would be
   1280 + 2x48 and would align the logo with the sections' 1280 column — but
   the nav does not use that column: Figma 249:305 spans it edge to edge, so
   its content box is 1344 at 1440 and the logo sits at x 48 while hero content
   sits at x 80. MEASURED, both before and after: they are 32px apart at 1440
   and have never been flush. Capping at 1376 would move the logo to 80 at
   1440, changing the one width Figma actually specifies in order to fix a
   width it does not. Freezing the real design keeps that 32px relationship at
   every width above it, which is what "stays as is" means. */
.nav__inner {
  min-height: 64px;
  max-width: var(--design-max);
  margin-inline: auto;
  padding-inline: var(--section-pad-inline);
  display: flex;
  align-items: center;
  justify-content: space-between;
}

/* Page themes. The inverted variants are applied by js/nav.js on return. */
.nav--navy  { background: var(--surface-brand);   color: var(--text-on-dark); }
.nav--light { background: var(--surface-default); color: var(--text-primary); }
.nav--navy.is-inverted  { background: var(--surface-default); color: var(--text-primary); }
.nav--light.is-inverted { background: var(--surface-brand);   color: var(--text-on-dark); }

/* THE OPEN MOBILE PANEL'S BACKGROUND DEPENDS ON THIS LINE. Measured defect at
   375: with the menu open, .nav__menu computed rgba(0,0,0,0) and the page showed
   through it in all four states.

   The panel says `background: inherit`, and `inherit` reads the IMMEDIATE PARENT
   ONLY — which is .nav__inner, and .nav__inner painted nothing. The nav's actual
   background lives one level up, on the four rules directly above. So the panel
   was inheriting transparency from a box that had never been given a colour.

   One line rather than four, and by construction: .nav__inner inherits from
   .nav, the panel inherits from .nav__inner, and any of the four theme or
   inversion states reaches the panel without being restated. Adding a fifth
   state later needs nothing here.

   REJECTED: a `--nav-surface` custom property on the four selectors, mirroring
   what --nav-indicator does below. It is more robust — custom properties inherit
   through any depth, so an element inserted between .nav and .nav__menu would not
   break it — but it means naming every colour twice, once for `background` and
   once for the variable, which is a drift risk of its own. The chain is the
   smaller change and the one that cannot disagree with itself.

   THE FRAGILITY IS REAL AND WORTH KNOWING: this works because the chain
   .nav -> .nav__inner -> .nav__menu is unbroken. A new wrapper between any two of
   them reintroduces the same transparent-parent bug, with the same symptom —
   page content visible through an open menu. If that happens, take the custom
   property instead of adding a second `background: inherit`.

   DESKTOP IS UNAFFECTED AND THAT WAS MEASURED, not assumed: at 900+ .nav__inner
   is the grid and .nav__menu is display:contents, so this paints the same colour
   over the same box the nav already painted. Grid columns and all three item
   positions are byte-identical before and after. */
.nav__inner { background: inherit; }

/* The open-item indicator follows the SURFACE, not the page theme, and the four
   rules mirror the background rules above line for line. That is the whole
   reason there are two orange tokens: §8 inverts the nav on scroll return, so
   BOTH surfaces occur on EVERY page and neither value alone would ever be
   enough. Bound to the surface here, so nothing downstream has to know. */
.nav--navy              { --nav-indicator: var(--border-active-on-dark);  }
.nav--light             { --nav-indicator: var(--border-active-on-light); }
.nav--navy.is-inverted  { --nav-indicator: var(--border-active-on-light); }
.nav--light.is-inverted { --nav-indicator: var(--border-active-on-dark);  }

.nav.is-hidden   { transform: translateY(-100%); }
/* §8 gives the shadow as the navy at 10 percent opacity. That navy is
   --surface-brand, so it is mixed here rather than restated as a literal —
   no colour value lives outside tokens.css. Tune the offsets by eye. */
.nav.is-shadow   { box-shadow: 0 3px 8px color-mix(in srgb, var(--surface-brand) 10%, transparent); }
.nav.is-resolving { transition: background-color 220ms ease, color 220ms ease; }

/* Logo crossfade: two stacked images, opacity only — never a src swap. */
/* The four lockups are one library component, true size 202.08 x 30.787,
   ratio 6.56381. Figma's exporter rounds the viewBox up to 203 x 31, so the
   markup declares the TRUE dimensions and the artwork sits inside that
   viewBox with a fraction of transparent slack. The nav shows the lockup at
   its Figma nav size of 210 x 32; object-fit keeps the 0.2 percent ratio
   difference as letterboxing rather than distortion. */
.nav__logo { position: relative; display: block; width: 210px; height: 32px; flex: none; }
.nav__logo-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; transition: opacity 220ms ease; }
.nav--navy  .nav__logo-img--white { opacity: 1; }
.nav--navy  .nav__logo-img--navy  { opacity: 0; }
.nav--light .nav__logo-img--white { opacity: 0; }
.nav--light .nav__logo-img--navy  { opacity: 1; }
.nav--navy.is-inverted  .nav__logo-img--white { opacity: 0; }
.nav--navy.is-inverted  .nav__logo-img--navy  { opacity: 1; }
.nav--light.is-inverted .nav__logo-img--white { opacity: 1; }
.nav--light.is-inverted .nav__logo-img--navy  { opacity: 0; }

.nav__menu { display: flex; align-items: center; gap: 32px; }
.nav__links { display: flex; align-items: center; gap: 32px; }
/* No `gap`: the chevron was its only consumer and it is gone. Every .nav__link is
   now a single text node, and .nav__sublink's second span is display:none on
   mobile and display:block inside a block-layout panel on desktop, so neither
   ever had two in-flow children to separate. */
.nav__link, .nav__sublink {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}
/* A real <button>, because it performs an action rather than navigating. It used to
   be an <a href="/gp-interact.html">, so clicking it left the page instead of
   opening anything. reset.css already gives it font and colour inheritance. */
.nav__trigger { background: none; border: 0; cursor: pointer; }

/* MOBILE: a nested list, open by default. The panel is already collapsed behind
   the toggle, and a second layer of tapping to reach four pages is not worth it. */
.nav__sub { display: block; padding-left: 16px; margin-top: 4px; }
.nav__sub .nav__sublink { min-height: 40px; }

/* The descriptions are a DESKTOP-PANEL feature. display:none rather than
   visually-hidden is deliberate: mobile should not read them out either, so
   they leave the accessibility tree with the box. Mobile is unchanged - a
   nested list of four names, open by default inside the collapsed panel. */
.nav__subdesc { display: none; }
.nav__ctas { display: flex; align-items: center; gap: 16px; }

/* No margin-left:auto — space-between on .nav__inner already pins the toggle to
   the right, since the menu is display:none while collapsed.

   display:none by default and grid only under .js: without JS the hamburger does
   nothing, and a dead control is worse than no control. */
.nav__toggle {
  width: 44px;
  height: 44px;
  display: none;
  place-content: center;
  gap: 5px;
  background: none;
  border: 0;
  cursor: pointer;
}
.nav__toggle-bar { width: 22px; height: 2px; background: currentColor; }

/* ---- The mobile panel, and the last load-bearing script on the site -------
   The hamburger used to be the only thing standing between a no-JS visitor and the
   whole nav: with scripting off, .nav__menu was display:none at 375 and the nav
   offered the logo alone. Same fix as the Solutions dropdown — scope the COLLAPSE
   to .js rather than making visibility the enhancement.

   Without .js: the panel is in flow, wrapping below the logo. The links stack, the
   page gets taller, everything is reachable, and the hamburger is not rendered.
   With .js: it collapses and the toggle overlays it, exactly as before.

   Register C15. Never drop the .js from these two selectors. */
@media (max-width: 899px) {
  .nav__inner { flex-wrap: wrap; }
  .nav__menu {
    flex-direction: column;
    align-items: flex-start;
    gap: 8px;
    width: 100%;
    padding-block: 8px 16px;
  }
  .nav__links { flex-direction: column; align-items: flex-start; gap: 8px; }
  .nav__ctas { padding-top: 8px; }

  .js .nav__toggle { display: grid; }
  .js .nav__menu { display: none; }
  .js .nav.is-open .nav__menu {
    display: flex;
    position: absolute;
    inset-inline: 0;
    top: 100%;
    padding: 16px var(--section-pad-inline) 24px;
    background: inherit;
    color: inherit;
  }
}

@media (min-width: 900px) {
  .nav__inner { min-height: 72px; }

  /* THREE-COLUMN GRID, so the links centre independently of the logo and CTA
     widths. Flex with space-between cannot do this: unequal siblings pull the
     middle item off centre. 1fr auto 1fr puts the links in an auto track between
     two equal tracks, which is centred by construction.

     .nav__menu KEEPS display:contents and the centring goes on .nav__links.
     That is the only way both requirements hold at once: .nav__ctas lives inside
     .nav__menu in the markup, so the wrapper must dissolve for the CTA to be a
     grid item at all — and the wrapper must survive because the mobile panel
     collapses into it. Putting justify-self on .nav__menu instead would make it
     a real box again and swallow the CTA into the centre track.

     With the wrapper dissolved the grid sees logo, links, ctas in source order,
     so they land in columns 1, 2, 3 without explicit placement.

     The .nav.is-open variants are restated deliberately. Without them, resizing
     from an open mobile menu up to desktop leaves the absolutely positioned panel
     styling in force, since .nav.is-open .nav__menu (0,3,0) outranks a bare
     .nav__menu (0,1,0). That bug predates this change. */
  .nav__inner {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
  }
  .nav__menu,
  .nav.is-open .nav__menu { display: contents; }
  .nav__logo  { justify-self: start; }
  .nav__links { justify-self: center; }
  .nav__ctas  { justify-self: end; }
  .nav.is-open .nav__links { flex-direction: row; align-items: center; gap: 32px; }
  .nav__inner { flex-wrap: nowrap; }

  /* DESKTOP: a disclosure dropdown, not a menubar. Four links to four pages are
     not a menu, and role="menu" would promise arrow-key navigation that is not
     implemented and should not be.

     PROGRESSIVE ENHANCEMENT: the collapse is scoped to .js, added by the inline
     head gate. With JS off the class never appears, the rule never applies, and the
     four links stay visible — so they remain reachable rather than being sealed
     behind a button nothing can operate. Never drop the .js from this selector.

     The panel shows off aria-expanded, not off a class, so the accessible state and
     the visual state cannot disagree. */
  /* The links row and the Solutions item both STRETCH to the nav's full height.
     Measured, not styled by eye: the nav is 72 and a nav link is 44, so the
     trigger's own box ends 14px ABOVE the nav's bottom edge. Stretching makes
     two things exact instead of hardcoding that 14:

       - the indicator's `bottom: 0` IS the nav's bottom edge
       - the panel's `top: 100%` IS the nav's bottom edge, so it hangs flush
         below the band instead of starting 14px up inside it

     Both then survive a change to the nav's height or to the link's, which a
     `bottom: -14px` would not. The trigger's contents stay centred because
     .nav__link is an inline-flex with align-items:center. */
  /* Every nav ITEM and LINK stretches to the nav's full height, not just the
     Solutions one. Measured: the nav is 72 and a nav link is 44, so a link's own
     box ends 14px ABOVE the nav's bottom edge. Stretching makes two things exact
     instead of hardcoding that 14:

       - each indicator's `bottom: 0` IS the nav's bottom edge
       - the panel's `top: 100%` IS the nav's bottom edge, so it hangs flush below
         the band instead of starting 14px up inside it

     Both survive a change to the nav's height or to a link's. Contents stay
     centred because .nav__link is an inline-flex with align-items:center. */
  .nav__links { align-self: stretch; }
  .nav__item { display: flex; align-self: stretch; }
  .nav__item--has-sub { position: relative; }
  .nav__link { position: relative; align-self: stretch; }

  /* THE ACTIVE INDICATOR (§8). ONE TREATMENT for every nav link, in every active
     state - hovered, focused, or holding the panel open. The bar means "this
     element is active" and nothing more.

     It does NOT distinguish hovered from open, and that is deliberate: the open
     state is already signalled by a 680x230 panel with a shadow directly beneath
     the trigger. The bar was never carrying that, and a second bar treatment
     would be a distinction without a job.

     On desktop hover and aria-expanded fire together, because hover opens the
     panel - so they resolve to the SAME declaration and there is nothing to
     transition between. On keyboard and on touch they diverge, which is why both
     are listed: without the hover half, hovering About would show a bar while
     Solutions showed nothing until activation.

     3px, chosen against the system rather than by eye:
       - every hairline here is 1px (--border-default rules, the footer divider),
         so 3px is 3x the nearest thing it could be confused with
       - the nav's own shadow is `0 3px 8px`, so 3px is already its vocabulary
       - it carries less contrast on the light nav (3.78:1) than on navy
         (5.06:1), and mass is what compensates for that
       - 4px+ starts reading as a tab bar and competes with the CTA

     It is the only VISUAL signal that a nav item is active - aria-expanded and
     :focus-visible carry it programmatically - so it is held to SC 1.4.11's 3:1
     even though it is not text. Both tokens clear it on their own surface. */
  .nav__link::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 3px;
    background: var(--nav-indicator);
    opacity: 0;
    transition: opacity 150ms ease;
    pointer-events: none;
  }
  .nav__link:hover::after,
  .nav__link:focus-visible::after,
  .nav__trigger[aria-expanded="true"]::after { opacity: 1; }

  .nav__sub {
    position: absolute;
    top: 100%;
    /* Centred on the trigger, not left-aligned to it. Measured at 1440: the
       trigger centre is 681.43 and the CTA starts at 1296.13, so a LEFT-aligned
       panel can be at most 667.6 wide before it collides - the bottom of the
       640-720 range this needs. Centred, 680 spans 341.43-1021.43 and clears the
       CTA by 274.7 and the content column's left edge by 261.4.
       Tightest desktop width is 900, measured: centre 411.43, so 341.43 of
       clearance to the left. The min() is a backstop, not a working constraint -
       it never binds at or above 900. */
    left: 50%;
    transform: translateX(-50%);
    width: min(680px, calc(100vw - 96px));
    z-index: 60;
    margin-top: 0;
    padding: 12px;
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 4px;
    background: var(--surface-default);
    border: 1px solid var(--border-default);
    border-radius: var(--radius-panel);
    /* A white panel on a WHITE light nav has no edge of its own. Elevation is the
       primary separator and the hairline is secondary: no border colour in the
       system reaches 3:1 on white (--border-default is 1.30:1), and it does not
       need to - the panel's boundary is a visual requirement, not an SC 1.4.11
       one. The state signal is the indicator above, which does clear 3:1. */
    /* Same value as .nav.is-shadow, and expressed the same way. §8 documents this
       shadow as a literal rgba, but that is a STARTING VALUE rather than the
       implementation: the colour it names IS --surface-brand, so 10% of that token
       through color-mix is identical and keeps every colour inside tokens.css (§4).
       check.js caught the literal — and then caught it a second time when it was
       written into this comment, because the scan reads comments too. */
    box-shadow: 0 3px 8px color-mix(in srgb, var(--surface-brand) 10%, transparent);
  }
  .js .nav__trigger[aria-expanded="false"] + .nav__sub { display: none; }

  /* The dropdown is a light surface whichever theme the nav is, so its links take
     the light-surface colour rather than inheriting the nav's white. */
  .nav__sub .nav__sublink {
    display: block;
    min-height: 44px;
    padding: 12px;
    border-radius: var(--radius-card);
    color: var(--text-primary);
  }
  /* NOT bold by default, and that is load-bearing rather than a style choice.
     aria-current="page" has always been signalled here by WEIGHT (build.js injects
     it on whichever sub-link matches the page, §8). Making every name semibold
     took that signal away, so the first build replaced it with an underline - and
     an underline is what HOVER means two rules below. The two states then looked
     identical and meant different things, which is what a screenshot of
     gp-interact.html showed: GP's name underlined with nothing hovered.

     Reverted to the original mechanism instead of inventing a third signal. The
     name still leads on size and colour - 16px --text-primary against the
     description's 14px --text-secondary - so the hierarchy survives without it.

     Three states, all distinguishable:
       current page   semibold
       hovered        underlined
       both           semibold AND underlined  */
  .nav__subname { display: block; }
  .nav__subdesc {
    display: block;
    margin-top: 2px;
    font-size: var(--fs-body-sm);
    line-height: var(--lh-body-sm);
    color: var(--text-secondary);
  }

  /* Hover carries BOTH a fill and an underline, and the underline is the
     load-bearing half. --surface-sunken on --surface-default is 1.06:1, which is
     the subtle fill the design asks for but is far too weak to be the only
     affordance. B5's reasoning still applies: underline is the one signal that
     cannot fail a contrast check, because it inherits the colour already there.
     It goes on the NAME only - underlining a two-line block reads as a mistake. */
  .nav__sub .nav__sublink:hover,
  .nav__sub .nav__sublink:focus-visible {
    background: var(--surface-sunken);
    text-decoration: none;
  }
  .nav__sub .nav__sublink:hover .nav__subname,
  .nav__sub .nav__sublink:focus-visible .nav__subname {
    text-decoration: underline;
    text-underline-offset: 3px;
  }
  .nav__sub .nav__sublink[aria-current="page"] .nav__subname { font-weight: var(--weight-body-strong); }
}

/* ==========================================================================
   Product Hero
   ========================================================================== */

.hero { background: var(--surface-brand); color: var(--text-on-dark); }
.hero__name {
  font-family: var(--font-display);
  font-weight: var(--weight-display-max);
  font-size: var(--fs-product-name);
  line-height: var(--lh-product-name);
  margin-top: var(--gap-after-eyebrow);
}
/* THE HOMEPAGE'S H1. Added 2026-08-07, register A50, closing O8.

   THREE hero headline roles now, and they are NOT interchangeable:
     .hero__name      product pages' h1 — a product NAME, "Ci Data Platform"
     .hero__headline  product pages' <p> BELOW that h1 — a secondary line
     .hero__lead      the homepage's h1 — a full sentence, and the largest
                      type on the site

   The homepage wrote .hero__headline on its h1 until today, and that class is
   24px/32 at weight 600 because it was built for a secondary <p>. Figma
   specifies 48/56 at Manrope Bold (42:202) — so the site's most prominent
   element rendered at HALF its size and one weight light, 2 lines at 1440
   where Figma has 4. That is D4's borrow-a-class trap for the third time.

   IT GETS ITS OWN NAME RATHER THAN REUSING .hero__name, deliberately: a
   sentence is not a product name, and renaming the role to fit an available
   class is how the collision happened in the first place.

   SIZE AND WEIGHT ARE SINGLE-SOURCED. It binds the same
   --fs-product-name/--lh-product-name/--weight-display-max triple .hero__name
   uses — 48/56/700 desktop, 40/48/700 mobile — which is exactly Figma's
   h1-hero at both breakpoints (42:202 and 43:207). Two consumers, one set of
   values, nothing to drift.

   Worth knowing, not acted on: the TOKEN name now serves two roles. A
   role-neutral name would be the cleaner end state, but that is a token rename
   across files and is not this change. */
.hero__lead {
  font-family: var(--font-display);
  font-weight: var(--weight-display-max);
  font-size: var(--fs-product-name);
  line-height: var(--lh-product-name);
  margin-top: var(--gap-statement-eyebrow);
}

.hero__headline {
  font-family: var(--font-display);
  font-weight: var(--weight-display);
  font-size: var(--fs-headline);
  line-height: var(--lh-headline);
  margin-top: var(--gap-after-headline);
}
.hero__sub {
  font-size: var(--fs-hero-sub);
  line-height: var(--lh-hero-sub);
  color: var(--text-on-dark-muted);
  margin-top: var(--gap-after-headline);
}
.hero__cta { margin-top: var(--gap-after-subhead); }

/* THE LEAD HERO STEPS AND THE PRODUCT HERO DOES NOT — O10, fixed 2026-08-11.
   Read from the artboards, and they disagree with each other:

     homepage 42:198 / 43:203   eyebrow 16/12   headline 24/16   subhead 32/24
                                subhead type 18/28 desktop, 16/24 mobile
     product  249:306 / 249:413  eyebrow 12     name 16   headline 16   sub 32
                                subhead type 18/28 at BOTH, no step anywhere

   So the product chain above is CORRECT as it stands and is deliberately left
   on the un-stepped --gap-after-* tokens. Only the hero built around a
   .hero__lead h1 — the homepage's and About's — takes the stepping trio.

   Scoped off .hero__lead rather than off a page or figure-width class because
   the lead IS the distinguishing feature: O8/D5 gave the homepage its own h1
   class precisely because a sentence is not a product name, and the gap set
   follows that same split. .hero--fig-500 would have worked today and would
   break the moment another page reuses the figure width. */
.hero__lead + .hero__sub {
  margin-top: var(--gap-statement-headline);
  font-size: var(--fs-hero-subhead);
  line-height: var(--lh-hero-subhead);
}
.hero__lead ~ .hero__cta { margin-top: var(--gap-statement-sub); }

/* Same governing rule as the capability rows (§9): the diagram never scales, the
   layout gives way. Ci's hero diagram is native 496x310 carrying ELEVEN 8px
   labels, and the old 900px two-column threshold rendered it at 67% — putting
   those labels at 5.4px.

   Unlike .row__copy and .row__figure, the hero children carry NO explicit
   grid-column. The grid exists only above 1344 and they fall into its tracks by
   source order, so stacking needs nothing reset. It does need the stack centred,
   which is what justify-items is for. */
.hero__inner { display: grid; gap: var(--feature-gap); justify-items: center; }

/* Both boxes take the SAME 616 measure, which is what lets their left edges
   agree while the stack is centred. The rows get this for free because copy and
   diagram are both 616; here the diagram is 496, so it sits at the left of its
   616 box rather than centred inside it. */
.hero__copy   { width: 100%; max-width: 616px; }

/* VISIBLE BY DEFAULT. GP and LP keep their hero diagram inside the hero at every
   width, served by a <picture>. Ci and Guardian are the exceptions — they have a
   separate mobile hero-diagram section — so THEY opt out, via a class rather than
   anything keyed to a page name. Majority behaviour is the default. */
.hero__figure { width: 100%; max-width: 616px; }
.hero--mobile-split .hero__figure { display: none; }
@media (min-width: 768px) { .hero--mobile-split .hero__figure { display: block; } }

/* Native 496, one rule, no 768 step. This element only ever holds the 496 desktop
   asset — the 343 mobile hero is a separate .herodiagram section — so a 343 cap
   below 768 would scale the desktop file BELOW native, which is the exact thing
   the governing rule forbids. */
/* Native caps per variant, stepped at the same 768 the <picture> switches at.
   --hero-fig is the figure's native width; nothing else needs to know which page
   it is on. */
.hero--fig-496 { --hero-fig: 496px; }
.hero--fig-539 { --hero-fig: 375px; }
@media (min-width: 768px) { .hero--fig-539 { --hero-fig: 539px; } }
.hero__figure img { width: 100%; max-width: var(--hero-fig); height: auto; }

/* GP/LP below 768 hold the 375-wide mobile hero, which is authored full-bleed.
   The figure box takes 375 so the asset is centred at native size rather than
   left-aligned in a 616 box. */
.hero--fig-539 .hero__figure { max-width: 375px; }
@media (min-width: 768px) { .hero--fig-539 .hero__figure { max-width: 616px; } }

/* 375 + 16 + 16 = 407. At 407 and up the content box already holds 375, so ONLY
   320–406 needs to break out. NOT 100vw: that includes the scrollbar, which
   overflows a narrowed desktop window by 15–17px, and it would stretch a 375
   asset to 187% at 700px. The bleed tracks --pad-inline so it cannot drift if
   the ladder changes. width:auto is required — with width:100% the box stays at
   the containing block and the negative margins only shift it. */
@media (max-width: 406px) {
  .hero--fig-539 .hero__figure {
    justify-self: stretch;
    width: auto;
    max-width: none;
    margin-inline: calc(var(--pad-inline) * -1);
  }
}

/* The ASSET switch, deliberately not the layout switch, and it stays at 900.
   It answers "which file suits the space", not "how many columns". So between
   900 and 1343 the hero is stacked AND showing the 496 desktop diagram. */
@media (min-width: 768px) {
  .hero__figure { display: block; }
}

/* 1344 is derived, not chosen: 704 + 48 + 496 = 1248, and a 1248 content band
   needs 1344 minus 96 of inset. At 1343 the tracks shrink and the diagram drops
   under native — which is how the 67%-at-900 bug worked. */
/* Ci and Guardian: 704 + 48 + 496 = 1248, which needs 1344 - 96 of inset. */
@media (min-width: 1344px) {
  .hero--fig-496 .hero__inner {
    grid-template-columns: minmax(0, 704px) minmax(0, 496px);
    align-items: center;
    justify-items: stretch;
  }
  .hero--fig-496 .hero__copy, .hero--fig-496 .hero__figure { max-width: none; }
}

/* GP and LP: 693 + 48 + 539 = 1280 exactly, which needs 1376 - 96. The threshold is
   unchanged by the crop because the total is unchanged.

   A8 is RESOLVED here, not recorded. The asset was cropped to its ink at 539 wide,
   so the artwork sits flush to the content column with no bleed and no negative
   margins, and Figma now matches. The old 640/592 put the 48px gap between the copy
   and 32px of EMPTY canvas, which read as an 80px gap to the artwork.

   Copy track 693: 77 characters of capacity at 18px, but the longest rendered line
   is 678px, which is 75 — exactly on the ceiling, and three lines either way. */
@media (min-width: 1376px) {
  .hero--fig-539 .hero__inner {
    grid-template-columns: minmax(0, 693px) minmax(0, 539px);
    align-items: center;
    justify-items: stretch;
  }
  .hero--fig-539 .hero__copy, .hero--fig-539 .hero__figure { max-width: none; }
}

/* Hero Diagram — its own section on mobile only (Figma 461:338) */
.herodiagram { background: var(--surface-sunken); text-align: center; }
/* 24, not --gap-after-headline's 16. Figma sets itemSpacing 24 between the
   eyebrow and the figure on ALL THREE pages carrying this section — Ci 461:338,
   Guardian 514:338 and LP 389:303 — and each section's height only reconciles at
   24: 64 pad + 16 eyebrow + 24 + 400 figure + 64 pad = 568 for Ci and Guardian,
   and the same arithmetic over a 482 figure = 650 for LP. The 16 was a
   transcription error that made Ci and Guardian 8px short of their artboards
   from the day they were built, unrecorded and never measured. Register D13. */
.herodiagram__figure { margin: var(--gap-before-figure) auto 0; }
/* Native 343x400. Capped explicitly for the same reason as the other two: this
   element only ever holds the mobile asset, so it must not be upscaled past 343
   in the wider part of its range. */
.herodiagram__figure img { margin-inline: auto; max-width: 343px; }

/* LP's mobile hero asset is 375 wide where Ci's and Guardian's are 343, and it is
   authored full-bleed — §5 records that a mobile hero's canvas width EQUALS the
   viewport width by design, so its side margin is edge clearance rather than dead
   space. The shared 343 cap would render it at 91% of native, which is exactly
   what §9's governing rule forbids. Same shape as C14, and the same arithmetic:
   375 + 16 + 16 = 407, so only 320–406 has to break out of the content box. The
   bleed tracks --pad-inline so it cannot drift if the inset ladder changes. */
.herodiagram--fig-375 .herodiagram__figure img { max-width: 375px; }
@media (max-width: 406px) {
  .herodiagram--fig-375 .herodiagram__figure {
    width: auto;
    max-width: none;
    margin-inline: calc(var(--pad-inline) * -1);
  }
}

@media (min-width: 768px) { .herodiagram { display: none; } }

/* ==========================================================================
   Value Statement
   ========================================================================== */

.value { background: var(--surface-default); }
.value__inner { max-width: var(--content-wide); text-align: center; }
.value__text {
  font-size: var(--fs-hero-sub);
  line-height: var(--lh-hero-sub);
  color: var(--text-primary);
}

/* ==========================================================================
   Capabilities — alternating rows
   ========================================================================== */

.caps { background: var(--surface-sunken); }
.caps__rows { display: grid; gap: var(--feature-row-gap); }

/* STACKED IS THE DEFAULT, two columns is the exception above 1200.

   The governing rule: a desktop capability diagram is 616px wide and carries
   10px labels, so it cannot be scaled down — at 500px those labels are 8px and
   unreadable. The diagram does not give way, the LAYOUT does. Everything below
   follows from that.

   1200 is a width threshold with no orientation query, because width alone
   already separates the two postures: every tablet in portrait is at most 1024
   wide, and at 1024 a two-column copy measure would be 264px. See CLAUDE.md §9
   for the measured table this came from. */
.row { display: grid; gap: var(--feature-gap); grid-template-columns: 1fr; justify-items: center; }

/* Both children need resetting, not just one. They carry explicit grid-column
   for the two-column layout — copy 1, figure 2 — and grid-template-columns: 1fr
   on its own does NOT stack them: the figure keeps its column 2 placement and
   grid creates an implicit second track for it. */
.row__copy, .row__figure { grid-column: 1; grid-row: auto; }

/* The copy measure was the worst of the three problems found in testing, worse
   than the diagram scaling. Left full width when stacked it runs to 90 characters
   at 768, 116 at 1024 and 136 at 1180, against a 75-character maximum. 616px
   holds it at 77 at every width — and 616 is the same measure the two-column
   desktop layout uses, so the cap is consistent rather than arbitrary. */
.row__copy { width: 100%; max-width: 616px; }
.row__title {
  font-family: var(--font-display);
  font-weight: var(--weight-display);
  font-size: var(--fs-row-title);
  line-height: var(--lh-row-title);
  color: var(--text-primary);
}
/* 12, not --gap-after-title's 8. A capability row's heading-to-body gap is 12 in
   Figma on all 30 rows across the four product pages and both breakpoints, while a
   More item's really is 8 — so the single token was right for one consumer and
   wrong for the other. Invisible on desktop, where the 616 visual sets the row
   height and absorbs it; on mobile the copy IS the height, so it cost 4px a row.
   Register D15. */
.row__body { color: var(--text-secondary); margin-top: var(--gap-after-row-title); }
/* width:100% is load-bearing, not cosmetic. The <img> carries the MOBILE
   width/height attributes, and those set the layout size for whichever source
   wins — so the desktop file arrived and then rendered at 343 inside the 616
   column. The global max-width:100% cannot catch it: 343 is under 616, so
   there is nothing to clamp. The diagram fills its column at both breakpoints.
   Height comes from the winning source's own ratio, which is why each <source>
   declares width and height too. See CLAUDE.md §5. */
/* max-width caps the asset at its own native size so it is never upscaled: 343
   for the mobile file, 616 for the desktop one. The <picture> switch is at 768
   and matches this step exactly. No margin-inline:auto — justify-items on .row
   does the centring now. */
.row__figure img { width: 100%; max-width: 343px; height: auto; }
@media (min-width: 768px) { .row__figure img { max-width: 616px; } }

@media (min-width: 1200px) {
  /* justify-items must go back to stretch here. Left at center, the two tracks
     collapse to their content width and the layout falls apart. */
  .row { grid-template-columns: 1fr 616px; justify-items: stretch; align-items: center; }
  .row--visual-left { grid-template-columns: 616px 1fr; }

  /* .row .row__copy, not .row__copy — it has to outrank the two-child reset
     above. max-width goes back to none so the copy fills its 1fr track.

     grid-row: 1 is REQUIRED, not tidiness. The stacking reset sets
     grid-row: auto, and with auto placement the visual-left rows break: DOM
     order is copy then figure, so copy takes row 1 column 2, and the cursor
     cannot go backwards to column 1 — the figure is pushed to a second grid
     row. That is the 521px-tall row. Pinning both to row 1 forces them side by
     side whichever column each occupies. */
  .row .row__copy   { grid-column: 1; grid-row: 1; max-width: none; }
  .row .row__figure { grid-column: 2; grid-row: 1; }
  .row--visual-left .row__figure { grid-column: 1; }
  .row--visual-left .row__copy   { grid-column: 2; }
}

/* ==========================================================================
   More Capabilities — static list, not an accordion (§11)
   ========================================================================== */

.more { background: var(--surface-default); }
.more__title {
  font-family: var(--font-display);
  font-weight: var(--weight-display);
  font-size: var(--fs-section-title);
  line-height: var(--lh-section-title);
  color: var(--text-primary);
  text-align: center;
}
/* DIVIDER RULES, from the Homepage Benefits item pattern (185:187 desktop,
   185:238 mobile) which is the source for these. Confirmed identical on Ci
   (249:366 / 249:473) and GP (196:176):

     stroke    border/default, strokeAlign INSIDE
     weights   top 1, right 0, bottom 0, left 0
     padding   24 / 0 / 24 / 0 on every item
     list gap  0 — the rules and the padding do the spacing, not a gap
     heading   Section Heading Wrap has padding-bottom 48

   The FIRST item carries the top rule, so the list opens with a line. That is
   deliberate in Figma, not an off-by-one.

   Ci and GP do NOT put a bottom rule on the last item; the list stays open at the
   foot. Homepage Benefits DOES (185:215 and 185:266 carry bottom 1), so when the
   Homepage is built its last item needs `border-bottom` and this rule does not
   cover it. */
.more__list {
  display: grid;
  gap: 0;
  max-width: var(--content-wide);
  margin-inline: auto;
}
.more__item {
  display: grid;
  grid-template-columns: 24px minmax(0, 1fr);
  gap: var(--more-icon-gap);
  padding-block: var(--more-item-pad);
  border-top: 1px solid var(--border-default);
}

/* DELIBERATE DIVERGENCE. Figma puts the rule on the FIRST item too, on all three
   sources — Homepage Benefits 185:187/185:238, Ci 249:366/249:473, GP 196:176 — so
   the list opens with a line. Zoran's call is that this reads as an unclosed box.
   The rules are separators BETWEEN items, so the first item has nothing above it to
   be separated from. Register A15. Do not "correct" this back to match Figma. */
.more__item:first-child { border-top: 0; }

/* 48px between the heading and the first rule, per Figma's Section Heading Wrap.
   Two selectors because the two pages disagree on markup: Ci uses .more__title,
   GP uses the shared .section-head. Covering both here lands them on the same 48
   without editing either page. Both were at 96 before, not 192: .section-head's
   margin-bottom and .more__list's margin-top were adjacent siblings, so they
   collapsed to the larger rather than summing. */
.more__title { margin-bottom: 48px; }
.more .section-head { margin-bottom: 48px; }
.more__icon { display: block; }
.more__heading {
  font-family: var(--font-display);
  font-weight: var(--weight-display);
  font-size: var(--fs-row-title);
  line-height: var(--lh-row-title);
  color: var(--text-primary);
}
.more__body { color: var(--text-secondary); margin-top: var(--gap-after-title); }

/* ==========================================================================
   How It Fits
   ========================================================================== */

.fits { background: var(--surface-sunken); }
.fits__inner { max-width: var(--content-narrow); text-align: center; }
.fits__title {
  font-family: var(--font-display);
  font-weight: var(--weight-display);
  font-size: var(--fs-headline);
  line-height: var(--lh-headline);
  color: var(--text-primary);
  margin-top: var(--gap-after-eyebrow);
}
.fits__body { color: var(--text-secondary); margin-top: var(--gap-after-headline); }

/* ==========================================================================
   CTA Band
   ========================================================================== */

.cta { background: var(--brand-accent); color: var(--text-on-accent); text-align: center; }
.cta__title {
  font-family: var(--font-display);
  font-weight: var(--weight-display);
  font-size: var(--fs-section-title);
  line-height: var(--lh-section-title);
}
.cta__sub {
  font-size: var(--fs-hero-sub);
  line-height: var(--lh-hero-sub);
  max-width: 600px;
  margin: var(--gap-after-headline) auto 0;
}
.cta__row { margin-top: var(--gap-after-subhead); }

/* ==========================================================================
   Footer
   ========================================================================== */

/* Two themes, matching the Figma component sets Footer Desktop and Footer
   Mobile, whose only variant property is Theme = Navy | Light.

   Read from instances 249:410 (Navy) and 225:196 (Light):
     Navy   surface/brand   links text/on-dark      eyebrow+legal neutral/400
     Light  surface/sunken  links brand-navy/100    eyebrow+legal brand-navy/200

   DELIBERATE DIVERGENCE — no border-top on Light. Figma's Light variant carries
   an INSIDE stroke border/default, top weight 1, 0 on the other three sides.
   We do not reproduce it. The surface-default to surface-sunken step is a
   near-flat edge by intent, and it carries the separation on its own. Do not
   "restore" this line on the grounds that Figma has it — the omission is the
   decision. CLAUDE.md §8 records it. Navy has no stroke in Figma either. */
/* GEOMETRY, read from 249:410 and 225:196 — both 1440x389, padding 48 top,
   64 sides, 48 bottom, vertical itemSpacing 32:

     Top Row    y=48   1312x208   space-between, top-aligned
       Address       x=0    w=475    gap 16   logo, then the Offices group
         Offices                     gap 12   label, then the entries group
           Office Entries            gap 8    the two office lines
       Link Columns  x=909  w=403    gap 48, columns hug their content
     Divider    y=288  1312x1        a filled rect, not a border
     Bottom Row y=321  1312x20       space-between, centred
       Legal         x=0    w=584    gap 16, the two strings adjacent
       Social        x=1292 w=20     hard right

   DELIBERATE DIVERGENCE — 80px insets and a 1280 content box, not Figma's 64
   and 1312. The footer uses the SAME mechanism as every page section: 48px of
   padding with .footer__inner capped at --inner-max and centred inside it. At
   1440 that lands the content at 80 and ends it at 1360.

   The reason is the join. The CTA band sits directly above the footer and its
   content is inset 80px, so a footer at 64 would put a visible 32px step right
   where the two meet. Matching the sections removes it. And because the footer
   borrows the section mechanism rather than restating a number, the two cannot
   drift apart later.

   Not a contradiction of the nav, which keeps 48px insets and no cap: the nav
   has nothing to align WITH. On these pages its logo and the footer logo are
   roughly 5,000px apart, so the reader never sees the two edges together, and
   the nav is chrome that spans the viewport by design. Three inset values, each
   earning its own reason — nav 48, sections 80, footer 80. CLAUDE.md §8.

   DELIBERATE DIVERGENCE — no border-top on Light. Figma's Light variant carries
   an INSIDE stroke border/default, top weight 1, 0 on the other three sides.
   We do not reproduce it. The surface-default to surface-sunken step is a
   near-flat edge by intent, and it carries the separation on its own. Do not
   "restore" this line on the grounds that Figma has it — the omission is the
   decision. CLAUDE.md §8 records it. Navy has no stroke in Figma either. */
.footer { padding-block: var(--section-pad-block); }

.footer--navy  { background: var(--surface-brand);  color: var(--text-on-dark); }
.footer--light { background: var(--surface-sunken); color: var(--brand-navy-100); }
/* max-width + auto margins are what produce the 80px inset, exactly as on
   .hero__inner and the rest. Do not swap this for padding-inline: 80px — the two
   agree at 1440 and diverge above it, where padding would let the footer keep
   widening while the sections above it stayed capped at 1280. */
.footer__inner { max-width: var(--inner-max); margin-inline: auto; display: grid; gap: 32px; }
/* Unlike the section eyebrows, this one DOES change visually: the copy is
   title case — Offices, Solutions, Company, Resources — and Figma's textCase
   UPPER is what makes it render SOLUTIONS. The <h2> text stays title case in
   the DOM, which is what the aria-labelledby references resolve to. */
.footer__eyebrow {
  font-size: var(--fs-eyebrow);
  line-height: var(--lh-eyebrow);
  font-weight: var(--weight-body-strong);
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
}

/* THE MUTED SET — column eyebrows, the office lines, and the legal lines. This
   is the type that does not inherit the footer's colour, and it takes a
   different variable in each theme. neutral/400 is 6.14:1 on navy; on
   surface-sunken it would be 2.06:1 and fail, so Light uses brand-navy/200 at
   7.72:1. In the footer, the inherited white is for LINKS; supporting text is
   muted.

   The office lines are in this set by decision, not by reading. Their two text
   nodes — I249:410;124:452 and ;124:453 — carry an empty fills array, so Figma
   specifies no colour for them at all and an earlier pass defaulted them to the
   inherited text/on-dark. They are supporting text, not links, so they belong
   with the eyebrows. CLAUDE.md §8 records it as a decision resolving a Figma
   defect rather than a divergence: there is nothing there to diverge from. */
.footer--navy  .footer__eyebrow,
.footer--navy  .footer__office,
.footer--navy  .footer__legal  { color: var(--neutral-400); }
.footer--light .footer__eyebrow,
.footer--light .footer__office,
.footer--light .footer__legal  { color: var(--brand-navy-200); }
/* Mobile keeps everything stacked. Footer Mobile (239:258) has NOT been read,
   so none of its geometry is asserted here — only the desktop block below
   carries Figma numbers. */
/* DELIBERATE DIVERGENCE — 80px more space below the top row than Figma.
   Figma's footer is a vertical stack with itemSpacing 32, so it puts 32px
   between the link block and the divider. The site puts 112.

   It is a margin on .footer__top, which adds to the 32px grid gap, rather than
   padding on the row or a spacer inside a column. That placement is the point:
   Solutions is the tallest column only because it has five links against
   Company's four, so anything anchored inside Solutions stops being at the
   bottom the moment Company regains an item. A margin on the row cannot drift.

   Theme-agnostic on purpose — .footer__top is not theme-scoped, so Navy and
   Light both get it from this one rule. CLAUDE.md §8 records the reason. */
.footer__top     { display: grid; gap: 48px; margin-bottom: 0; }

/* A5: the 80px above the divider is DESKTOP ONLY. It was never media-scoped, so
   mobile inherited it by omission and carried 112px inside a footer already 1223
   tall. 1024 rather than 900: the footer goes two-column at 900, but the extra air
   is a wide-viewport gesture, not part of the two-column switch. Register A5. */
@media (min-width: 1024px) { .footer__top { margin-bottom: 80px; } }
.footer__address { display: grid; gap: 16px; justify-items: start; }
.footer__offices { display: grid; gap: 12px; }
.footer__office-entries { display: grid; gap: 8px; }

/* Figma shows the footer lockup at 183.79 x 28 desktop, 170.66 x 26 mobile —
   both uniform scales of the same 203 x 31 asset, so width alone drives it. */
.footer__logo { display: block; width: 171px; }
.footer__logo img { width: 100%; height: auto; }
.footer__office { font-size: var(--fs-body-sm); line-height: var(--lh-body-sm); }
.footer__columns { display: grid; gap: 32px; }

/* body-sm is declared on the LIST, not just on the link, and that placement is
   the actual fix for the 4px-per-item overshoot.

   .footer__link already declared 14/20. But the <a> is inline-flex, so each
   <li> still generates a line box, and that box is at least as tall as the li's
   own strut — which inherited body 16/24. So the li measured 24 while the link
   inside it measured 20. Setting the type on the ul brings the strut down to 20
   and the link inherits the same values, so there is nothing left to disagree.
   Five list items at 4px over is the 20px the Solutions column was too tall. */
.footer__list {
  display: grid;
  gap: 12px;
  margin-top: 12px;
  font-size: var(--fs-body-sm);
  line-height: var(--lh-body-sm);
}
.footer__link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

/* An <hr>, because it is a thematic break between the link navigation and the
   legal line. Figma draws it as a filled 1312x1 rectangle rather than a border,
   so it is a background here and the default hr border is cleared. */
.footer__divider { height: 1px; border: 0; margin: 0; }
.footer--navy  .footer__divider { background: var(--neutral-700); }
.footer--light .footer__divider { background: var(--border-default); }

.footer__bottom { display: grid; gap: 8px; }

/* The legals pair STACKS below 768 because it does not fit, measured rather than
   assumed: the copyright renders 272.03px — Figma says 272 — and "Privacy policy"
   92.28px, so side by side with Figma's 16px gap needs 380.3px against the 343 a
   375 viewport gives. It is 37.3px short. From 768 the content column is 720 and
   the pair fits with room to spare.
   768 rather than a new number: it is already the inset step, so nothing is
   earned by inventing one (§9). Between 768 and 899 the pair is a row while
   .footer__bottom is still stacked, which is correct — they answer different
   questions. Stacked gap matches the row's own 8px; inline gap is Figma's 16. */
.footer__legals { display: flex; flex-direction: column; gap: 8px; }
@media (min-width: 768px) {
  .footer__legals { flex-direction: row; align-items: center; gap: 16px; }
}
/* Element-qualified so the underline reaches the LINK and not the copyright
   paragraph beside it, which shares the class. Consistent with B5 — Figma models
   no footer hover at all, and underline is the one signal that works on navy and
   light and cannot fail a contrast check, since it inherits the link's colour. */
a.footer__legal:hover, a.footer__legal:focus-visible {
  text-decoration: underline; text-underline-offset: 3px;
}
.footer__legal { font-size: var(--fs-body-sm); line-height: var(--lh-body-sm); }
.footer__social { display: inline-flex; align-items: center; justify-content: center; width: 44px; height: 44px; }

@media (min-width: 900px) {
  /* Block only. The inline padding stays on --section-pad-inline so it tracks
     the sections; the 80px inset comes from the cap on .footer__inner. */
  .footer { padding-block: 48px; }
  .footer__logo { width: 184px; }

  /* Two columns, not a stack: Address at the left, Link Columns hard right,
     top-aligned because Address (120) is shorter than the tallest link
     column (176) and Figma's counterAxisAlignItems is MIN. */
  .footer__top { display: flex; justify-content: space-between; align-items: flex-start; gap: 48px; }
  .footer__address { max-width: 475px; }

  /* Content-width columns packed left, 48px apart — not three equal 1fr
     tracks. Figma's are 115 / 111 / 81, which is what their text measures. */
  .footer__columns { display: flex; gap: 48px; }

  /* Figma's links are 20px tall on a 32px pitch. The 44px minimum is a touch
     target and belongs on mobile; at 32px pitch these still satisfy WCAG 2.2
     SC 2.5.8, which lets an under-24px target pass on spacing alone. */
  .footer__link { min-height: 0; }

  .footer__bottom { display: flex; justify-content: space-between; align-items: center; gap: 24px; }

  /* Figma's Social frame is 20x20, which is what sets Bottom Row to 20 tall.
     Same reasoning as .footer__link: the 44px box is a touch target for mobile.
     It is the only target in the row — the legal strings are plain text, not
     links — so nothing sits inside its 24px circle and SC 2.5.8 is met on the
     spacing exception rather than on size. */
  .footer__social { width: 20px; height: 20px; }
}

/* ==========================================================================
   Reduced motion — the NAV-SPECIFIC part (§8)

   The global floor is in reset.css. What is left here cannot go there: these
   rules name .nav and .nav__logo-img, and they do more than stop motion. The
   nav carries its theme swap in a transition, so with motion suppressed it has
   to be told what the inverted state looks like statically, and which of the
   two stacked lockups is the visible one.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  .nav, .nav__logo-img { transition: none !important; }
  .nav.is-hidden { transform: none !important; }
  .nav.is-inverted { background: inherit; }
  .nav--navy.is-inverted  { background: var(--surface-brand);   color: var(--text-on-dark); }
  .nav--light.is-inverted { background: var(--surface-default); color: var(--text-primary); }
  .nav--navy.is-inverted  .nav__logo-img--white { opacity: 1; }
  .nav--navy.is-inverted  .nav__logo-img--navy  { opacity: 0; }
  .nav--light.is-inverted .nav__logo-img--white { opacity: 0; }
  .nav--light.is-inverted .nav__logo-img--navy  { opacity: 1; }
}

/* ==========================================================================
   Scroll reveal (§11)

   One gesture, section level, first time only.

   PROGRESSIVE ENHANCEMENT IS THE HARD REQUIREMENT. The hidden state lives only
   under .js-reveal, a class an inline script in <head> adds. With JS off the
   class never appears and every section renders visible. Do NOT lift opacity:0
   out of this guard — that is the one change that would make the page depend on
   JavaScript to be readable.

   The gate is inline and synchronous rather than in the deferred bundle. A
   deferred script would let sections paint visible and then hide them, which is
   a worse artefact than no animation at all.

   The hero is excluded. It is above the fold and holds the first thing anyone
   reads, so it must not wait on an observer.
   ========================================================================== */

.js-reveal main > section:not(.hero) {
  opacity: 0;
  transform: translateY(12px);
  transition: opacity 350ms ease-out, transform 350ms ease-out;
}
.js-reveal main > section.is-revealed { opacity: 1; transform: none; }

/* Belt and braces. The inline gate already withholds .js-reveal under reduced
   motion, but the class survives a preference changed after load — this makes
   the sections visible in that case rather than stuck at opacity 0. */
@media (prefers-reduced-motion: reduce) {
  .js-reveal main > section:not(.hero) {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* #main carries tabindex="-1" so the skip link moves FOCUS and not merely the
   scroll position — without it activeElement stays on <body> and the next Tab
   goes back into the nav, which defeats the skip entirely.

   The ring is suppressed here and only here. #main is a landing target, not a
   control, and the two-tone ring drawn around a full-page element reads as
   breakage rather than as feedback. What tells a keyboard user they arrived is
   the scroll jump plus the next Tab landing inside the content. Every actual
   control keeps its ring. */
main:focus { outline: none; }

/* Anchor landing offset. #main sits directly under the sticky nav, so a jump to
   it puts main's top at viewport 0 with the nav covering the first 72px — the
   skip link would deliver a keyboard user to content hidden behind the header.
   scroll-margin-top pushes the landing down by the nav's own height, which also
   makes the skip link resolve at scrollY 0 rather than 72. The hero CTA points
   at #main too, so it benefits identically. Values match .nav__inner's
   min-height at each breakpoint. */
main { scroll-margin-top: 64px; }
@media (min-width: 900px) { main { scroll-margin-top: 72px; } }


/* ==== Homepage-only bands ================================================
   Trust Layer and Product Showcase exist on the Homepage and nowhere else.
   Figma specifies 375 and 1440 only (§9), so every width between is a
   DECISION recorded here, not a transcription.

   The Homepage hero diagram is 500x440 — a third hero size, alongside Ci and
   Guardian's 496 and GP and LP's 539. It gets its own variant rather than
   reusing --hero-fig: 496px, because a 496 cap would render a 500 asset at
   99.2% of native and §9 forbids scaling a diagram below native at all. The
   4px would not be noticed; the rule is not negotiable per-case or it stops
   being a rule. It reuses the 1344 two-column threshold rather than adding a fifth
   breakpoint, per the rule below.

   ~~"616 copy + 48 gap + 500 figure = 1164"~~ — that arithmetic was WRONG and the
   rule it described was NEVER WRITTEN. Figma's copy column is 682, not 616, and
   until 2026-08-07 no media block matched .hero--fig-500 at all, so the hero
   stacked at every width. See the block below and register A48. */
.hero--fig-500 { --hero-fig: 500px; }

/* THE TWO-COLUMN RULE. Added 2026-08-07, register A48, closing O7.

   The comment above asserted this behaviour from the day the Homepage was built
   and NOTHING IMPLEMENTED IT: the 1344 block near .hero--fig-496 is scoped to
   that class and the 1376 block to .hero--fig-539, so .hero--fig-500 set a
   variable and received no layout at all. Measured at 1440 before the fix:
   .hero__inner computed grid-template-columns: 1280px — one track — the figure
   sat below the copy, and the section was 924 tall against Figma's 632.

   TRACK WIDTHS ARE FIGMA'S OWN, read from Inner 1280 (42:198): Left Column 682
   at x 48, diagram 500 at x 778, so the gap is 48. NOT 616, which the old
   comment guessed, and NOT 700-to-fill-the-1248-band: the headline wraps to
   exactly four 56px lines inside 682, so widening the track rewraps the largest
   type on the site.

   align-items: center because Figma centres the copy against the diagram — the
   682x416 column and the 500x440 figure both centre on y 316 inside Inner 1280.
   justify-items: stretch to undo the base rule's centring, as the -496 block does.

   The subhead takes Figma's own 560 (Subhead Wrap 42:203) rather than the full
   682. Without it the measure would grow when .hero__copy's 616 cap is released.

   1344 needs no new breakpoint and the arithmetic holds at the tightest width:
   682 + 48 + 500 = 1230, inside the 1248 content band 1344 gives. */
@media (min-width: 1344px) {
  .hero--fig-500 .hero__inner {
    grid-template-columns: minmax(0, 682px) minmax(0, 500px);
    align-items: center;
    justify-items: stretch;
  }
  .hero--fig-500 .hero__copy, .hero--fig-500 .hero__figure { max-width: none; }
  .hero--fig-500 .hero__sub { max-width: 560px; }
}

/* Homepage has no mobile hero diagram at all — the mobile artwork is the
   343x560 vertical in its own How It Works band — so the figure is simply
   hidden below 768 by .hero--mobile-split and needs no mobile sizing. */

/* ---- Trust Layer ---- */
/* WHITE, not sunken. Register D8, 2026-08-07 — the Trust Layer and the Product
   Showcase had each other's fills. Figma's Trust Layer takes the default surface
   on BOTH artboards (132:176, 133:206); the build had --surface-sunken. Stated
   explicitly rather than left to inherit from body, matching how .more already
   declares its white. */
.trust { background: var(--surface-default); }

/* 48px between the heading and the chips, per Figma's Headline Wrap, which
   carries paddingBottom 48 on BOTH breakpoints (132:176 / 133:206). The build
   had only .trust__list's 16px margin-top, so the clearance was 16 — and the
   .more section already lands its equivalent on 48 (see .more .section-head).
   Same margin-collapse mechanism as there: 48 here against the list's 16 are
   adjacent siblings, so they collapse to the larger rather than summing.
   Triage 2026-08-07, register D6. */
.trust__head { text-align: center; margin-bottom: 48px; }
.trust__title {
  font-family: var(--font-display);
  font-weight: var(--weight-display);
  font-size: var(--fs-section-title);
  line-height: var(--lh-section-title);
  color: var(--text-primary);
  margin: var(--gap-after-eyebrow) 0 0;
}
/* One column, then two, then four. Four across only above 1200: each item is a
   short label over a full sentence, and below that the sentence wraps to three
   or more lines and the row stops reading as four peers. 2-up at 768 rather
   than 4-up because 4 x ~170px puts the labels at two words per line. */
.trust__list {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--gap-after-subhead);
  margin: var(--gap-after-headline) 0 0;
  padding: 0;
  list-style: none;
}
@media (min-width: 768px)  { .trust__list { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1200px) { .trust__list { grid-template-columns: repeat(4, 1fr); } }
.trust__label {
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  font-weight: 600;
  color: var(--text-primary);
  margin: 0 0 4px;
}
.trust__note { font-size: var(--fs-body-sm);
  line-height: var(--lh-body-sm); color: var(--text-secondary); margin: 0; }

/* ---- Product Showcase ---- */

/* TINTED, and DESKTOP-ONLY — which is the half of D8 that is not a swap.

   Figma's Product Showcase takes the SUNKEN tint on the desktop artboard
   (152:238) and the DEFAULT surface on the mobile one (152:302). That looks
   inconsistent until the two
   artboards are read as a whole: the page carries EXACTLY ONE tinted band
   between the navy hero and the teal CTA, and which section holds it depends on
   which sections exist.

     mobile   navy hero | white trust | TINTED How It Works | white showcase |
              white benefits | teal CTA | navy footer
     desktop  navy hero | white trust | TINTED showcase | white benefits |
              teal CTA | navy footer

   How It Works is mobile-only (.herodiagram, display:none at 768), so above 768
   the tint would simply vanish if the showcase did not take it. 768 is therefore
   not a chosen number — it is the width at which the tinted band hands over, and
   using any other value would leave a band with two tints or none.

   The white Product Cards (D7) read against this tint the way Figma intends;
   on a white section their 1px border was doing all the work. Contrast checked
   before the move: only the section header and intro sit on the tint — the card
   contents keep their own white fill — and --text-secondary is 5.93:1 on sunken,
   which tokens.css already records.

   The mobile white is DECLARED, not left transparent. Both render identically,
   but a transparent section only looks right for as long as whatever is behind it
   stays white, and Figma states the fill explicitly on the frame. Declaring it
   also makes the computed value comparable to Figma's — a background audit that
   reads a fully transparent computed colour cannot tell "white by intent" from
   "white by accident". Same reason .more declares its own. */
.showcase { background: var(--surface-default); }
@media (min-width: 768px) { .showcase { background: var(--surface-sunken); } }
.showcase__head { text-align: center; }
.showcase__title {
  font-family: var(--font-display);
  font-weight: var(--weight-display);
  font-size: var(--fs-section-title);
  line-height: var(--lh-section-title);
  color: var(--text-primary);
  margin: var(--gap-after-eyebrow) 0 0;
}
/* 18/28, not 16/24. Register D9, 2026-08-07. Figma sets BOTH intro lines that
   use this class at Inter Regular 18/28 — the Product Showcase's (152:240, 640
   wide) and the Benefits opening intro (185:185, 560 wide) — and the build had
   body copy. Changed on the shared class rather than scoped, because Figma
   agrees on both uses.

   --fs-hero-sub IS the site's single 18/28 step, so this reuses it rather than
   declaring the numbers twice. The token's name is hero-specific and now serves
   a second role; renaming it is a token decision on its own, the same note D5
   left on --fs-product-name. */
.showcase__intro {
  font-size: var(--fs-hero-sub);
  line-height: var(--lh-hero-sub);
  color: var(--text-secondary);
  max-width: 616px;            /* same 616 measure the rows use, per §9 */
  margin: var(--gap-after-headline) auto 0;
  text-align: center;
}
/* Two columns only at 1024. These are four long paragraphs; at 768 a two-column
   card is ~340px and the body runs to a dozen short lines, which reads worse
   than one full-measure column. Capped at 616 per card for the same reason the
   rows are — line length, not available space (§9). */
.showcase__list {
  display: grid;
  grid-template-columns: 1fr;
  /* Figma's Product Grid itemSpacing, and it DIFFERS BY BREAKPOINT: 16 on mobile
     (152:302) and 24 on desktop (152:238). D7 read the desktop value and applied
     it unconditionally, which left mobile 8px loose on each of three gaps.
     Caught by D9: correcting the intro's type exposed a +32 that the too-short
     intro had been masking. Mobile-first per §9 — base 16, step up at 768.
     Was calc(--gap-after-subhead * 2) = 64 before D7, which was reasonable
     between UNSTYLED text blocks and far too much between padded, bordered
     cards; the cards now carry their own 32px inset. */
  gap: 16px;
  /* 48 between the heading block and the grid, per Figma's Section Header, which
     carries paddingBottom 48 at both breakpoints. Was --gap-after-headline (16).
     THIRD instance of one systemic cause, not three bugs: every homepage section
     gives its heading 48px of clearance in Figma, and the build used 16 everywhere
     except .more, where 48 had already been applied by hand. Register D6. */
  margin: 48px 0 0;
  padding: 0;
  list-style: none;
}
@media (min-width: 768px)  { .showcase__list { gap: 24px; } }
@media (min-width: 1024px) { .showcase__list { grid-template-columns: repeat(2, 1fr); } }
/* THE PRODUCT CARDS ARE CARDS. Triage 2026-08-07, register D7.

   Figma's Product Card (152:276) carries a WHITE fill, a 1px stroke, cornerRadius
   16 and paddingBottom/Top/Left/Right 32. The build had `max-width: 616px` and
   NOTHING ELSE — four unstyled text blocks where the design specifies four cards
   on the page surface. That is not a spacing delta; it is a missing visual
   treatment, and it was the single largest contributor to the section's -156.

   Every value is an existing token: --surface-default IS Figma's white,
   --border-default IS the 1px stroke colour, --radius-panel IS radius/lg 16.
   The 32 is spacing/32, written as a literal for the same reason
   .more__title's 48 is — there is no semantic token for a card inset.

   WIDTH 628, not 616: Figma's grid is two columns in 1280 at gap 24, so
   (1280 - 24) / 2 = 628. The old 616 cap predates the gap correction below and
   would now clip the track by 12px at 1440.

   SAFE TO CHANGE because .showcase is homepage-only — 41 uses in src/index.html
   and zero on the other four pages. */
.showcase__item {
  max-width: 628px;
  background: var(--surface-default);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-panel);
  padding: 32px;
}
.showcase__name {
  font-family: var(--font-display);
  font-weight: var(--weight-display);
  font-size: var(--fs-row-title);
  line-height: var(--lh-row-title);
  color: var(--text-primary);
  margin: 0;
}
.showcase__for {
  font-size: var(--fs-body-sm);
  line-height: var(--lh-body-sm);
  color: var(--text-accent);
  margin: 4px 0 0;
}
.showcase__body { font-size: var(--fs-body);
  line-height: var(--lh-body); color: var(--text-secondary); margin: 12px 0 0; }
.showcase__link {
  display: inline-block;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  font-weight: 600;
  color: var(--text-accent);
  margin: 12px 0 0;
}
/* Underline on hover and focus-visible, never a colour shift — the one signal
   that cannot fail a contrast check because it inherits the link's colour (B5). */
.showcase__link:hover, .showcase__link:focus-visible { text-decoration: underline; }

/* ---- Benefits (the homepage's .more) ---- */

/* THE 720 CENTRED COLUMN. Added 2026-08-07, register A53, closing O9.

   Figma puts the ENTIRE Benefits content in a 720 column — Content 720
   (185:178) at x 280 inside Inner 1280 — and nests it: Section Header 720,
   Headline Wrap 640, Intro Wrap 560, Benefits List 720, Closing Wrap 560.
   The build constrained ONLY the list, so the h2 spanned the full 1280 and
   rendered on ONE line where Figma has two.

   HOMEPAGE-SCOPED, per the D5/D7 pattern, and this is the whole reason O9 was
   raised rather than fixed in the triage pass: .section-head is on all four
   pages and Ci's own Figma More heading genuinely IS 1280 wide. The pages
   differ, so this must never touch .more or .section-head themselves.

   THE 48 ALSO MOVES. Figma's clearance sits between the INTRO and the list —
   Section Header carries paddingBottom 48 and Headline Wrap only 16 — where the
   shared `.more .section-head` rule puts 48 between the heading and whatever
   follows. That is right on the product pages, which have no intro, and one
   element too early here.

   NO MEDIA QUERY, deliberately. Figma's mobile Benefits (185:227) is full-width
   343 with no inner column, and every cap here is larger than the mobile inner,
   so none of them binds below 768 — the widths scope themselves. The two margin
   rules DO apply at every width and were measured at 375 before being left
   unscoped. */
/* .more--principles is About's How We Work. It is added to these selectors
   rather than given its own block because the geometry is IDENTICAL — Figma
   built that section by cloning this one, so Content 720, headline 640, intro
   560 and the 48 after the intro are the same numbers. Two copies of one set of
   numbers is how they drift apart; D13-D16 are four instances of that. */
.more--benefits .section-head,
.more--principles .section-head        { max-width: 720px; margin-inline: auto; }
.more--benefits .section-head__title,
.more--principles .section-head__title { max-width: 640px; margin-inline: auto; }
.more--benefits .showcase__intro,
.more--principles .showcase__intro     { max-width: 560px; }
.more--benefits .section-head,
.more--principles .section-head        { margin-bottom: var(--gap-after-headline); }
.more--benefits .more__list,
.more--principles .more__list          { margin-top: 48px; }
/* The one real difference: Benefits is white, this band is tinted (Figma
   768:158 / 768:209, fill bound to surface/sunken). */
.more--principles { background: var(--surface-sunken); }
/* THE CLOSING LINE IS ITS OWN THING. Register D9, 2026-08-07, closing O11.

   It was `.showcase__intro` — borrowed-class instance SIX, and the least
   defensible of them: the line is neither a showcase nor an intro. It is the
   section's closing statement, and Figma treats it as a designed one
   (185:226, node name "Closing line (manifesto)"): Manrope MEDIUM 22/34 in
   --text-primary, three lines at 560, stepping to 20/30 on mobile (185:277).
   The build rendered it as Inter 16/24 body copy in --text-secondary — wrong
   family, wrong weight, wrong size, wrong colour.

   Both the 500 weight and the 22/34 step are new in tokens.css and declared
   only there, per §4. They did not exist because nothing else on the site uses
   them; this line is the only Manrope Medium and the only 22 on the ramp.

   It keeps Figma's 48 clearance after the list — the opening intro takes 16
   after the heading — which the removed adjacent-sibling rule used to carry. */
.more__closing {
  font-family: var(--font-display);
  font-weight: var(--weight-display-med);
  font-size: var(--fs-manifesto);
  line-height: var(--lh-manifesto);
  color: var(--text-primary);
  max-width: 560px;
  margin: 48px auto 0;
  text-align: center;
}


/* ==== 404 ================================================================
   The first LIGHT-theme page, so it is also the first exercise of the light
   nav, the light footer and the focus ring on white. Nothing here is
   theme-specific: the page inherits --surface-default and --text-primary,
   which is the point — a light page should need no light-specific rules. */
.notfound__inner { text-align: center; }
.notfound__code {
  font-family: var(--font-display);
  font-weight: var(--weight-display-max);
  font-size: var(--fs-numeral);
  line-height: var(--lh-numeral);
  color: var(--text-accent);
  margin: 0;
}

/* THE QUOTE BLOCK.

   min-height is RESERVED SPACE, not decoration. The five quotes run 51 to 116
   characters, so the longest takes one more line than the shortest at every
   width - and js/quote.js swaps the text after load, so without a floor the
   heading above would sit still while everything below it jumped. The values
   are measured, per breakpoint, from the LONGEST quote (van Gogh, 116).

   The measure cap is the same reasoning as §9's row copy: line length is the
   constraint, not the available width. */
.notfound__quote {
  max-width: 560px;
  margin-inline: auto;
  margin-block-start: 32px;
  /* MEASURED floor, from the longest of the five (van Gogh, 116 chars). Below
     768 it takes 3 lines against everything else's 2; at 768 and up it takes 2
     against the others' 1. Either way the swing is exactly ONE line of
     --lh-hero-sub, 28px, and without the floor the footer would jump by that
     much when js/quote.js runs. 112 = 84 of text + 28 of cite; 84 = 56 + 28. */
  min-height: 112px;
}
@media (min-width: 768px) {
  .notfound__quote { min-height: 84px; }
}
.notfound__quote-text {
  font-size: var(--fs-hero-sub);
  line-height: var(--lh-hero-sub);
  color: var(--text-primary);
  margin: 0;
}
.notfound__quote-cite {
  display: block;
  margin-block-start: 8px;
  font-size: var(--fs-body-sm);
  line-height: var(--lh-body-sm);
  color: var(--text-secondary);
}
.notfound__title {
  font-family: var(--font-display);
  font-weight: var(--weight-display);
  font-size: var(--fs-section-title);
  line-height: var(--lh-section-title);
  color: var(--text-primary);
  margin: var(--gap-after-title) 0 0;
}
/* Links inline with middle dots at >=768, stacked below it. Figma draws the
   separators as their own text nodes on desktop and omits them on mobile, so
   they are decorative punctuation between links, not content: aria-hidden. */
.notfound__links {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  margin: var(--gap-after-subhead) 0 0;
  padding: 0;
  list-style: none;
}
.notfound__links li { display: flex; align-items: center; gap: 12px; }
.notfound__sep { color: var(--brand-navy-200); }
@media (max-width: 767px) { .notfound__sep { display: none; } }
@media (min-width: 768px) {
  .notfound__links { flex-direction: row; justify-content: center; }
}
.notfound__link {
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  font-weight: var(--weight-body-strong);
  color: var(--text-accent);
}
.notfound__link:hover, .notfound__link:focus-visible {
  text-decoration: underline; text-underline-offset: 3px;
}

/* ==== THANK YOU =========================================================
   The 404's statement band in its happy form — same four parts in the same
   order, so this block deliberately mirrors the one above rather than
   generalising it. Two pages is not enough shared shape to justify a common
   abstraction, and the parts that differ are exactly the interesting ones.

   Light theme, like the 404, and for the same reason nothing here is
   theme-specific: the page inherits --surface-default and --text-primary.

   WHAT DIFFERS FROM .notfound, and why:
   - an .eyebrow replaces the 170px numeral, so the gap beneath it is
     --gap-after-eyebrow (12) rather than the numeral's --gap-after-title (8)
   - the body paragraph occupies the quote's slot at the quote's scale, so it
     needs no type rule of its own beyond the measure cap
   - NO min-height. The 404's floor is reserved space for a quote that gets
     swapped after load (§11); this copy is fixed at build time and never
     changes, so a floor here would reserve space against nothing. */
.thanks__inner { text-align: center; }
/* The gap belongs BETWEEN the eyebrow and the title, not on the title — the
   lesson D16 recorded when an unconditional margin-top pushed three More
   headers down through a collapsed edge. Here the eyebrow is always present,
   but expressing it as the adjacency keeps the rule true if it ever is not. */
.thanks__title {
  font-family: var(--font-display);
  font-weight: var(--weight-display);
  font-size: var(--fs-section-title);
  line-height: var(--lh-section-title);
  color: var(--text-primary);
  margin: 0;
}
.eyebrow + .thanks__title { margin-top: var(--gap-after-eyebrow); }
/* 560 is the 404 quote's measure, and the cap is the constraint rather than
   the available width — §9's rule for row copy. 32 above matches the gap the
   quote takes in the band this page was cloned from. */
.thanks__body {
  max-width: 560px;
  margin: 32px auto 0;
  font-size: var(--fs-hero-sub);
  line-height: var(--lh-hero-sub);
  color: var(--text-primary);
}
.thanks__links {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  margin: var(--gap-after-subhead) 0 0;
  padding: 0;
  list-style: none;
}
.thanks__links li { display: flex; align-items: center; gap: 12px; }
.thanks__sep { color: var(--brand-navy-200); }
@media (max-width: 767px) { .thanks__sep { display: none; } }
@media (min-width: 768px) {
  .thanks__links { flex-direction: row; justify-content: center; }
}
.thanks__link {
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  font-weight: var(--weight-body-strong);
  color: var(--text-accent);
}
.thanks__link:hover, .thanks__link:focus-visible {
  text-decoration: underline; text-underline-offset: 3px;
}

/* ==== FORM PAGES — Let's Talk and Book a Demo ============================
   Built 2026-08-07, register D10. Zoran's override to §12: build them with a
   STUBBED endpoint rather than waiting for the endpoint decision.

   NOT added to the shared `padding-block: var(--section-pad-block)` list,
   because Figma does not give these two sections symmetric padding. The Page
   Header is 96 top / 48 bottom and the Form band is 0 top / 96 bottom
   (207:210 and 210:207; 64/32 and 0/64 on mobile). One band, split across two
   frames, with the 48 acting as the gap between them — so a symmetric token
   would put 96 where Figma has 48 and 96 where Figma has 0. */
/* Both fills DECLARED rather than inherited from body, per D8's reasoning: Figma
   states the default surface on both frames, and a background audit that reads a
   fully transparent computed colour cannot tell white-by-intent from
   white-by-accident. */
.pagehead, .formband, .legalband { background: var(--surface-default); }
.pagehead { padding-block: 96px 48px; }
.formband, .legalband { padding-block: 0 96px; }
@media (max-width: 767px) {
  .pagehead { padding-block: 64px 32px; }
  .formband, .legalband { padding-block: 0 64px; }
}

/* Content 640 centred, with the intro at 560 inside it — Figma 207:212 sits the
   640 column at x 320 of Inner 1280 and insets the Intro Wrap by 40 each side. */
.pagehead__inner { text-align: center; }
.pagehead__title {
  font-family: var(--font-display);
  font-weight: var(--weight-display-max);
  font-size: var(--fs-page-title);
  line-height: var(--lh-page-title);
  color: var(--text-primary);
  max-width: 640px;
  margin: 0 auto;
}
.pagehead__intro {
  font-size: var(--fs-hero-sub);
  line-height: var(--lh-hero-sub);
  color: var(--text-secondary);
  max-width: 560px;
  margin: var(--gap-after-headline) auto 0;
}

/* THE INTRO RUNS WIDER THAN THE FORM — page-scoped, Zoran 2026-08-07.
   Let's Talk's new intro is one 152-character sentence pair that must set as
   exactly TWO lines at 1440. At the shared 560 — which is the FORM's width, not
   a measure chosen for prose — it sets four.

   SCOPED WITH A VARIANT rather than widened in place, because `.pagehead__intro`
   is shared with Book a Demo, whose own intro is a different string and is not
   being changed. Widening the base class would have silently rewrapped a page
   nobody asked about — the pattern this file has now recorded five times, most
   recently as `A53`/`O9`. The variant follows `.pagehead--doc`'s precedent.

   720 IS --content-wide, NOT A NEW NUMBER. It is the measure the More list and
   the About reading column already use, so the site keeps one wide-prose width
   instead of gaining a second. It clears the 560 form by 160 and leaves 280 of
   breathing room each side inside the 1280 inner at 1440. Zoran's brief allows
   the copy to extend past the fields, and at 720 it does so symmetrically.
   Measured, not predicted: 2 lines at 1440, and unchanged at 375 where the
   375-viewport width governs long before 720 does. */
.pagehead--wide .pagehead__intro { max-width: var(--content-wide); }

/* 560 for a single-column form, 720 where two fields sit side by side. The name
   says WHY the width differs rather than which page it is on: Book a Demo needs
   720 because three of its rows are paired (215:189 et al, two 348s and a 24
   gap), and Let's Talk needs 560 because none of its are. */
.form { max-width: 560px; margin-inline: auto; }
.form--paired { max-width: 720px; }

.form__fields { display: grid; gap: 24px; }
.form__row    { display: grid; gap: 24px; }
@media (min-width: 768px) { .form__row { grid-template-columns: 1fr 1fr; } }

/* THE MOBILE STACK TIGHTENS ON THE PAIRED FORM ONLY — 16, not 24. Figma's Book a
   Demo mobile Fields frame (inside 216:176) carries itemSpacing 16 where its
   desktop frame carries 24, and where Let's Talk carries 24 at BOTH breakpoints.
   The reason is legible in the design: stacked, Book a Demo is eight fields deep
   against Let's Talk's five, so the longer form gets a tighter rhythm.
   Applied via .form--paired rather than a page name — the same variant that
   marks the form as having side-by-side rows is the one that gets long when they
   stack, so the condition and the cause are the same thing.
   The rows take 16 too, which is what makes the flattened sequence uniform and
   matches Figma's mobile frame having no row wrappers at all. */
@media (max-width: 767px) {
  .form--paired .form__fields,
  .form--paired .form__row { gap: 16px; }
}

/* Field: label row, 8px gap, input box. Figma 210:219 — the component also
   carries a Helper text slot, hidden on every page instance except the one
   flagged below. */
.field { display: grid; gap: 8px; }
.field__label {
  font-size: var(--fs-body-sm);
  line-height: var(--lh-body-sm);
  font-weight: var(--weight-body-med);
  color: var(--text-primary);
}
/* The required marker is its OWN element, which is how Figma models it (a
   sibling TEXT node inside Label Row) and also what keeps §6's content-flag
   check off it: that check ignores any element whose entire content is an
   asterisk, so a required marker and a provisional-wording flag cannot be
   confused for one another. aria-hidden because `required` carries the meaning.

   IT TAKES THE LABEL'S COLOUR, NOT RED — §6's colour convention, Zoran
   2026-08-07. RED IS RESERVED EXCLUSIVELY FOR CONTENT AWAITING DON'S REVIEW.
   Until now both meanings painted --feedback-error-text, so a reader met one red
   asterisk meaning "you must fill this in" and another meaning "this wording is
   not approved" — on the same form, in the same glyph, in the same colour. The
   structural separation (§6) always kept the GATE from confusing them; it never
   kept a PERSON from doing so. Colour is what a person reads.
   Inheriting rather than re-declaring --text-primary: the marker follows the
   label wherever the label goes, so a future label colour cannot leave it
   stranded. */
.field__req { color: inherit; font-weight: var(--weight-body); }
/* Helper text is 12/16 Inter Medium (Figma 210:219's Helper text slot), which is
   the same pair --fs-eyebrow carries. Reused rather than re-declared — the site
   has one 12/16 step and it should stay single-sourced. The token's name is
   eyebrow-specific and now serves a second role, the same note D5 and D9 left on
   --fs-product-name and --fs-hero-sub. */
.field__help {
  font-size: var(--fs-eyebrow);
  line-height: var(--lh-eyebrow);
  font-weight: var(--weight-body-med);
  color: var(--text-secondary);
}
.field__input {
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--text-primary);
  /* background-COLOR, not the `background` shorthand: the shorthand resets
     background-image to none, and the Subject select paints its chevron there.
     A later shorthand here would silently erase it. */
  background-color: var(--surface-default);
  border: 1px solid var(--border-field);
  border-radius: var(--radius-card);
  width: 100%;
  height: 40px;
  padding: 0 12px;
}

/* ---- NO FOCUS RING ON FIELDS — Zoran's decision, 2026-08-07 ----------------
   THE 2px EXPANSION IS THE FIELDS' SOLE FOCUS INDICATOR. Scoped to .field__input
   and nothing else: `reset.css`'s site-wide `:focus-visible` keeps painting every
   link, button, nav item and the skip link, and MUST NOT be touched — it is the
   only focus indicator those have.

   Both properties are neutralised because the ring is drawn with both: a 2px
   white `outline` over a 4px navy `box-shadow` (reset.css:159). Killing only the
   outline would leave a navy halo, which is worse than either state.

   RAISED, DECIDED, RECORDED — WCAG 2.4.7 Focus Visible (AA). A 2px geometry
   change IS a visible focus indicator, so 2.4.7 is met on its terms. Two honest
   caveats, both measured rather than asserted:
     - it is SUBTLER than the ring it replaces, and it is the one cue, so a
       distracted or low-vision keyboard user has less to track. Zoran raised
       this himself and accepted it.
     - WCAG 2.2's 2.4.13 Focus Appearance (AAA) wants a 2px-thick perimeter at
       3:1 against adjacent colours. The expansion changes no colour, so it does
       not meet 2.4.13 — but that is AAA and this site targets AA.
   THE ALTERNATIVE WAS CHECKED, not ignored: Figma's own `State=Focus` draws a
   2px `brand/accent` border, which measures 1.96:1 on the sky fill and 2.37:1 on
   white — BOTH under SC 1.4.11's 3:1. So the design system's focus treatment
   would not have been an accessible substitute either; it is the `O1`/`O12`
   family again. Recorded so this is not relitigated as an oversight. */
.field__input:focus,
.field__input:focus-visible {
  outline: none;
  box-shadow: none;
}
.field__input::placeholder { color: var(--text-secondary); opacity: 1; }

/* ---- FIELD EXPANSION ON HOVER AND FOCUS — 2px each side, no layout shift ----
   Zoran 2026-08-07. The field grows 2px on every side; a field that already has
   a value rests at normal size even under the pointer.

   THE MECHANISM IS NEGATIVE MARGIN, and the alternative was rejected for a
   concrete reason rather than taste. The obvious choice is a box-shadow ring —
   it never participates in layout, so neighbours cannot move. But
   `reset.css:159` draws the SITE-WIDE FOCUS RING with `box-shadow: 0 0 0 4px`,
   and any box-shadow declared here would replace it outright, silently removing
   the focus indicator from every field on both forms. reset.css's own comment
   anticipates this collision and says to reach for a second outline instead.
   So: the box GROWS by 4px in each axis and the growth is absorbed by −2px
   margins, leaving the outer box — and therefore the grid track and every
   neighbour — exactly where it was. Verified by measurement, not by reasoning:
   the .field row heights and the form band are identical hovered and at rest.

   PADDING COMPENSATES so the TEXT does not move. Without it the left inset stays
   12px from an edge that has itself moved 2px left, and every label's text
   appears to shift. 14px from the new edge is 12px from the old one.

   WHICH FIELDS COUNT AS EMPTY, and this is where item 4 bit item 6:
   `:placeholder-shown` is the semantically correct test for an empty input, and
   it was the first choice — but it only matches when a placeholder ATTRIBUTE is
   present, and the same pass removed the placeholders from three of these
   fields. Removing them removed the hook. Rather than reinstate `placeholder=" "`
   purely as a CSS handle — markup invented for the stylesheet's benefit, which is
   the shape §5's KNOWN LIMIT warns off — the empty test is `:required:invalid`.
   Every field on both forms is `required` except Let's Talk's Subject, so an
   empty one is invalid and a filled one is not.
   The two SELECTs are matched on their own empty option instead, which covers
   Subject (optional, so never `:invalid`) and reads the same way for Products.
   `:has()` is inside §10's floor — last two stable versions of all six, and it
   has shipped in every one of them since 2023. Checked, not assumed.

   NO DISABLED STATE — `O2`, whose token pair measures 1.89:1 and fails AA. The
   form pages have never drawn one and this does not add one. */
.field__input:focus,
.field__input:required:invalid:hover,
select.field__input:has(option[value=""]:checked):hover {
  margin: -2px;
  width: calc(100% + 4px);
  height: 44px;
  padding: 0 14px;
}
textarea.field__input:focus,
textarea.field__input:required:invalid:hover {
  height: 100px;   /* 96 + 4 */
  padding: 14px;
}

/* ---- THE SELECT CHEVRON — replaces the native indicator, 2026-08-07 --------
   Zoran: the native arrowhead rides the right edge during the expansion. It is
   drawn by the OS, sits outside the box model, and nothing in CSS can hold it
   still — so it is replaced rather than adjusted.

   ~~D10~~ CLOSES HERE, AND `reset.css`'s STATED REASON IS NOW FALSE. That file
   says `appearance` is deliberately not reset because "the site has no chevron
   asset to replace it with — §5 forbids inventing artwork". True when written,
   and the asset now exists.

   WHERE THE ASSET CAME FROM, because §5's ban on inventing artwork still binds:
   it was EXPORTED FROM FIGMA, not drawn. The Select component's `icon-slot`
   holds a 10x5 Vector (`59:354`) — stroke `#54626F` = text/secondary, weight
   1.5, round cap and join — and `icon-chevron-down.svg` reproduces it inside a
   16x16 box at the slot's own offset (x 3, y 6). So this is transcription of
   design-system artwork, which §1 makes the normal direction of travel. It is
   NOT the "accordion chevron": this site has no accordion (§11/§12) and had no
   chevron asset of any kind — checked across all 27 icons, all 78 masters and
   the whole git history.

   `appearance: none` is scoped to `select.field__input`, NOT added to reset.css's
   global row. Every other select on the site — there are none today — keeps the
   native indicator until it has an asset too.

   GEOMETRY IS FIGMA'S, not invented: right inset 12 (the box's own padding),
   16x16 slot, and a 36px right padding = 12 + 16 + 8, where 8 is the Input Box's
   `itemSpacing`. Checked against the design: Figma's Select text is 512 wide in
   a 560 box, and 560 − 12 − 12 − 16 − 8 = 512 exactly.

   HOLDING IT STILL — the compensation path, not the fallback. `background-position`
   resolves against the PADDING BOX, whose right edge moves +2px when the field
   expands, so `right 12px` would carry the chevron 2px right with it. The
   expanded states use `right 14px`, which lands it on the same absolute pixel.
   VERTICAL NEEDS NO COMPENSATION and that is arithmetic rather than luck: the box
   grows 2px at the top and 2px at the bottom, so `center` does not move.
   Zoran's fallback — moving the chevron next to the "Select a Subject" text — was
   NOT needed and deliberately not taken: it would have moved the indicator away
   from where Figma puts it, and the compensation holds exactly (measured at both
   breakpoints, 0.0px drift). */
select.field__input {
  appearance: none;
  background-image: url("/img/icons/icon-chevron-down.svg");
  background-repeat: no-repeat;
  background-position: right 12px center;
  background-size: 16px 16px;
  padding-right: 36px;
}
/* After the shared expansion rule, and more specific than it, so the shorthand
   `padding: 0 14px` there cannot flatten the 36 back to 14 and run the label
   under the chevron. */
select.field__input:focus,
select.field__input:has(option[value=""]:checked):hover {
  background-position: right 14px center;
  padding-right: 38px;
}
/* Textarea: 96 tall with 12px padding all round, per Figma 203:835's Input Box.
   `resize: vertical` because the component draws a resize handle, and vertical
   is the only axis that cannot break the column. */
textarea.field__input { height: 96px; padding: 12px; resize: vertical; }

/* Figma puts 32 above the Submit button, then 24 above the consent line and 16
   above the response note (211:176, 211:181, 211:183). Both legal lines are
   LEFT-aligned while the page header is centred — deliberate in the design. */
.form__submit  { margin: 32px 0 0; }
.form__consent { font-size: var(--fs-body-sm); line-height: var(--lh-body-sm);
                 color: var(--text-secondary); margin: 24px 0 0; }
.form__note    { font-size: var(--fs-body-sm); line-height: var(--lh-body-sm);
                 color: var(--text-secondary); margin: 16px 0 0; }
@media (max-width: 767px) { .form__submit .btn { width: 100%; } }

/* ---- Endpoint wiring, 2026-08-09 — honeypot, Turnstile, no-JS -------------

   THE HONEYPOT IS OFF-CANVAS, NOT display:none, and the distinction is the
   whole mechanism. A bot that parses the DOM fills every input it finds; the
   cruder ones skip inputs that are display:none or hidden, which would defeat
   the trap. Moved out of view instead, so it is present, fillable and
   unreachable: aria-hidden and tabindex="-1" in the markup take it out of the
   accessibility tree and the tab order, so no assistive-tech user meets it
   either. Never give this a `display` value. */
.form__hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.form__turnstile { margin: 24px 0 0; }

/* NO-JS IS A REAL BREAK NOW, AND IT IS MADE LOUD RATHER THAN SILENT.

   Turnstile needs JavaScript. A submission with no token is rejected server
   side — accepting one would let any bot that does not run JS walk past the
   check — so with JS off this form genuinely cannot deliver. The notice and the
   submit button are therefore inverses of each other, both driven by the `.js`
   class the inline head gate already sets (§11): no new mechanism, and because
   that gate is synchronous and in <head>, no flash of the wrong state.

   Note the direction. Everywhere else on this site JS is an enhancement and its
   absence changes nothing; here the dependency is real, so the DEFAULT state is
   the no-JS one and `.js` reveals the working form. Written that way round
   deliberately: if the stylesheet loads and the script does not, a reader sees
   the explanation rather than a button that silently fails. */
.form__noscript {
  font-size: var(--fs-body-sm);
  line-height: var(--lh-body-sm);
  color: var(--feedback-error-text);
  font-weight: var(--weight-body-strong);
  margin: 24px 0 0;
}
.js .form__noscript { display: none; }
.form__submit { display: none; }
.js .form__submit { display: block; }

/* Field-level error text. Rendered ONLY by contact.php's error page — the built
   pages never emit it — but it belongs in the stylesheet rather than inline in
   the PHP so the error state is themed by the same tokens as everything else
   and cannot drift from them. --feedback-error-text is 6.75:1 on white; D11
   records why the content flag borrows the same token. */
.field__error {
  font-size: var(--fs-body-sm);
  line-height: var(--lh-body-sm);
  color: var(--feedback-error-text);
  margin: 8px 0 0;
}

/* ==== THE CONTENT FLAG =====================================================
   Zoran's rule update 2026-08-07, register D11: a content flag must be
   VISUALLY LOUD, not ignorable. It is now wrapped so it can be coloured —
   <span class="content-flag">*</span> — and §6 records why the wrapper
   collided with the required-field convention and how the class resolves it.

   FOURTH SURFACE SPLIT, and §4's own lesson applies unchanged: no single red
   clears AA-text on both light and navy, so one value would have been right on
   the pages it was checked against and wrong on the rest. Measured:

                        white   sunken   navy
     error-strong        6.75     6.39   2.23   <- light surfaces
     error/base          4.38     4.15   3.44
     error-text-dark     3.61     3.42   4.17
     warning/base        ----     ----   5.06   <- dark surfaces

   Light takes error-strong at 6.75/6.39, the best of the three and AA-text.
   Dark takes warning/base at 5.06, also AA-text and the LIBRARY'S OWN warning
   hue, so the semantics stay honest — Zoran's rule explicitly allows a
   brand-appropriate colour where red cannot be loud, and on navy red cannot.

   Bold rather than enlarged: the flag inherits its parent's size, so it is
   already proportionate to whatever it marks — 22px on the manifesto line,
   14px on a form note — and weight is the axis that adds volume without
   disturbing the line box.

   NOT aria-hidden, deliberately, unlike .field__req. The required marker is
   hidden because `required` carries its meaning programmatically; a flag has no
   programmatic equivalent, and the page it sits on can never ship, so being
   announced during review is a small gain at no production cost. */
.content-flag {
  color: var(--feedback-error-text);
  font-weight: var(--weight-body-strong);
}

/* The dark surfaces that exist and could carry copy. NOT .cta: on teal the best
   candidate measures 1.6 and nothing in the palette is loud there, so it is
   recorded as unsolved in D11 rather than given a value that fails. No flag sits
   on teal today, and §5's habit is to act on the day one does. */
.hero .content-flag,
footer .content-flag { color: var(--brand-orange-100); }

/* ==== PRIVACY POLICY ======================================================
   Built 2026-08-07, register D12. Ninth and last page.

   .pagehead--doc is a page-scoped variant, per the D5/D7/A53 pattern: this page
   is LEFT-aligned in a 720 column (Figma's Content 720 at x 280 of Inner 1280,
   with the title itself LEFT), where the form pages are CENTRED in 640. Same
   header band otherwise, so the padding is shared rather than restated. */
.pagehead--doc .pagehead__inner { text-align: left; }
.pagehead--doc .pagehead__title { max-width: 720px; }

/* The document column: 720, centred, left-aligned — Figma's Document 720. */
.legal { max-width: 720px; margin-inline: auto; }

/* 32 between sections, and none above the first — Figma gives every H3 block
   paddingTop 32 except the opening one, which is 0. Expressed as an adjacent
   sibling so the first section needs no exception of its own. */
.legal__section + .legal__section { margin-top: 32px; }

.legal__h {
  font-family: var(--font-display);
  font-weight: var(--weight-display);
  font-size: var(--fs-row-title);
  line-height: var(--lh-row-title);
  color: var(--text-primary);
  margin: 0;
}
/* The sub-heading is BODY font at hero-sub size in the primary colour — Inter
   Regular 18/28, not a display face. Read from the node, not assumed: a
   sub-heading that looked like a smaller .legal__h would have been the obvious
   guess and is wrong. */
.legal__sub {
  font-family: var(--font-body);
  font-weight: var(--weight-body);
  font-size: var(--fs-hero-sub);
  line-height: var(--lh-hero-sub);
  color: var(--text-primary);
  margin: 24px 0 0;
}
.legal__p {
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--text-secondary);
  margin: 16px 0 0;
}
/* 12 rather than 16 after a sub-heading, per Figma's paddingTop on the two
   paragraphs that follow one. */
.legal__sub + .legal__p { margin-top: 12px; }

/* Em-dash markers, drawn as a grid column rather than a list-style: Figma holds
   the dash as its own TEXT node beside the item content, and no list-style-type
   produces an em dash. The reset already removed bullets and padding, and
   role="list" is on the <ul> per C12 for Safari. */
.legal__list { display: grid; gap: 8px; margin: 16px 0 0; padding-left: 16px; }
.legal__list li {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 8px;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--text-secondary);
}
.legal__list li::before { content: "\2014"; }

/* Figma's Title Wrap carries paddingBottom 12 and an Effective Date Wrap below
   it (221:182 / 221:184), which is why this header is 224 and the form pages' is
   320 with an intro. Missed on the first pass and caught by the height
   cross-check — the header measured 192 against 224, and the 32 was exactly this
   12 plus the 20-tall date line. */
.pagehead--doc .pagehead__title { margin-bottom: 12px; }
.pagehead__date {
  font-size: var(--fs-body-sm);
  line-height: var(--lh-body-sm);
  color: var(--text-secondary);
  margin: 0;
}

/* NESTED SECTIONS INHERIT THE PAGE INSET — zero it here. Register D12.

   §9 puts the horizontal inset ladder on `section, footer` as ELEMENT selectors,
   deliberately, "so a new band picks them up without being added to a list".
   That is right for top-level bands and it also reaches NESTED ones: this page is
   the first on the site to put a <section> inside a section, and each one picked
   up 48px each side at 1440, rendering every paragraph at 624 instead of 720.

   Nothing showed it element by element — nothing overflowed, nothing looked broken,
   every measure stayed inside §9's 75-character ceiling — and it surfaced only as
   long paragraphs wrapping one line more than Figma, which the page-height
   cross-check reported as +288. (The word this comment first used for that is on
   §3's banned list, and the gate caught it here, in a comment, exactly as §4
   describes for colour literals.)

   Fixed on the class rather than by narrowing §9's selector, which is load-bearing
   for all nine pages. The general hazard is recorded: a nested <section> needs its
   inset zeroed, and it is the fourth instance of "a site-wide rule reaching
   somewhere its author did not picture". */
.legal__section { padding-inline: 0; }

/* ==========================================================================
   ABOUT US — the three blocks that page introduces
   Figma page 763:279, artboards 763:280 / 763:636. Register D18.
   ========================================================================== */

/* ---- Statement hero ----------------------------------------------------
   A hero with no figure and no button. .hero__inner is a grid whose two-column
   rules are all scoped to a --hero-fig variant class, so with one child and no
   variant it stays single-column at every width and needs nothing disabled.

   The gaps are Figma's own and are NOT the shared --gap-after-* set, which is
   12/16/32 at both breakpoints against Figma's 16/24/32 desktop and 12/16/24
   mobile. That mismatch is `O10`, which is a site-wide question about all six
   heroes; scoping the right values to this class settles About without
   pre-empting it. The mobile subhead is Figma's 16/24 for the same reason —
   the shared --fs-hero-sub is 18/28 at both breakpoints and O10 records that
   Figma's mobile value is 16/24. */
/* 682, not the shared 616. Figma's Left Column is 682 at both breakpoints and
   the headline wraps to exactly three 56px lines inside it; at 616 it wraps to
   four and the hero grows 56. That is A48b's finding on the homepage hero,
   arriving again on the only other hero with no figure beside it — with no
   figure to set the band height, the copy width IS the height. */
/* The decorative logo mark (O16, Zoran's pick). Anchored to .hero__inner rather
   than to the section, so above 1440 it tracks the capped content column instead
   of drifting toward the viewport edge with it.

   The numbers are the artboard's, converted once. Figma places the mark at
   (825, -44) 520x620 in a 1440 section whose inner starts at x 80 and whose
   padding-top is 96, so relative to the inner that is right 15 (1360 - 1345) and
   top -140 (-44 - 96). Mobile: (175, 290) 319x380 in a 375 section with a 343
   inner at x 16 and 64 of padding-top, giving right -135 and top 226.

   OVERFLOW HIDDEN ON THE SECTION is what crops it, which is the design: the band
   edge is the crop, per the chosen variant. It clips nothing else — the mark is
   the only thing on this page that leaves its band.

   POSITION ABSOLUTE also means it cannot affect the band height, which is the
   whole reason B was measurable as "changes nothing": 532/580 with and without. */
.hero--statement { position: relative; overflow: hidden; }
.hero--statement .hero__inner { position: relative; }
.hero__mark {
  position: absolute;
  z-index: 0;                      /* behind the copy, never over it */
  width: 319px;
  height: 380px;
  right: -135px;
  top: 226px;
  opacity: 0.07;
  pointer-events: none;
}
@media (min-width: 768px) {
  .hero__mark { width: 520px; height: 620px; right: 15px; top: -140px; }
}
/* The copy sits above the mark. Without this the two are siblings at the same
   stacking level and source order alone decides — which happens to be correct
   today and would stop being correct the moment anything is inserted before it. */
.hero--statement .hero__copy { position: relative; z-index: 1; }

/* START, not the inherited centre. .hero__inner is `justify-items: center`, and
   on the product pages that is load-bearing — §9 records it as what keeps a
   capped copy and a capped figure sharing a left edge while stacked. With no
   figure and no two-column rule to reset it, this hero never escaped it, so the
   copy sat centred at x 379 where Figma has it at the inner's left edge.

   Invisible until the mark arrived: a centred copy in an empty band just looks
   like a centred copy. It became a defect the moment something else occupied the
   right of the band — at 379 the copy ran under the mark, which the artboard does
   not do. Same family as D18b: a shared hero rule that is only wrong on the hero
   with no figure.

   80, not Figma's 128, per A49 — the section inset the whole site uses, and the
   reason A49 gives holds here too: 128 would be a fourth inset value with no
   other element on the page agreeing with it. */
.hero--statement .hero__inner { justify-items: start; }
.hero--statement .hero__copy { max-width: 682px; }
.hero--statement .hero__lead { margin-top: var(--gap-statement-eyebrow); }
.hero--statement .hero__sub  {
  margin-top: var(--gap-statement-headline);
  margin-bottom: var(--gap-statement-sub);
  max-width: 560px;                    /* Figma's Subhead Wrap, both breakpoints */
}
@media (max-width: 767px) {
  .hero--statement .hero__sub {
    font-size: var(--fs-body);
    line-height: var(--lh-body);
  }
}

/* ---- Reading column ----------------------------------------------------
   §2, §4 and §6: a 720 column of running prose, centred in the band, text left.
   NOT .legalband, which carries the Privacy Policy's document scaffolding —
   sub-headings, dashed lists, 32px between sections — none of which applies to
   three plain sections of prose. The 720 is --content-wide, the same token the
   More list uses, so the measure has one source.

   The title is the section-title scale (Manrope 32/40), not Privacy's 24/32 H3:
   these are section headlines like every other section headline on the site.
   The 16px below it is A53's own value for a headline inside a 720 column —
   Figma's Headline Wrap there carries paddingBottom 16, and these H2 blocks
   carry 0 with the paragraphs carrying 16 above, which computes the same. */
/* White DECLARED, not left transparent. Both compute white on this page, but a
   background audit cannot tell white-by-intent from white-by-accident when the
   computed value is rgba(0, 0, 0, 0) — that is D8's lesson, and it is the reason
   the homepage's mobile showcase carries an explicit white too. */
.reading { background: var(--surface-default); }
.reading__inner {
  max-width: var(--content-wide);
  margin-inline: auto;
}
.reading__title {
  font-family: var(--font-display);
  font-weight: var(--weight-display);
  font-size: var(--fs-section-title);
  line-height: var(--lh-section-title);
  color: var(--text-primary);
  margin: 0;
}
.reading__p {
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--text-secondary);
  margin: var(--gap-after-headline) 0 0;
}
/* §6 ONLY — Zoran 2026-08-12 (A83). .reading is a LEFT-aligned prose column by
   design (D18) and §2 and §4 must stay that way, so this centres §6 alone rather
   than touching the shared rules. It matches §5's pattern, where .offices__head
   is centred.

   ALIGNMENT ONLY: no max-width, no margin, no geometry. The 720 column is
   unchanged, so the paragraph wraps identically and the band height does not
   move — verified 344/392 before and after in both carriers.

   On the SECTION rather than on the title and paragraph separately, because
   text-align inherits and §6 holds nothing else. One declaration, and a third
   element added later is centred without being added to a list — the same
   argument reset.css makes for putting a site-wide rule in one place. */
.reading--center { text-align: center; }

/* Underline on hover and focus-visible, never a colour shift — B5, and the same
   treatment every other in-body link on the site carries. */
.reading__link { text-decoration: underline; }
.reading__link:hover, .reading__link:focus-visible { text-decoration: none; }

/* ---- Offices -----------------------------------------------------------
   A headline and a body paragraph. THE TWO LOCATION CARDS WERE DELETED
   2026-08-12 (A82) on Zoran's ruling: they duplicated the body directly above
   them and the footer's own office entries, so one fact had three carriers on
   one page. Their rules went with them — .offices__list, __card, __city and
   __role had no other consumer, and a rule with no consumer is a value someone
   reaches for by accident later.

   Retired with them: A68 (the cards carried no `Learn more ->` link, so they
   could not become a fifth arrow) and D19 (the vestigial 12px under the role,
   transcribed from a Figma Subtitle Wrap whose Copy Wrap the cards never had).
   Both were records about nodes that no longer exist.

   .offices__head HAS NO margin-bottom, and its absence is the decision. The 48px
   it used to carry was a gap to the deleted card grid; with the grid gone it was
   TRAILING space, so the band closed with 144px under the body (48 + the
   section's own 96) against 96 above the title. Zoran ruled KEEP on 2026-08-12
   and REVERSED it on 2026-08-13 (A82) — trim it, so the space above and below
   the text is equal. Figma moved in the same pass: Section Header 771:160 /
   771:212 now carry paddingBottom 0, so the two carriers still agree.

   Do not reintroduce a bottom margin here to "separate" the band from what
   follows. The section's own padding is what does that, and it is symmetric. */
.offices { background: var(--surface-sunken); }
.offices__head { text-align: center; }
.offices__title {
  font-family: var(--font-display);
  font-weight: var(--weight-display);
  font-size: var(--fs-section-title);
  line-height: var(--lh-section-title);
  color: var(--text-primary);
  margin: 0;
}
.offices__body {
  font-size: var(--fs-hero-sub);
  line-height: var(--lh-hero-sub);
  color: var(--text-secondary);
  max-width: 640px;                    /* Figma's Body Wrap */
  margin: var(--gap-after-headline) auto 0;
}

