/* ═══════════════════════════════════════════════════════════════════════════
   byteroc.css — the one stylesheet.

   This file exists to end a specific bug. P1-T1 found the site's CSS was 100%
   inline across five diverging copies; index.html's :root carried two
   overlapping naming systems in one block with --bd redefined mid-file; and
   /ecommerce/ and /coach/ both *used* var(--cream-2) and var(--mud2), which
   are not defined on those pages, so the colour silently did not apply. Every
   one of those was a token bug, not a markup bug. One file, one set of names,
   defined once — that is the whole point.

   THE RULE: no improvised colour values. Every colour below is either copied
   verbatim from research/brand/byteroc-tokens-c.css or derived from one of
   those tokens with color-mix(). If you need a colour that is not here, the
   answer is a new token, agreed against the brand brief — not a hex literal
   in a component.

   Colour system: Option C "Navy & Emerald", Moin's decision 2026-10-08.
   Gold and amber are retired and appear nowhere in this file.

   Authorities:
     research/brand/byteroc-tokens-c.css      colour, verbatim
     research/brand/WEBSITE-BRAND-BRIEF.md    type, motion, rhythm
     docs/experts/P1-T2-benchmark.md          the reference-tier mechanics
     docs/00_PLAN.md SAC 1-11                 the standing rules

   Contents
     1  Font loading and the metric-matched fallbacks
     2  Primitive tokens (colour, verbatim)
     3  Type scale
     4  Space, size, radius, shadow, border, motion
     5  Theme mapping — dark and light
     6  Reset and base
     7  Layout primitives
     8  Type utilities
     9  Focus
    10  Buttons
    11  Nav and footer scaffold
    12  Reduced motion

   Sections 13-25 are the P4-T2 component kit; they have their own index at
   the head of that block, further down this file.
   ═══════════════════════════════════════════════════════════════════════════ */
/* ───────────────────────────────────────────────────────────────────────────
   1  FONT LOADING AND THE METRIC-MATCHED FALLBACKS

   The two families are SELF-HOSTED from /assets/fonts and preloaded in
   partials/head.php. They used to come from fonts.googleapis.com; P4-T6 proved
   that cost both of the scores this section exists to protect:

     - The Google Fonts stylesheet is a render-blocking THIRD-PARTY request.
       FCP could not happen until a DNS + TLS + request round trip to
       fonts.googleapis.com finished and a second one to fonts.gstatic.com
       began. Lighthouse put the saving at 2,220 ms on mobile.
     - Because the font files were two origins deep behind that stylesheet,
       they could not be preloaded, so the fallback painted for ~2 s before
       the webfont landed. Mobile CLS measured 0.277.

   Self-hosting collapses both: one origin, no blocking stylesheet, and the two
   woff2 files start downloading from <link rel="preload"> in the same round
   trip as this stylesheet. Same families, same weights, same display=swap
   behaviour the brand brief asks for — only the delivery changed.

   THE FILES. Byte-for-byte the variable woff2 Google serves for the `latin`
   subset, with one transformation: the wght axis is instanced down to the range
   the site actually uses (Syne 700-800, DM Sans 400-600), which is exactly what
   the old Google Fonts URL already requested. Glyph coverage, hinting and DM
   Sans's opsz axis are untouched. 97 KB -> 60 KB.
   Rebuild: scripts/build-fonts.py. Licences: assets/fonts/OFL-*.txt (SIL OFL
   1.1 — self-hosting is expressly permitted; the licence must ship with them).

   The unicode-range below is Google's own `latin` range, copied verbatim, so
   coverage is identical to what the live site resolves today. Note it does NOT
   include U+2192 (->). The one "->" on the page fell through to a system font
   under Google Fonts too, so that is unchanged behaviour, not a regression.

   ── The fallbacks ──
   display=swap means the fallback paints first and the webfont swaps in. That
   is good for LCP and bad for CLS — unless the fallback occupies the same
   space. The rules below are adjusted fallbacks: Arial, resized and
   re-metricked so its text box matches the webfont's. With the fonts now
   preloaded from this origin the swap window is near-zero, so these are
   defence in depth rather than the primary mechanism — but they are still
   wrong-or-right, and they were wrong.

   WHAT WAS WRONG, because it is an easy mistake to repeat. There was ONE
   "Syne Fallback" rule at size-adjust: 124.09%, derived from a measured
   "Syne 0.554332" average advance. Syne is a VARIABLE font whose default
   instance is wght 400, and 0.5543 is the wght *700* figure. The hero H1 is
   Syne *800*, where the real figure is 0.803828 — 45% wider. So the fallback
   was ~37% too narrow for every h1 and h2 on the site and set the 39-character
   hero H1 on THREE lines where Syne 800 sets it on FOUR. A height match cannot
   survive a line-count change, which is where 0.277 of CLS came from: at the
   swap the H1 grew a whole line and everything below it moved.

   One size-adjust cannot serve a weight axis this wide (0.5575 -> 0.8038 is a
   44% spread), so there is now one rule PER WEIGHT, matched by font-weight.
   Declaring the weight explicitly also stops the browser synthetic-bolding
   Arial, which was quietly widening the fallback by a further ~4%.

   These numbers are MEASURED, not guessed. Method, so they can be re-derived:
     1. Fetch the woff2 Google serves (a modern browser UA; the no-UA .ttf path
        gives you a different build).
     2. Read upm from `head`, and ascender/descender/lineGap from OS/2 typo
        metrics (both faces set USE_TYPO_METRICS) or hhea (Arial does not).
     3. INSTANCE THE wght AXIS to the weight you are matching, then compute the
        average advance width over a-z plus space, weighted by English letter
        frequency, from cmap + hmtx. Reading hmtx off the un-instanced file is
        the bug described above. Do NOT use OS/2.xAvgCharWidth either: Arial
        reports it under the old OS/2 definition and the Google faces under the
        new one, so comparing them gives a badly wrong ratio.
     4. size-adjust       = avgWidth(webfont @ weight) / avgWidth(Arial)
        ascent-override   = ascender(webfont)  / upm / size-adjust
        descent-override  = |descender|        / upm / size-adjust
        line-gap-override = lineGap            / upm / size-adjust

   Measured 2026-10-08 against the shipped files and macOS Arial
   (scripts/measure-font-metrics.py reproduces every figure):
     avg advance / upm   Syne    700 0.557498 · 800 0.803828
                         DM Sans 400 0.479850 · 500 0.489135 · 600 0.500632
                         Arial       0.448415
     ascender / upm      Syne 0.925 · DM Sans 0.992
     descender / upm     Syne 0.275 · DM Sans 0.310
     lineGap             zero on both
     upm                 1000 on both; Arial 2048

   Re-measure if Google ships a new version of either family.
   ─────────────────────────────────────────────────────────────────────────── */

/* The webfonts, self-hosted. `latin` subset, variable, wght trimmed to use. */

@font-face {
  font-family: "Syne";
  src: url("/assets/fonts/syne-latin-var.woff2") format("woff2");
  font-weight: 700 800;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "DM Sans";
  src: url("/assets/fonts/dmsans-latin-var.woff2") format("woff2");
  font-weight: 400 600;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* The metric-matched fallbacks, one per weight in use. */

@font-face {
  font-family: "Syne Fallback";
  src: local("Arial"), local("Helvetica"), local("Liberation Sans");
  font-weight: 700;              /* h3, .stat__num at --fw-head */
  size-adjust: 124.33%;
  ascent-override: 74.40%;
  descent-override: 22.12%;
  line-gap-override: 0%;
}

@font-face {
  font-family: "Syne Fallback";
  src: local("Arial"), local("Helvetica"), local("Liberation Sans");
  font-weight: 800;              /* h1, h2 at --fw-head-bold — the CLS case */
  size-adjust: 179.26%;
  ascent-override: 51.60%;
  descent-override: 15.34%;
  line-gap-override: 0%;
}

@font-face {
  font-family: "DM Sans Fallback";
  src: local("Arial"), local("Helvetica"), local("Liberation Sans");
  font-weight: 400;              /* body, .lede — the LCP element */
  size-adjust: 107.01%;
  ascent-override: 92.70%;
  descent-override: 28.97%;
  line-gap-override: 0%;
}

@font-face {
  font-family: "DM Sans Fallback";
  src: local("Arial"), local("Helvetica"), local("Liberation Sans");
  font-weight: 500;
  size-adjust: 109.08%;
  ascent-override: 90.94%;
  descent-override: 28.42%;
  line-gap-override: 0%;
}

@font-face {
  font-family: "DM Sans Fallback";
  src: local("Arial"), local("Helvetica"), local("Liberation Sans");
  font-weight: 600;              /* labels, .eyebrow */
  size-adjust: 111.64%;
  ascent-override: 88.85%;
  descent-override: 27.77%;
  line-gap-override: 0%;
}


/* ───────────────────────────────────────────────────────────────────────────
   2  PRIMITIVE TOKENS — colour

   Copied verbatim from research/brand/byteroc-tokens-c.css. Do not edit these
   here; edit the brand file and copy across, so the two never drift.

   Primitives are raw brand values. Components must NOT use them directly —
   use the semantic tokens in §5, which is what makes a section switchable
   between dark and light by changing one attribute.
   ─────────────────────────────────────────────────────────────────────────── */

:root {
  /* dark surfaces */
  --navy-ink:      #0B1129;  /* page/section bg on dark */
  --navy-base:     #101838;  /* cards, nav — exact logo navy */
  --navy-raised:   #172046;  /* elevated cards, inputs */
  --border-dark:   #232D58;  /* standard border on dark */
  --border-faint:  rgba(240, 238, 228, .06);
  --border-strong: rgba(240, 238, 228, .14);
  --muted-dark:    #5A648C;  /* NON-TEXT only (icons, rules) */

  /* text on dark */
  --cream:         #F0EEE4;  /* headlines — never #FFF        16.03:1 */
  --cream-sub:     #A9AFC6;  /* body                           8.55:1 */
  --cream-dim:     #828AAD;  /* helper text   5.49:1 ink · 4.65:1 raised */

  /* light surfaces */
  --paper:         #FAF8F2;
  --paper-2:       #F2EFE7;
  --card-light:    #FFFFFF;
  --border-light:  #E5E1D4;
  --head-light:    #101838;  /* headlines on light            16.34:1 */
  --body-light:    #4C5169;  /* body on light                  7.36:1 */
  --caption-light: #666A78;  /* captions on light             ≥4.69:1 */

  /* THE ONE FEEDBACK HUE — D2, approved by Moin at G3 (2026-10-08).
     Option C defined no feedback colour, so the form's error state shipped
     colourless on the one form this site exists to fill. This is the only
     non-accent hue in the system and it is for form errors and nothing else.

     Clay: a desaturated terracotta (hue 16deg, sat 46%), not red. It reads as
     "wrong" against navy without introducing a second brand colour, and it is
     nowhere near Emerald in hue, so the two can never be confused.

     WHY TWO VALUES AND NOT ONE. D2 asked for a single value at >=4.5:1 on
     BOTH Navy Raised #172046 and Paper #FAF8F2. That is arithmetically
     impossible: Paper (L=0.938696) needs a token luminance <= 0.169710, Navy
     Raised (L=0.016574) needs >= 0.249582, and the best any single sRGB value
     can do against both at once is 3.854:1 — below the bar on both counts.
     So --error is ONE semantic token with a per-theme primitive, exactly as
     --accent-text already resolves to Emerald on dark and Emerald Deep on
     light. Each value clears 4.5:1 on its own ground, with headroom:

       --clay       #CB8972   5.53:1 on Navy Raised #172046   (field surface)
                              6.08:1 on Navy Base   #101838   (card)
                              6.53:1 on Navy Ink    #0B1129   (section)
                              — and 3.25:1 on Paper, which is why it is not
                                the light value.
       --clay-deep  #8D4C34   6.15:1 on Paper       #FAF8F2   (section)
                              6.53:1 on White       #FFFFFF   (field surface)
                              5.68:1 on Paper 2     #F2EFE7   (alt band)
                              — and 2.42:1 on Navy Raised, likewise.

     Both also clear the 3:1 non-text bar of SC 1.4.11 as a field border.
     Colour is ADDED to the error state, never substituted: the 2px effective
     border weight, the glyph and the 500 weight all stay. */
  --clay:          #CB8972;  /* error on dark  */
  --clay-deep:     #8D4C34;  /* error on light */

  /* the ONLY accent */
  --emerald:       #00E5A0;  /* on dark: CTA, figures, labels, numerals, hover borders */
  --emerald-hover: #00CC8E;
  --emerald-soft:  #E6FDF6;  /* badge / soft bg on light */
  --emerald-deep:  #00694A;  /* ANY emerald glyph on light    ≥5.86:1 */
  --on-emerald:    #0B1129;  /* text on emerald buttons       11.29:1 */

  /* focus */
  --focus-dark:    #00E5A0;
  --focus-light:   #0B1129;
}


/* ───────────────────────────────────────────────────────────────────────────
   3  TYPE SCALE

   Syne 800 for every headline, DM Sans for everything else. No other family,
   ever. Sizes, tracking and line-heights are the brand brief's, fluid between
   a 375px and a 1440px viewport.

   Departure D3 (approved at G1) raises two of the brief's floors because they
   were below the readability bar: labels are at least 12px (the brief said
   10-12), and body is at least 15px on desktop and 16px on mobile (the brief
   said 13.5-15). Body is therefore the one step that gets SMALLER as the
   viewport grows — 16px on mobile, 15px from 768px up. That is deliberate:
   16px on mobile also stops iOS zooming when an input is focused.
   ─────────────────────────────────────────────────────────────────────────── */

:root {
  --font-head: "Syne", "Syne Fallback", Arial, sans-serif;
  --font-body: "DM Sans", "DM Sans Fallback", Arial, sans-serif;

  /* Headline sizes. Syne 800, except h3 which is Syne 700 per the brief. */
  --text-h1:      clamp(3rem,    2.4718rem + 2.2535vw, 4.5rem);    /* 48 → 72 */
  --text-h2:      clamp(1.75rem, 1.3539rem + 1.6901vw, 2.875rem);  /* 28 → 46 */
  --text-h3:      clamp(1.25rem, 1.1180rem + 0.5634vw, 1.625rem);  /* 20 → 26 */

  /* Figure sizes — the two stat scales. A figure is Syne 800 and it is NOT a
     heading: it is a number set at display size, so it gets its own steps
     rather than borrowing h2/h1. The wireframe asks for 28px in the trust-bar
     cell and 56px for the calculator's single result figure. */
  --text-figure:    clamp(1.75rem, 1.6181rem + 0.5634vw, 2.125rem);  /* 28 → 34 */
  --text-figure-lg: clamp(2.25rem, 1.5465rem + 3.0047vw, 4rem);      /* 36 → 64 */

  /* Body sizes. */
  --text-lede:    clamp(1.0625rem, 1.0405rem + 0.0939vw, 1.125rem); /* 17 → 18 */
  --text-body:    1rem;        /* 16px; → 15px at ≥768px, see below */
  --text-caption: 0.875rem;    /* 14px — captions and fine print only, never
                                  a paragraph. Above the brief's 13.5px floor
                                  so it stays comfortable next to 15px body. */
  --text-label:   0.75rem;     /* 12px — D3 floor */

  /* Line heights. 1.06, 1.72 and 1.65 are the brief's. The two headline
     values it does not specify are set to the reference tier's convention:
     everything large gets a tight leading that falls as size rises. */
  --lh-h1:    1.06;
  --lh-h2:    1.10;
  --lh-h3:    1.20;
  --lh-lede:  1.72;
  --lh-body:  1.65;
  --lh-label: 1.40;

  /* Tracking. Syne must never run at default tracking. */
  --ls-h1:    -0.035em;
  --ls-h2:    -0.030em;
  --ls-h3:    -0.025em;
  --ls-body:   0em;
  --ls-label:  0.07em;

  --fw-head-bold: 800;  /* h1, h2 */
  --fw-head:      700;  /* h3 */
  --fw-regular:   400;
  --fw-medium:    500;
  --fw-semibold:  600;  /* labels */
}

@media (min-width: 768px) {
  :root { --text-body: 0.9375rem; } /* 15px — D3 desktop floor */
}


/* ───────────────────────────────────────────────────────────────────────────
   4  SPACE, SIZE, RADIUS, SHADOW, BORDER, MOTION
   ─────────────────────────────────────────────────────────────────────────── */

:root {
  /* SECTION RHYTHM — two variables, Ramp's mechanic (P1-T2 §B4).
     Vertical rhythm on this site is these two tokens and nothing else. Do not
     write per-section padding; a section gets --spacer-m or --spacer-l, and
     the whole page's rhythm stays adjustable from one place. */
  --spacer-m: 40px;
  --spacer-l: 80px;

  /* COMPONENT SPACE — for the inside of components only. Never for section
     rhythm; that is what the two spacers above are for. */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  --space-7: 48px;

  /* MEASURE */
  --wrap:        1200px;
  --wrap-narrow: 760px;   /* long-form text; keeps the line length readable */
  --gutter:      20px;

  /* RADIUS — not specified by the brand brief. Set from the reference tier
     (Linear 4/6/8/12/16/24, Stripe's named scale, Ramp's card 12-16px).
     Stripe's nesting rule applies: a card inside a card takes the next step
     down, so the inner curve reads as concentric rather than parallel. */
  --r-xs:   4px;
  --r-sm:   6px;
  --r-md:   8px;
  --r-lg:  12px;
  --r-xl:  16px;
  --r-2xl: 24px;
  --r-pill: 999px;

  /* BORDER WIDTH — a true retina hairline. Linear's mechanic (P1-T2 §B1):
     1px everywhere, halved on 2x displays so the line stays optically 1px
     rather than doubling in weight. */
  --hairline: 1px;

  /* MOTION — one easing for the whole site. D4 (approved at G1) replaces the
     brief's per-row easings with Linear's signature curve: fast out,
     decelerating, zero overshoot. Nothing on this site bounces.
     D4 also sets every hover to 160ms, which overrides the brief's 200ms and
     250ms hover rows. The remaining durations are the brief's motion table. */
  --ease:          cubic-bezier(.32, .72, 0, 1);
  --dur-hover:      160ms;  /* D4 — all hovers, cards included */
  --dur-nav:        350ms;  /* nav transparent → blurred on scroll */
  --dur-accordion:  400ms;  /* FAQ open/close */
  --dur-reveal:     600ms;  /* scroll reveal, opacity + 22px rise */
  --dur-count:     1400ms;  /* stat count-up */
  --stagger:         80ms;  /* per-card delay in a staggered grid */
}

@media (min-width: 768px) {
  :root {
    --spacer-m: 48px;
    --spacer-l: 96px;
    --gutter:   32px;
  }
}

@media (min-width: 992px) {
  :root {
    --spacer-m:  64px;
    --spacer-l: 128px;
  }
}

@media (min-resolution: 192dpi) {
  :root { --hairline: 0.5px; }
}


/* ───────────────────────────────────────────────────────────────────────────
   5  THEME MAPPING — dark and light

   This is the layer components talk to. A component reads var(--surface) and
   var(--text); it never reads var(--navy-ink) or var(--cream). Put
   data-theme="dark" or data-theme="light" on any element and everything
   inside it re-themes, because these are just custom properties and they
   inherit. That is the whole switch — one attribute, nests freely, no
   duplicate rulesets per theme.

   The brief's section rhythm alternates the two deliberately: dark for
   authority and impact, light for trust and readability.

   Note --accent versus --accent-text. The emerald FILL is #00E5A0 on both
   themes, because a button fill with Navy Ink text on it is 11.29:1 and the
   brief names it the one primary style site-wide. Emerald used as a GLYPH —
   text, an icon, a numeral, a check — must become Emerald Deep #00694A on
   light, where raw emerald would fail contrast. Keeping them as two tokens is
   what stops that mistake being made by hand in a component.
   ─────────────────────────────────────────────────────────────────────────── */

[data-theme="dark"] {
  color-scheme: dark;

  --surface:        var(--navy-ink);
  --surface-2:      var(--navy-base);
  --surface-raised: var(--navy-raised);
  --surface-card:   var(--navy-base);

  --text:        var(--cream);
  --text-sub:    var(--cream-sub);
  --text-dim:    var(--cream-dim);
  --text-nontext: var(--muted-dark);   /* icons and rules only, never type */

  --border:        var(--border-dark);
  --border-weak:   var(--border-faint);
  --border-heavy:  var(--border-strong);

  --accent:        var(--emerald);
  --accent-hover:  var(--emerald-hover);
  --accent-text:   var(--emerald);
  --accent-border: var(--emerald);          /* hover + featured hairlines */
  --on-accent:     var(--on-emerald);

  --error:         var(--clay);             /* 5.53:1 on --field-bg */

  /* A soft emerald wash, for the pricing "Most popular" badge and nothing
     else. The brand brief specifies the LIGHT pair (#E6FDF6 bg + Emerald Deep
     text) because §11 Pricing ships on White; this dark pair exists so the
     component is still correct if it is ever placed on a dark band. A flat
     #E6FDF6 fill on navy would be a large light block, which the emerald
     usage rule forbids, so on dark the wash is a 12% tint of the accent. */
  --badge-bg:   color-mix(in srgb, var(--emerald) 12%, transparent);
  --badge-text: var(--emerald);

  /* Input surface. The brand brief assigns Navy Raised to "elevated cards,
     inputs", so a field is one surface step above the card it sits in. */
  --field-bg: var(--navy-raised);

  --focus-color: var(--focus-dark);

  /* Linear's rule: no shadows on dark. Elevation is a surface step —
     ink → base → raised — not a drop shadow. */
  --shadow-sm: none;
  --shadow-md: none;
}

[data-theme="light"] {
  color-scheme: light;

  --surface:        var(--paper);
  --surface-2:      var(--paper-2);
  --surface-raised: var(--card-light);
  --surface-card:   var(--card-light);

  --text:        var(--head-light);
  --text-sub:    var(--body-light);
  --text-dim:    var(--caption-light);
  --text-nontext: var(--caption-light);

  --border:       var(--border-light);
  --border-weak:  color-mix(in srgb, var(--head-light) 6%, transparent);
  --border-heavy: color-mix(in srgb, var(--head-light) 14%, transparent);

  --accent:        var(--emerald);       /* fill only */
  --accent-hover:  var(--emerald-hover);
  --accent-text:   var(--emerald-deep);  /* every emerald glyph on light */
  --accent-border: var(--emerald-deep);  /* a raw-emerald hairline disappears
                                            on Paper; Emerald Deep reads */
  --on-accent:     var(--on-emerald);

  --error:         var(--clay-deep);     /* 6.53:1 on --field-bg, 6.15 on Paper */

  /* The brand brief's specified badge pair, verbatim. */
  --badge-bg:   var(--emerald-soft);
  --badge-text: var(--emerald-deep);

  --field-bg: var(--card-light);

  --focus-color: var(--focus-light);

  /* Ramp's near-zero elevation, tinted with the brand navy rather than pure
     black (Stripe's lesson — a neutral black shadow reads as dirt). */
  --shadow-sm: 0 3px 3px color-mix(in srgb, var(--navy-ink) 12%, transparent);
  --shadow-md: 0 4px 4px color-mix(in srgb, var(--navy-ink) 15%, transparent);
}


/* ───────────────────────────────────────────────────────────────────────────
   6  RESET AND BASE
   ─────────────────────────────────────────────────────────────────────────── */

*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--surface);
  color: var(--text-sub);
  font-family: var(--font-body);
  font-size: var(--text-body);
  font-weight: var(--fw-regular);
  line-height: var(--lh-body);
  letter-spacing: var(--ls-body);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

h1, h2, h3, h4, h5, h6 {
  margin: 0;
  color: var(--text);
  font-family: var(--font-head);
  font-weight: var(--fw-head-bold);
}

p, ul, ol { margin: 0; }
img, svg, video { max-width: 100%; height: auto; display: block; }

a {
  color: inherit;
  text-decoration-color: color-mix(in srgb, currentColor 40%, transparent);
  text-underline-offset: 0.18em;
  transition: color var(--dur-hover) var(--ease),
              text-decoration-color var(--dur-hover) var(--ease);
}

a:hover { text-decoration-color: currentColor; }

/* Inputs must inherit the family, or the browser silently uses its own and
   the page gains a third typeface. */
button, input, select, textarea { font: inherit; color: inherit; }

code, kbd, samp, pre {
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.9em;
}


/* ───────────────────────────────────────────────────────────────────────────
   7  LAYOUT PRIMITIVES
   ─────────────────────────────────────────────────────────────────────────── */

.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.wrap--narrow { max-width: var(--wrap-narrow); }

/* A section is a themed band with the standard rhythm. Add .section--lg for
   the major beats (hero, the gap calculator, the closing ask). */
.section {
  background: var(--surface);
  color: var(--text-sub);
  padding-block: var(--spacer-m);
}

.section--lg { padding-block: var(--spacer-l); }
.section--flush { padding-block: 0; }

/* Hairline rules between bands. */
.section--ruled { border-block-start: var(--hairline) solid var(--border); }

.stack > * + * { margin-block-start: var(--space-4); }
.stack--lg > * + * { margin-block-start: var(--space-6); }


/* ───────────────────────────────────────────────────────────────────────────
   8  TYPE UTILITIES
   ─────────────────────────────────────────────────────────────────────────── */

.h1, h1 {
  font-size: var(--text-h1);
  line-height: var(--lh-h1);
  letter-spacing: var(--ls-h1);
  font-weight: var(--fw-head-bold);
}

.h2, h2 {
  font-size: var(--text-h2);
  line-height: var(--lh-h2);
  letter-spacing: var(--ls-h2);
  font-weight: var(--fw-head-bold);
}

.h3, h3 {
  font-size: var(--text-h3);
  line-height: var(--lh-h3);
  letter-spacing: var(--ls-h3);
  font-weight: var(--fw-head);  /* Syne 700, per the brief */
}

.lede {
  font-size: var(--text-lede);
  line-height: var(--lh-lede);
  color: var(--text-sub);
}

.caption {
  font-size: var(--text-caption);
  color: var(--text-dim);
}

/* The eyebrow/label. Emerald is allowed here — the brief lists eyebrows and
   labels among the places it may go. On light that resolves to Emerald Deep
   automatically. */
.eyebrow {
  display: block;
  font-size: var(--text-label);
  line-height: var(--lh-label);
  letter-spacing: var(--ls-label);
  font-weight: var(--fw-semibold);
  text-transform: uppercase;
  color: var(--accent-text);
}

.eyebrow--muted { color: var(--text-dim); }

/* A dollar or percent figure. The other place emerald is allowed. */
.figure {
  font-family: var(--font-head);
  font-weight: var(--fw-head-bold);
  letter-spacing: var(--ls-h2);
  color: var(--accent-text);
  font-variant-numeric: tabular-nums;
}

.text-sub { color: var(--text-sub); }
.text-dim { color: var(--text-dim); }

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}


/* ───────────────────────────────────────────────────────────────────────────
   9  FOCUS

   Vercel's mechanic (P1-T2 §B2): a 2px gap in the surface colour, then a 2px
   ring. The gap is what makes the ring legible against a focused element of
   any colour, including the emerald button. Emerald ring on dark, Navy Ink
   ring on light — both from the brand brief.

   :focus-visible only, so a mouse click does not draw a ring, but every
   keyboard path does. WCAG 2.2 AA.
   ─────────────────────────────────────────────────────────────────────────── */

:root { --focus-ring: 0 0 0 2px var(--surface), 0 0 0 4px var(--focus-color); }

:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
  border-radius: var(--r-xs);
}

/* Windows High Contrast Mode strips box-shadow, so keep a real outline there. */
@media (forced-colors: active) {
  :focus-visible { outline: 2px solid Highlight; outline-offset: 2px; }
}

.skip-link {
  position: absolute;
  left: var(--space-4);
  top: var(--space-4);
  z-index: 100;
  padding: var(--space-3) var(--space-4);
  background: var(--accent);
  color: var(--on-accent);
  font-weight: var(--fw-semibold);
  border-radius: var(--r-md);
  transform: translateY(-200%);
  transition: transform var(--dur-hover) var(--ease);
}

.skip-link:focus-visible { transform: translateY(0); }


/* ───────────────────────────────────────────────────────────────────────────
   10  BUTTONS

   One primary style site-wide: emerald fill, Navy Ink text, 11.29:1.
   Secondary is outlined. There is no third.
   ─────────────────────────────────────────────────────────────────────────── */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-5);
  min-height: 44px;              /* tap target */
  border: var(--hairline) solid transparent;
  border-radius: var(--r-md);
  font-size: var(--text-body);
  font-weight: var(--fw-semibold);
  line-height: 1.2;
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--dur-hover) var(--ease),
              border-color     var(--dur-hover) var(--ease),
              color            var(--dur-hover) var(--ease),
              transform        var(--dur-hover) var(--ease);
}

.btn--primary {
  background: var(--accent);
  color: var(--on-accent);
}

.btn--primary:hover { background: var(--accent-hover); }

.btn--secondary {
  background: transparent;
  color: var(--text);
  border-color: var(--border);
}

.btn--secondary:hover { border-color: var(--accent); }

.btn--sm {
  /* 44px, not the 38px this shipped with. G4 §6.7, Moin's decision. .btn
     itself sets 44px as the tap target and this modifier was the one place
     that opted out of it; the instances are the four "Get a quote" buttons on
     /pricing/ and the nav CTA. The padding and the 12px label type are
     unchanged, so it is still visually the small button — min-height is what
     binds either way, because the content box measures ~32px. */
  min-height: 44px;
  padding: var(--space-2) var(--space-4);
  font-size: var(--text-label);
  letter-spacing: 0.01em;
}


/* ───────────────────────────────────────────────────────────────────────────
   11  NAV AND FOOTER SCAFFOLD

   Enough to make partials/nav.php and partials/footer.php render correctly
   and accessibly. P4-T2 replaces these with the real component kit, including
   the mobile menu and the nav's scroll-blur. Nothing here hardcodes a colour.
   ─────────────────────────────────────────────────────────────────────────── */

.nav {
  position: sticky;
  top: 0;
  z-index: 50;
  background: transparent;
}

/* THE BAR'S PAINT LIVES ON ITS OWN LAYER, AND THAT IS NOT COSMETIC.
   backdrop-filter establishes a containing block for fixed-position
   descendants, exactly as transform and filter do. With the blur on .nav
   itself, the scrim's `position: fixed; inset: 0` resolved against the 64px
   bar instead of the viewport, so it measured 375x64 and dimmed only the nav
   — the page behind the open sheet was never covered at all. The sheet's own
   `inset: 64px 0 auto 0` happened to land correctly only because the bar is
   exactly 64px tall and pinned to the top, which is a coincidence waiting to
   break.

   Moving the translucent fill and the blur onto an absolutely positioned
   child leaves .nav with no filter, so both fixed children resolve against
   the viewport, which is what they always meant. */
.nav__bg {
  position: absolute;
  inset: 0;
  background: transparent;
  border-block-end: var(--hairline) solid transparent;
  transition: background-color var(--dur-nav) var(--ease),
              border-color     var(--dur-nav) var(--ease);
  pointer-events: none;
}

/* space-between does the whole job at every variant: logo | links | CTA on
   the homepage, and logo | CTA once the links hide on small screens. No
   margin-auto rules, which would strand the CTA mid-bar the moment the links
   became display:none. */
.nav__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-5);
  min-height: 64px;

  /* Above the scrim (z-index 55). The bar is part of the open sheet, not part
     of the background it dims: the burger is the sheet's close control, so
     dimming it — which is what happened before this line — makes the one
     affordance that dismisses the thing look disabled. */
  position: relative;
  z-index: 58;
}

.nav__logo { display: inline-flex; align-items: center; flex: 0 0 auto; }

/* The bar's right-hand group. The CTA and the burger have to travel together:
   below 880px the links are position:fixed and leave the flow, and with three
   loose children space-between would strand the CTA in the middle of the bar. */
.nav__end {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex: 0 0 auto;
}
.nav__logo img { width: auto; height: 32px; }

/* Hidden below 880px. The mobile menu that replaces them is P4-T2's job —
   until then the primary CTA stays reachable at every width, and no page
   that ships depends on this nav. */
.nav__links {
  display: none;
  list-style: none;
  margin: 0;
  padding: 0;
  gap: var(--space-5);
}

/* :not(.btn) is load-bearing, not tidiness. `.nav__links a` is specificity
   (0,1,1) and `.btn--primary` is (0,1,0), so without the exclusion these
   rules beat the button's own colour and the CTA at the foot of the mobile
   sheet rendered Cream Sub text on an emerald fill. */
.nav__links a:not(.btn) {
  color: var(--text-sub);
  text-decoration: none;
  font-size: var(--text-body);
}

.nav__links a:not(.btn):hover,
.nav__links a:not(.btn)[aria-current="page"] { color: var(--text); }

.nav__back {
  color: var(--text-sub);
  text-decoration: none;
  font-size: var(--text-body);
}

.nav__back:hover { color: var(--text); }

@media (min-width: 880px) {
  .nav__links { display: flex; }
}

.footer {
  background: var(--surface);
  color: var(--text-sub);
  border-block-start: var(--hairline) solid var(--border);
  padding-block: var(--spacer-m);
}

.footer__inner {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-6);
  justify-content: space-between;
}

.footer__brand img { height: 32px; width: auto; }
.footer__line { margin-block-start: var(--space-3); max-width: 34ch; color: var(--text-dim); }

.footer__nav ul,
.footer__social {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.footer__social { flex-direction: row; gap: var(--space-4); flex-wrap: wrap; }

.footer__nav a,
.footer__social a,
.footer__email { color: var(--text-sub); text-decoration: none; }

.footer__nav a:hover,
.footer__social a:hover,
.footer__email:hover { color: var(--text); }

.footer__contact { display: flex; flex-direction: column; gap: var(--space-2); }
.footer__email { font-weight: var(--fw-medium); }

/* .footer__book is deleted with the markup — D7, approved at G3. */

/* FIX 6 — footer social hit areas. Approved by Moin at G3; QA m5.
   These are word links, so the "X" rendered as a 10 x 21px target. WCAG 2.2
   AA SC 2.5.8 is satisfied at 24x24 by the inline exception, but 10px wide is
   not a thumb target under any reading and the brief asks for 44.

   Two layers, because the two numbers do different jobs:
     - the box itself goes to at least 24 x 24, which is the measurable
       minimum and is laid out, so it affects the row;
     - ::after centres an invisible 44 x 44 floor over it, which extends the
       pressable area without taking any layout space, so the row's rhythm is
       unchanged. Pointer events land on the pseudo-element because it is a
       child of the anchor.
   The row gap goes 16 -> 24px so two 44px floors can never overlap: the
   widest expansion is the "X", whose floor overhangs about 17px each side,
   and 24 > 17 leaves clearance on both neighbours. Measured, not assumed. */
.footer__social { gap: var(--space-5); }

.footer__social a {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: 24px;
  min-block-size: 24px;
}

.footer__social a::after {
  content: "";
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: 50%;
  inline-size: 100%;
  block-size: 100%;
  min-inline-size: 44px;
  min-block-size: 44px;
  transform: translate(-50%, -50%);
}

.footer__legal {
  margin-block-start: var(--space-6);
  padding-block-start: var(--space-4);
  border-block-start: var(--hairline) solid var(--border-weak);
  color: var(--text-dim);
  font-size: var(--text-caption);
}


/* ───────────────────────────────────────────────────────────────────────────
   12  REDUCED MOTION

   A genuine no-motion fallback, not a slower animation. Durations collapse to
   effectively zero so any JS that waits on transitionend still fires, and
   smooth scrolling is turned off.

   P4-T4 SETTLED THE SCROLL-REVEAL HALF OF THIS, AND SETTLED IT IN THE
   MARKUP RATHER THAN HERE. There is no .rv rule in this file, no reveal
   keyframe and no `brc-on` class, because nothing meaningful on this site is
   ever at opacity: 0 in CSS. Every page is served in its final, readable
   state; js/byteroc-motion.js writes a start state only after it has passed
   its own reduced-motion and capability checks, and only onto elements that
   are already below the fold. Under `reduce` that file returns before it
   writes a single style, so the final state is simply what the server sent.
   Nothing in this media block has to undo a reveal, because no reveal was
   ever started. See the header of js/byteroc-motion.js.
   ─────────────────────────────────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  :root {
    --dur-hover:     1ms;
    --dur-nav:       1ms;
    --dur-accordion: 1ms;
    --dur-reveal:    1ms;
    --dur-count:     1ms;
    --stagger:       0ms;
  }

  html { scroll-behavior: auto; }

  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
}


/* ═══════════════════════════════════════════════════════════════════════════
   P4-T2 — THE CORE COMPONENT KIT

   Everything below is the reusable kit the pages assemble from. It is NOT a
   page: no section here knows what content goes in it.

   Provenance. The stack is Option A (static HTML/CSS + PHP partials), so none
   of this is 21st.dev React/Tailwind source. Each component is authored from
   the LAYOUT DECISION recorded for its 21st.dev pick in
   docs/experts/P2-T1-component-shortlist.md and re-checked in
   docs/G1_DIRECTION.md §4 — the grid, the hierarchy, the semantics — expressed
   in this file's own tokens. Nothing is copied, which is also why G1 open
   question 12 (the 21st.dev commercial licence) does not block the build.

   Donor per component is named in each section header.

   TYPEFACE. Three of the picks (hirael's comparison-02, comparison-03 and
   feature-08) set `font-serif` on their H2 and `font-mono` on their eyebrow,
   and pricing-4 and booking-2 both set `font-heading`. P2-T1 called that the
   single highest-risk detail in the build: retype some and the page splits in
   half. None of those classes survives. Mechanically checkable, and the check
   is this: every `font-family` declaration in this stylesheet resolves to
   either var(--font-head) (Syne) or var(--font-body) (DM Sans). The only
   exceptions are the @font-face names in §1 — which ARE those two families,
   self-hosted, plus their per-weight metric-matched fallbacks — and §6's
   `code/kbd/samp/pre` rule,
   which is the browser's own monospace stack and is used by no component in
   this kit — there is no third family in the design.

       grep -n 'font-family' css/byteroc.css

   Contents
    13  Buttons — the full set and every state
    14  Section composition
    15  The hairline grid primitive
    16  Card
    17  Stat / figure block
    18  Badge and check list
    19  Accordion (FAQ)
    20  Nav — scroll state, toggle, mobile sheet
    21  Pricing card
    22  Comparison table
    23  Form controls
    24  Footer newsletter widget
    25  Reduced-motion overrides for the kit
   ═══════════════════════════════════════════════════════════════════════════ */


/* ───────────────────────────────────────────────────────────────────────────
   13  BUTTONS — the full set and every state

   §10 above established .btn, --primary and --secondary. This completes them:
   a ghost tier, pressed and disabled states, a block width, and a row.

   Three tiers and no more:
     primary    emerald fill, Navy Ink text — 11.29:1. One per viewport.
     secondary  transparent, 1px border. The outlined tier.
     ghost      transparent, NO border. Text that behaves like a button —
                used for the hero's "Calculate your gap" and for text links
                that need a 44px tap target.

   The focus ring is §9's and is not re-declared here; it already works on an
   emerald fill because of the 2px surface-coloured gap.
   ─────────────────────────────────────────────────────────────────────────── */

/* A press is the one transform a button gets. Hover is colour only — the lift
   is reserved for cards, so the two never read as the same affordance. */
.btn:active { transform: translateY(1px); }

.btn--primary:active { background: var(--accent-hover); }

/* The outlined tier gains an emerald hairline and emerald text on hover. The
   fill stays transparent: a secondary that fills on hover competes with the
   primary at the exact moment the user is choosing between them. */
.btn--secondary:hover {
  border-color: var(--accent-border);
  color: var(--accent-text);
}

.btn--ghost {
  background: transparent;
  color: var(--text-sub);
  border-color: transparent;
  padding-inline: var(--space-3);
}

.btn--ghost:hover {
  color: var(--accent-text);
  background: color-mix(in srgb, var(--accent) 8%, transparent);
}

/* Disabled. Both spellings: :disabled for <button>, [aria-disabled] for an
   <a> that cannot carry the real attribute. aria-disabled alone does not stop
   navigation, so a disabled link must also lose its href in markup — the
   style exists so the two cases look identical. */
.btn:disabled,
.btn[aria-disabled="true"] {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

.btn--block { width: 100%; }

.btn--lg {
  min-height: 52px;
  padding: var(--space-4) var(--space-6);
}

/* Icons inside a button never shrink and never inherit the text colour by
   accident — currentColor is explicit so an inline SVG tracks the tier. */
.btn svg {
  flex: 0 0 auto;
  width: 1em;
  height: 1em;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
}

/* A row of buttons. Wraps rather than overflowing; full-width and stacked
   below 480px, where two side-by-side buttons are both too narrow to read. */
.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: center;
}

@media (max-width: 479px) {
  .btn-row { flex-direction: column; align-items: stretch; }
  .btn-row > .btn { width: 100%; }
}


/* ───────────────────────────────────────────────────────────────────────────
   14  SECTION COMPOSITION

   .section and .section--lg are in §7 and carry the whole vertical rhythm in
   the two spacer tokens. This adds the inside of a section: the heading block
   every band shares.

   ON "ONE CLASS FOR DARK AND LIGHT". The switch is the data-theme attribute
   from §5, and it is deliberately NOT duplicated as a .section--dark /
   .section--light class pair. Two ways to theme the same band is precisely
   the bug class P1-T1 found on the live site, where two naming systems
   overlapped in one :root and a redefinition mid-file went unnoticed. One
   switch, inherited by everything inside it:

       <section class="section section--lg" data-theme="light"> … </section>

   Changing a band from dark to light is still a one-token edit; it is just an
   attribute rather than a class.
   ─────────────────────────────────────────────────────────────────────────── */

/* The shared heading block: eyebrow → h2 → lede. Measure-capped so a lede
   never runs the full 1200px, which is unreadable at any size. */
.section__head {
  max-width: 62ch;
  margin-block-end: var(--space-7);
}

.section__head > * + * { margin-block-start: var(--space-3); }
.section__head .lede { max-width: 56ch; }

/* Centred variant, for the bands whose heading sits over a symmetrical grid. */
.section__head--center {
  margin-inline: auto;
  text-align: center;
}

.section__head--center .lede { margin-inline: auto; }

/* A source line under a heading block or a figure. Every statistic on this
   site prints its source in the same cell as the number — not a tooltip, not
   a footnote marker. This is the style that line takes everywhere. */
.source {
  display: block;
  font-size: var(--text-caption);
  line-height: 1.45;
  color: var(--text-dim);
  text-wrap: pretty;
}

.source a { color: inherit; }
.source a:hover { color: var(--text-sub); }


/* ───────────────────────────────────────────────────────────────────────────
   15  THE HAIRLINE GRID PRIMITIVE

   Donor: logo-cloud-1 (olewandowski1) and comparison-03 (hirael, MIT) — both
   build every divider as `grid gap-px` over a border-coloured background,
   with the cells painted in the surface colour. The gap IS the rule, so two
   adjacent cells can never double a border to 2px, and no cell needs to know
   which of its edges are interior.

   This is the brand brief's "hairline borders instead of shadows", as one
   primitive. §3's trust bar, §4's two panels, §7's audience router and §12's
   promise row are all this grid at different cell counts.
   ─────────────────────────────────────────────────────────────────────────── */

.hgrid {
  display: grid;
  gap: var(--hairline);
  background: var(--border);
  border: var(--hairline) solid var(--border);
  border-radius: var(--r-lg);
  overflow: hidden;         /* clips the cell corners to the outer radius */
  grid-template-columns: 1fr;
}

.hgrid > * {
  background: var(--surface);
  padding: var(--space-5);
  margin: 0;
}

/* Column counts. Mobile is always one column: a hairline grid at 375px with
   two columns gives every cell about 160px of usable width, which no figure
   plus claim plus source line survives. */
@media (min-width: 560px) {
  .hgrid--2 { grid-template-columns: repeat(2, 1fr); }
  .hgrid--4 { grid-template-columns: repeat(2, 1fr); }
}

@media (min-width: 880px) {
  .hgrid--3 { grid-template-columns: repeat(3, 1fr); }
  .hgrid--4 { grid-template-columns: repeat(4, 1fr); }
}

/* Cells on a raised surface, for a grid sitting on the section background
   rather than inside a card. */
.hgrid--raised > * { background: var(--surface-card); }

/* Whole-cell links — §7's audience router. The cell is the hit area, so the
   entire card is one target rather than a link buried in a paragraph. */
a.hgrid__cell,
.hgrid > a {
  display: block;
  text-decoration: none;
  color: inherit;
  transition: background-color var(--dur-hover) var(--ease),
              box-shadow      var(--dur-hover) var(--ease);
}

/* THE BRAND'S CARD-HOVER ROW, ON THE ONE CLICKABLE CARD SURFACE THE PAGES
   HAVE — AND WITHOUT THE LIFT, DELIBERATELY. (P4-T4.)

   The brief's card hover is a 4px lift plus an emerald border, and
   .card--interactive below implements exactly that for a free-standing card.
   A hairline grid cell cannot use it: .hgrid clips to its outer radius, so a
   cell translated up by 4px loses 4px off its own top edge and exposes 4px of
   the grid's border-coloured background underneath — the hairline rails come
   apart on hover. So the lift is dropped here and the emerald is kept.

   It is drawn as an inset hairline rather than a border because these cells
   have no border to recolour: the rails between them ARE .hgrid's background
   showing through 1px gaps. An inset shadow is the same 1px line optically,
   paints only, and cannot change the cell's box — which is what keeps a hover
   on a 2-up grid from reflowing the row.

   Same 160ms, same signature easing, same --accent-border as the card. */
.hgrid > a:hover {
  background: var(--surface-card);
  box-shadow: inset 0 0 0 var(--hairline) var(--accent-border);
}

/* FOCUS IS NOT HOVER. This selector used to carry :focus-visible alongside
   :hover, which silently destroyed the focus ring: §9's global :focus-visible
   sets `outline: none` and draws the ring as a box-shadow, and `.hgrid > a`
   (0,2,1) beats the bare `:focus-visible` (0,1,0) on the box-shadow while the
   outline:none still applies. A keyboard user landing on a router cell got the
   1px hover hairline and nothing else — indistinguishable from a mouse hover,
   and the only visible difference between two adjacent cells was a hairline
   recolour. WCAG 2.2 AA 2.4.11/2.4.13. (P6-T3b.)

   The ring is drawn INSET. .hgrid is `overflow: hidden` so it can clip the
   cell corners to its outer radius, which means an outward 4px ring on a cell
   is clipped by the grid and overpainted by the next sibling cell. So Vercel's
   mechanic (§9) is mirrored inwards: 2px of the cell's own surface at the
   edge, then the 2px ring inside it. Same gap-then-ring legibility, drawn in
   the only direction this primitive leaves available.

   forced-colors is unaffected: §9's `outline: 2px solid Highlight` lives in
   its own media block, and box-shadow never replaces it. */
.hgrid > a:focus-visible {
  background: var(--surface-card);
  box-shadow: inset 0 0 0 2px var(--surface-card),
              inset 0 0 0 4px var(--focus-color);
}


/* ───────────────────────────────────────────────────────────────────────────
   16  CARD

   Donor: stats-04 (shadcnui-blocks) for the chrome — G1 §4.2 demoted it from
   a section pick to exactly this — plus feature-08 (hirael, MIT) for the
   bordered icon tile.

   Hover is the brand brief's card hover with departure D4's duration: 160ms,
   lift 4px, border → emerald. Only .card--interactive gets it. A card that
   is not a link must not lift, or the page teaches that lift means nothing.

   Works on Ink, Base and Raised:
     .card            --surface-card  — on an Ink band
     .card--raised    --surface-raised — on a Base band, or nested in a card
   ─────────────────────────────────────────────────────────────────────────── */

.card {
  background: var(--surface-card);
  border: var(--hairline) solid var(--border);
  border-radius: var(--r-lg);
  padding: var(--space-5);
  box-shadow: var(--shadow-sm);   /* none on dark, by §5 */
}

.card--raised { background: var(--surface-raised); }
.card--flush { padding: 0; }

/* Stripe's nesting rule: a card inside a card takes the next radius step
   down, so the inner curve reads as concentric rather than parallel. */
.card .card { border-radius: var(--r-md); }

.card__title {
  font-size: var(--text-h3);
  line-height: var(--lh-h3);
  letter-spacing: var(--ls-h3);
  font-weight: var(--fw-head);
  font-family: var(--font-head);
  color: var(--text);
}

.card__body { color: var(--text-sub); }
.card__title + .card__body { margin-block-start: var(--space-3); }

.card__foot {
  margin-block-start: var(--space-5);
  padding-block-start: var(--space-4);
  border-block-start: var(--hairline) solid var(--border-weak);
}

/* The bordered square icon tile. No radius beyond --r-sm: feature-08's tile
   is square-ish and that drafted, instrument-like feel is the point. */
.card__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  margin-block-end: var(--space-4);
  border: var(--hairline) solid var(--border);
  border-radius: var(--r-sm);
  color: var(--text-nontext);
}

.card__icon svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.75; }

/* Interactive. transform is promoted with will-change only while hovered —
   a permanently promoted layer on every card costs memory for nothing. */
.card--interactive {
  transition: transform    var(--dur-hover) var(--ease),
              border-color var(--dur-hover) var(--ease);
}

.card--interactive:hover,
.card--interactive:focus-within {
  transform: translateY(-4px);
  border-color: var(--accent-border);
}

/* A card that is itself a link. */
a.card { display: block; text-decoration: none; color: inherit; }


/* ───────────────────────────────────────────────────────────────────────────
   17  STAT / FIGURE BLOCK

   Donor: logo-cloud-1's figure+label cell, repurposed from a logo grid to a
   sourced-stat grid (G1 §4.1 §3).

   THE RULE THIS COMPONENT ENFORCES: every statistic on this site prints its
   source in the same cell, at caption size, visibly. .stat__source is not
   optional styling — it is the component. A .stat without one is a bug, and
   the markup contract in docs/experts/P4-T2-component-notes.md says so.

   COUNT-UP CONTRACT (P4-T4 owns the animation, not this file):

     <p class="stat__figure">
       <span class="visually-hidden">70%</span>
       <span class="stat__num" aria-hidden="true"
             data-count-to="70" data-count-suffix="%">70%</span>
     </p>

   The visible span holds the FINAL value as static HTML, so the number is
   correct with JavaScript off and is never only in JS. It is aria-hidden, and
   the screen reader reads the static twin instead — so a count-up can never
   turn into a stream of announcements.
   ─────────────────────────────────────────────────────────────────────────── */

.stat { margin: 0; }

.stat__figure {
  display: block;
  margin: 0;
  font-family: var(--font-head);
  font-weight: var(--fw-head-bold);
  font-size: var(--text-figure);
  line-height: 1.05;
  letter-spacing: var(--ls-h2);
  color: var(--accent-text);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* The single large figure — §5's calculator result, and nothing else on a
   page. More than one 64px number competes with itself. */
.stat--lg .stat__figure { font-size: var(--text-figure-lg); }

/* A figure that is a plain fact rather than a dollar or percent value takes
   the text colour: emerald is reserved for $ and % figures. */
.stat--plain .stat__figure { color: var(--text); }

.stat__claim {
  margin-block-start: var(--space-2);
  color: var(--text-sub);
  text-wrap: pretty;
}

.stat__source {
  display: block;
  margin-block-start: var(--space-2);
  font-size: var(--text-caption);
  line-height: 1.45;
  color: var(--text-dim);
  text-wrap: pretty;
}

/* First-party observation, not a published statistic. The label is the
   honesty signal and it must stay visible — G1's system rules. */
.stat__source--firstparty { font-style: normal; }
.stat__source--firstparty::before {
  content: "Our own audits — ";
  font-weight: var(--fw-medium);
}

/* A figure slot deliberately left empty, e.g. §6's sixth flow card, which
   carries no number because no benchmark is published for it. The asymmetry
   is an honesty signal and must not be "fixed". */
.stat--nonumber .stat__figure {
  color: var(--text-dim);
  font-size: var(--text-caption);
  font-family: var(--font-body);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-body);
  text-transform: none;
}

/* A marked placeholder. May ship empty and marked; may never ship filled
   with a plausible invention. */
.placeholder {
  border: var(--hairline) dashed var(--border-heavy);
  border-radius: var(--r-lg);
  padding: var(--space-6);
  color: var(--text-dim);
  text-align: center;
}

.placeholder__tag {
  display: block;
  font-size: var(--text-label);
  line-height: var(--lh-label);
  letter-spacing: var(--ls-label);
  font-weight: var(--fw-semibold);
  text-transform: uppercase;
  color: var(--text-dim);
}


/* ───────────────────────────────────────────────────────────────────────────
   18  BADGE AND CHECK LIST
   ─────────────────────────────────────────────────────────────────────────── */

/* The badge. Its only sanctioned use on the homepage is the pricing tier's
   "Most popular". Gold and amber are retired; this is emerald-soft. */
.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: 3px var(--space-3);
  border-radius: var(--r-pill);
  background: var(--badge-bg);
  color: var(--badge-text);
  font-size: var(--text-label);
  line-height: var(--lh-label);
  letter-spacing: var(--ls-label);
  font-weight: var(--fw-semibold);
  text-transform: uppercase;
  white-space: nowrap;
}

/* An outlined badge, for a neutral state label with no emerald attached. */
.badge--outline {
  background: transparent;
  color: var(--text-dim);
  border: var(--hairline) solid var(--border);
}

/* A checked list. The tick is one of the places emerald is allowed. The glyph
   is aria-hidden and the list semantics carry the meaning, so a screen reader
   hears the items and not a run of "check, check, check". */
.check-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-3);
}

.check-list > li {
  display: grid;
  grid-template-columns: 1.125em 1fr;
  gap: var(--space-3);
  align-items: start;
  color: var(--text-sub);
}

.check-list__icon {
  width: 1.125em;
  height: 1.125em;
  margin-block-start: 0.28em;   /* optical centring on the first line */
  flex: 0 0 auto;
  fill: none;
  stroke: var(--accent-text);
  stroke-width: 2.25;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* The "not included" row, for a feature a tier does not have. A minus in the
   non-text colour, never a red cross. */
.check-list--muted > li { color: var(--text-dim); }
.check-list--muted .check-list__icon { stroke: var(--text-nontext); }


/* ───────────────────────────────────────────────────────────────────────────
   19  ACCORDION (FAQ)

   Donor: faq1 (bundui) — the plainest FAQ in the catalogue: eyebrow, H2,
   lede, a bordered list and nothing else. Its Radix accordion is replaced by
   a hand-built one, because a portalled React control is not available on
   this stack and the ARIA is not hard to get right.

   NO-JS BEHAVIOUR, and it is the reason the CSS is shaped this way. The
   closed state is driven by [data-open="false"] on the item, which the markup
   authors directly — so there is no flash of every panel open on load. With
   JavaScript off, those panels would otherwise be unreachable, so
   partials/footer.php ships one <noscript> rule that forces them all open. A
   visitor without JS gets a plain, fully readable Q&A list.

   Keyboard, per the WAI-ARIA accordion pattern plus the brief's requirement:
     Enter / Space   toggle            (native — it is a real <button>)
     Tab             in and out        (native)
     ArrowDown/Up    previous/next trigger
     Home / End      first/last trigger
   Esc is deliberately NOT bound: an accordion is not a dialog, and swallowing
   Esc here would break the mobile sheet when the two are on screen together.

   Motion: 400ms (--dur-accordion), max-height plus a content fade, per the
   brand brief's motion table. Reduced motion collapses the duration to 1ms in
   §12, which still fires transitionend — which the JS depends on.
   ─────────────────────────────────────────────────────────────────────────── */

.accordion {
  border-block-start: var(--hairline) solid var(--border);
}

.accordion__item {
  border-block-end: var(--hairline) solid var(--border);
}

/* The heading wrapper carries no visual weight at all — it exists so the
   question is a real heading in the document outline. The button inside is
   what is styled. */
.accordion__heading {
  margin: 0;
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
}

.accordion__trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  width: 100%;
  min-height: 56px;
  padding: var(--space-4) 0;
  background: none;
  border: 0;
  border-radius: var(--r-xs);
  text-align: left;
  cursor: pointer;
  font-family: var(--font-head);
  font-weight: var(--fw-head);
  font-size: var(--text-h3);
  line-height: var(--lh-h3);
  letter-spacing: var(--ls-h3);
  color: var(--text);
  transition: color var(--dur-hover) var(--ease);
}

/* faq1 underlines the question on hover. A colour change is quieter and does
   not shift the baseline grid. */
.accordion__trigger:hover { color: var(--accent-text); }
.accordion__trigger:hover .accordion__icon { color: var(--accent-text); }

.accordion__icon {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  color: var(--text-nontext);
  transition: transform var(--dur-hover) var(--ease),
              color     var(--dur-hover) var(--ease);
}

.accordion__trigger[aria-expanded="true"] .accordion__icon {
  transform: rotate(180deg);
}

/* OPEN is the default state, so the no-JS fallback is one rule rather than a
   rewrite. visibility, not the hidden attribute: `hidden` cannot be
   transitioned, and visibility:hidden is what takes the collapsed content out
   of the tab order and out of the accessibility tree. */
.accordion__panel {
  overflow: hidden;
  max-height: none;
  opacity: 1;
  visibility: visible;
  transition: max-height var(--dur-accordion) var(--ease),
              opacity    var(--dur-accordion) var(--ease),
              visibility 0s;
}

.accordion__item[data-open="false"] > .accordion__panel {
  max-height: 0;
  opacity: 0;
  visibility: hidden;
  /* visibility flips only after the collapse has finished, or the content
     would vanish on frame one and the fade would be invisible. */
  transition: max-height var(--dur-accordion) var(--ease),
              opacity    var(--dur-accordion) var(--ease),
              visibility 0s var(--dur-accordion);
}

/* The padding lives on an inner element. Animating max-height on a box that
   also has padding makes the collapsed height equal to the padding, not zero,
   and the panel never fully closes. */
.accordion__body {
  padding-block: 0 var(--space-5);
  padding-inline-end: var(--space-6);
  color: var(--text-sub);
}

.accordion__body > * + * { margin-block-start: var(--space-4); }


/* ───────────────────────────────────────────────────────────────────────────
   20  NAV — SCROLL STATE, TOGGLE, MOBILE SHEET

   Donor: morphing-scroll-navbar (laziekiki) for exactly one thing — the
   scroll state. G1 §4.1 deletes the rest of it by name: the morph/compact
   stage (it hides the nav CTA on scroll-down, a conversion own-goal), the
   overshoot easing cubic-bezier(.34,1.32,.5,1), the box-shadow, the gradient
   progress bar and its hardcoded Geist Variable.

   The sheet has no donor. Every drawer in the 21st.dev catalogue checked for
   this task — coss.com/drawer, wensity/drawer, hero_ui/heroui-drawer,
   hyperiux/immersive-full-screen-nav — is a React portal library (Vaul, Radix
   or HeroUI) with swipe-to-dismiss and snap points. Portalled JS-state
   controls are the class of component G2 rejected, and swipe-to-dismiss is
   not in the motion spec. P2-T1's verdict that this is a ~50-line custom
   build stands, and it is built here.

   AT REST the nav is transparent, so the hero reads as one uninterrupted
   field. Past 24px of scroll it becomes rgba(11,17,41,.97) + blur(16px) over
   350ms (--dur-nav). That colour is not a literal here: the nav is
   data-theme="dark", so --surface is Navy Ink and 97% of it is the same
   value. The nav is also forced opaque whenever the sheet is open.
   ─────────────────────────────────────────────────────────────────────────── */

/* .nav--solid is for a page whose first band is light: a transparent nav
   with Cream links over Paper is unreadable, so such a page opts out of the
   at-rest state entirely ($nav['solid'] in partials/nav.php). Every page in
   the current plan opens on an Ink hero, so nothing sets it today — it exists
   so the first light-hero page cannot reintroduce the bug. */
.nav--scrolled .nav__bg,
.nav--solid .nav__bg,
.nav[data-nav-open="true"] .nav__bg {
  background: color-mix(in srgb, var(--surface) 97%, transparent);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border-block-end-color: var(--border-weak);
}

/* The burger. A real <button> with aria-expanded and aria-controls; hidden
   from 880px up, where the links are a row again. 44px square tap target. */
.nav__toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  margin-inline-end: calc(var(--space-3) * -1);  /* optical edge alignment */
  padding: 0;
  background: none;
  border: 0;
  border-radius: var(--r-sm);
  color: var(--text);
  cursor: pointer;
}

.nav__toggle-bars {
  position: relative;
  display: block;
  width: 18px;
  height: 12px;
}

/* Two bars, not three. They rotate into a cross when open — one transform
   each, no pseudo-element gymnastics, and nothing to get wrong at 0.5px. */
.nav__toggle-bars::before,
.nav__toggle-bars::after {
  content: "";
  position: absolute;
  left: 0;
  width: 100%;
  height: 2px;
  background: currentColor;
  border-radius: 2px;
  transition: transform var(--dur-hover) var(--ease);
}

.nav__toggle-bars::before { top: 0; }
.nav__toggle-bars::after  { bottom: 0; }

.nav__toggle[aria-expanded="true"] .nav__toggle-bars::before {
  transform: translateY(5px) rotate(45deg);
}

.nav__toggle[aria-expanded="true"] .nav__toggle-bars::after {
  transform: translateY(-5px) rotate(-45deg);
}

@media (min-width: 880px) {
  .nav__toggle { display: none; }
}

/* THE SHEET.
   Below 880px #nav-menu stops being a row and becomes a panel anchored under
   the bar. It is the SAME <ul> in both roles — the links exist once in the
   markup, so the two can never drift apart. */
@media (max-width: 879px) {
  .nav__links {
    position: fixed;
    inset: 64px 0 auto 0;       /* flush under the 64px bar */
    z-index: 60;
    display: flex;
    flex-direction: column;
    gap: 0;
    max-height: calc(100dvh - 64px);
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: var(--space-4) var(--gutter) var(--space-6);
    background: var(--surface-2);
    border-block-end: var(--hairline) solid var(--border);

    /* Closed. visibility is what removes it from the tab order; the
       translate is the motion. */
    visibility: hidden;
    opacity: 0;
    transform: translateY(-8px);
    transition: opacity    var(--dur-nav) var(--ease),
                transform  var(--dur-nav) var(--ease),
                visibility 0s var(--dur-nav);
  }

  .nav[data-nav-open="true"] .nav__links {
    visibility: visible;
    opacity: 1;
    transform: translateY(0);
    transition: opacity   var(--dur-nav) var(--ease),
                transform var(--dur-nav) var(--ease),
                visibility 0s;
  }

  .nav__links > li { border-block-end: var(--hairline) solid var(--border-weak); }
  .nav__links > li:last-child { border-block-end: 0; }

  .nav__links a:not(.btn) {
    display: flex;
    align-items: center;
    min-height: 52px;
    font-size: var(--text-lede);
  }

  /* The CTA repeated at the foot of the sheet. It is a separate element from
     the bar CTA because the bar CTA may be hidden on mobile once the sticky
     bottom bar exists (see nav.php's $nav['mobile_cta']). */
  .nav__sheet-cta { margin-block-start: var(--space-5); }
}

@media (min-width: 880px) {
  .nav__sheet-cta { display: none; }
}

/* The scrim. Dismisses on click; aria-hidden because the Esc key and the
   close affordance are the accessible paths. */
.nav__scrim {
  position: fixed;
  inset: 0;
  z-index: 55;
  background: color-mix(in srgb, var(--navy-ink) 60%, transparent);
  visibility: hidden;
  opacity: 0;
  transition: opacity    var(--dur-nav) var(--ease),
              visibility 0s var(--dur-nav);
  border: 0;
  padding: 0;
  cursor: default;
}

.nav[data-nav-open="true"] .nav__scrim {
  visibility: visible;
  opacity: 1;
  transition: opacity var(--dur-nav) var(--ease), visibility 0s;
}

@media (min-width: 880px) {
  .nav__scrim { display: none; }
}

/* Scroll lock. Set on <html> while the sheet is open. The sheet only exists
   below 880px, where scrollbars are overlay, so there is no gutter to
   compensate and no layout shift. */
.is-nav-open,
.is-nav-open body { overflow: hidden; }

/* The bar CTA, optionally desktop-only — G1 §5 wants no CTA in the collapsed
   mobile bar once the sticky bottom bar carries one. */
@media (max-width: 879px) {
  .nav__cta--desktop { display: none; }
}

/* NO-JS. The fallback is NOT a class here, deliberately. A class JavaScript
   adds cannot help (there is no JavaScript), and a class JavaScript REMOVES
   would render the unfolded mobile nav until the deferred script runs, which
   is a visible flash on a slow connection. partials/nav.php ships the rules
   inside a <noscript> instead: the burger and the scrim hide, and the links
   unfold into a static in-flow row. Zero bytes and zero flash when JS is on,
   a completely usable nav when it is off. */


/* ───────────────────────────────────────────────────────────────────────────
   21  PRICING CARD

   Donor: pricing-4 (olewandowski1). Deleted per G1 §4.1: the monthly/annual
   toggle and its useState (ByteRoc sells monthly retainers — a toggle invents
   an annual plan), the ring-2 featured treatment, the shadow-sm, and the
   IconPlaceholder runtime lucide lookup.

   G1 Q10(b): the $800 tier is a named fixed-scope one-time "Core Flows Build",
   not the bottom rung of a retainer ladder. The retainer ladder starts at
   $1,800. That is why .pricing__period exists as its own element — one tier
   says "one-time" and two say "/month", and the layout has to hold both
   without the baselines drifting.
   ─────────────────────────────────────────────────────────────────────────── */

.pricing {
  display: grid;
  gap: var(--space-5);
  grid-template-columns: 1fr;
  align-items: stretch;
}

@media (min-width: 880px) {
  .pricing { grid-template-columns: repeat(3, 1fr); }
}

.pricing__tier {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  background: var(--surface-card);
  border: var(--hairline) solid var(--border);
  border-radius: var(--r-lg);
  padding: var(--space-5);
}

/* A 1px emerald hairline — the brief says hairline emphasis, not a 2px ring
   and not a glow. */
.pricing__tier--featured { border-color: var(--accent-border); }

/* The badge row always occupies its line, featured or not, so the three
   tiers' names sit on one baseline instead of the flagged one dropping.
   A flex box rather than a plain block: as a block, the empty <p> resolved to
   its min-height while the flagged <p> resolved to the line box around an
   inline-flex badge, and the two differed by about five pixels — enough to
   see across three columns. As a flex container both are the same height and
   the badge is not on a baseline at all. */
.pricing__flag {
  display: flex;
  align-items: center;
  min-height: 26px;
}

.pricing__name {
  font-size: var(--text-h3);
  line-height: var(--lh-h3);
  letter-spacing: var(--ls-h3);
  font-weight: var(--fw-head);
  color: var(--text);
}

.pricing__price {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0;
}

.pricing__amount {
  font-family: var(--font-head);
  font-weight: var(--fw-head-bold);
  font-size: var(--text-figure);
  line-height: 1;
  letter-spacing: var(--ls-h2);
  color: var(--text);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* tabular-nums is why $800, $1,800 and $4,000 line up down the column. */
.pricing__period {
  font-size: var(--text-caption);
  color: var(--text-dim);
}

.pricing__summary { color: var(--text-sub); }

/* The feature list grows to fill, so every tier's CTA sits on one line
   regardless of how many features it lists. */
.pricing__features { flex: 1 1 auto; }

.pricing__foot { margin-block-start: auto; }

/* The published market range, under the three tiers — B's sharpest borrowed
   move, landed at §11 per G1 §4.2 #7. It is a stat, so it prints its sources. */
.pricing__market {
  margin-block-start: var(--space-6);
  padding-block-start: var(--space-5);
  border-block-start: var(--hairline) solid var(--border);
}


/* ───────────────────────────────────────────────────────────────────────────
   22  COMPARISON TABLE

   Donor: comparison-02 (hirael, MIT) — the most accessible component in the
   shortlist and the reason it survived re-check: a real <table> with an
   sr-only <caption>, scope="col" / scope="row", and an sr-only Yes/No behind
   every glyph. All of that is kept.

   THE PAGE BODY MUST NEVER SCROLL HORIZONTALLY. The table is 42rem wide at
   minimum and scrolls inside its own container. The container is also a
   keyboard-reachable region — WCAG 2.2 requires a scrollable area to be
   operable without a pointer, which a plain overflow div is not, so it takes
   role="region", an accessible name and tabindex="0".
   ─────────────────────────────────────────────────────────────────────────── */

.table-scroll {
  max-width: 100%;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  border: var(--hairline) solid var(--border);
  border-radius: var(--r-lg);

  /* contain: paint is here for a measurement reason, not a performance one.
     The page does not overflow: at a 375px viewport window.scrollX stays 0
     after scrollTo(600, 0) and document.body.scrollWidth is 375. But Chrome
     still reports document.documentElement.scrollWidth as 554, because the
     42rem table's scrollable overflow propagates up out of the nested scroll
     container even though none of it is reachable. Since
     documentElement.scrollWidth > clientWidth is the standard test for
     horizontal overflow, leaving that reporting 554 would mean every audit
     of this page opens with a false positive on the one section most likely
     to deserve a true one. Paint containment resolves it to 375, and it is
     semantically honest: nothing in this box is meant to paint outside it. */
  contain: paint;
}

.table-scroll:focus-visible { box-shadow: var(--focus-ring); }

/* A printed hint, because a scroll region with no visible affordance is a
   discoverability failure on touch. Hidden once there is room for the table. */
.table-scroll__hint {
  display: block;
  margin-block-end: var(--space-3);
  font-size: var(--text-caption);
  color: var(--text-dim);
}

@media (min-width: 720px) {
  .table-scroll__hint { display: none; }
}

.ctable {
  min-width: 42rem;
  width: 100%;
  border-collapse: collapse;
  text-align: left;
}

.ctable th,
.ctable td {
  padding: var(--space-4);
  border-block-end: var(--hairline) solid var(--border);
  vertical-align: top;
}

.ctable tbody tr:last-child th,
.ctable tbody tr:last-child td { border-block-end: 0; }

.ctable thead th {
  background: var(--surface-2);
  font-family: var(--font-head);
  font-weight: var(--fw-head);
  font-size: var(--text-body);
  letter-spacing: var(--ls-h3);
  color: var(--text);
  white-space: nowrap;
}

/* The row header column — the thing being compared. */
.ctable tbody th[scope="row"] {
  font-family: var(--font-body);
  font-weight: var(--fw-medium);
  color: var(--text);
  min-width: 12rem;
}

.ctable td { color: var(--text-sub); }

/* The featured column: an emerald hairline down both sides, not a fill.
   Drawn with inset shadows rather than borders on purpose — border-collapse
   resolves adjacent cells' borders against each other, and an emerald 1px
   edge can silently lose that contest to its neighbour's identical 1px edge.
   An inset shadow is not part of that resolution, so it always draws. */
.ctable .is-featured {
  background: var(--surface-card);
  box-shadow: inset var(--hairline) 0 0 0 var(--accent-border),
              inset calc(var(--hairline) * -1) 0 0 0 var(--accent-border);
}

.ctable thead .is-featured {
  box-shadow: inset var(--hairline) 0 0 0 var(--accent-border),
              inset calc(var(--hairline) * -1) 0 0 0 var(--accent-border),
              inset 0 var(--hairline) 0 0 var(--accent-border);
}

.ctable tbody tr:last-child .is-featured {
  box-shadow: inset var(--hairline) 0 0 0 var(--accent-border),
              inset calc(var(--hairline) * -1) 0 0 0 var(--accent-border),
              inset 0 calc(var(--hairline) * -1) 0 0 var(--accent-border);
}

/* Yes / No glyphs. The glyph is aria-hidden and a .visually-hidden word sits
   beside it, so the meaning never depends on recognising a tick. */
.ctable__mark {
  display: inline-block;
  width: 18px;
  height: 18px;
  fill: none;
  stroke-width: 2.25;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.ctable__mark--yes { stroke: var(--accent-text); }
.ctable__mark--no  { stroke: var(--text-nontext); }


/* ───────────────────────────────────────────────────────────────────────────
   23  FORM CONTROLS

   Donor: booking-2 (olewandowski1) for the two-column layout — reassurance
   <dl> left, form in a bordered card right. Its controls are NOT ported:
   booking-2's ESP field is a Radix Select, which renders a portalled button
   and holds its value in React state, so it submits NOTHING in a native POST.
   That specific failure is the reason Option A won at G2.

   EVERY CONTROL BELOW IS A NATIVE HTML ELEMENT. <input>, <select>, <textarea>,
   <button>. No portals, no JS-held state, no hidden mirror inputs. Each field
   carries name= (not just id=), so a plain
   <form method="post" action="/process.php"> delivers it with JavaScript off.
   P4-T5 wires them; this file only styles them.

   NOTE A GAP, AND IT NEEDS A DECISION. Option C defines no feedback hue —
   there is no error red and no success green that is not the accent. The
   error state below is therefore built without colour: a full-contrast
   border, a warning glyph and a 500-weight message. That satisfies WCAG
   1.4.1 (colour is not the only cue) and is how monochrome systems do it, but
   it is quieter than a red field and worth Moin deciding on deliberately.
   Recorded in docs/experts/P4-T2-component-notes.md §Open.
   ─────────────────────────────────────────────────────────────────────────── */

.form { display: grid; gap: var(--space-5); }

.field { display: grid; gap: var(--space-2); }

.field__label {
  font-size: var(--text-caption);
  font-weight: var(--fw-medium);
  color: var(--text);
}

/* The required marker is decorative; `required` on the control is what
   actually conveys it, so the asterisk is aria-hidden and a visually-hidden
   "(required)" goes in the label text where it matters. */
.field__req {
  color: var(--accent-text);
  margin-inline-start: 2px;
}

.field__help,
.field__error {
  font-size: var(--text-caption);
  line-height: 1.45;
  color: var(--text-dim);
}

.input,
.select,
.textarea {
  width: 100%;
  min-height: 48px;                 /* comfortable tap target */
  padding: var(--space-3) var(--space-4);
  background: var(--field-bg);
  color: var(--text);
  border: var(--hairline) solid var(--border);
  border-radius: var(--r-md);
  font-size: var(--text-body);
  line-height: 1.4;
  transition: border-color var(--dur-hover) var(--ease);
}

/* 16px on mobile, which is what stops iOS zooming the viewport when a field
   takes focus. --text-body is already 16px below 768px; this makes it
   explicit so a future change to the body scale cannot reintroduce the bug. */
@media (max-width: 767px) {
  .input, .select, .textarea { font-size: 1rem; }
}

.textarea {
  min-height: 120px;
  padding-block: var(--space-3);
  resize: vertical;
  line-height: var(--lh-body);
}

.input::placeholder,
.textarea::placeholder { color: var(--text-dim); opacity: 1; }

.input:hover,
.select:hover,
.textarea:hover { border-color: var(--border-heavy); }

/* No custom focus ring here — §9's global :focus-visible already draws it,
   and the 2px surface-coloured gap keeps it legible on the field surface. */

/* NATIVE <select>, with the arrow drawn in CSS rather than as a background
   SVG. A data-URI SVG would have to carry a hardcoded fill colour, which
   breaks the one rule this stylesheet exists to enforce — and it would not
   re-colour between themes. A ::after chevron in currentColor does both. */
.select-wrap { position: relative; display: block; }

.select {
  appearance: none;
  -webkit-appearance: none;
  padding-inline-end: var(--space-7);
  cursor: pointer;
}

.select-wrap::after {
  content: "";
  position: absolute;
  inset-inline-end: var(--space-4);
  top: 50%;
  width: 8px;
  height: 8px;
  margin-block-start: -6px;
  border-inline-end: 2px solid currentColor;
  border-block-end: 2px solid currentColor;
  transform: rotate(45deg);
  color: var(--text-nontext);
  pointer-events: none;   /* the click must reach the <select> underneath */
}

/* The browser styles the dropdown list itself; color-scheme in §5 is what
   makes that list dark on a dark page rather than a white flash. */

.checkbox-field {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-3);
  align-items: start;
}

/* A native checkbox, sized up and tinted with accent-color — the one-property
   way to brand a native control without replacing it. */
.checkbox {
  width: 20px;
  height: 20px;
  margin: 2px 0 0;
  accent-color: var(--accent);
  cursor: pointer;
}

.checkbox-field label {
  font-size: var(--text-caption);
  color: var(--text-sub);
  cursor: pointer;
}

/* ERROR STATE. js/byteroc-ui.js sets aria-invalid="true" on the control,
   fills the .field__error (which carries role="alert" and is named by the
   control's aria-describedby) and puts .field--error on the wrapper.

   D2: colour is ADDED here, not substituted. The 2px effective border (1px
   border + 1px inset ring), the glyph and the 500 weight were the whole cue
   before and they are all still here, so the state survives a monochrome
   display, a colour-blind reader and a forced-colours mode untouched. */
.field--error .input,
.field--error .select,
.field--error .textarea {
  border-color: var(--error);
  box-shadow: inset 0 0 0 1px var(--error);
}

/* The error state is also set on the CONTROL, not only on a .field wrapper.
   The audit form's controls sit in .field; the footer newsletter's input sits
   in a .newsletter__row and has no wrapper at all. aria-invalid is the one
   hook present in both cases, and it is set by the same code that writes the
   message, so the border can never disagree with the words. */
.input[aria-invalid="true"],
.select[aria-invalid="true"],
.textarea[aria-invalid="true"] {
  border-color: var(--error);
  box-shadow: inset 0 0 0 1px var(--error);
}

/* A populated error line. Styled on the element itself for the same reason,
   and :empty is what guarantees this only ever paints a line with something
   in it — so an authored-empty element costs no layout and shows no colour.
   An empty error line is not a state; it is present from the first paint only
   so the live region and the aria-describedby target exist before the message
   does. */
.field__error {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  color: var(--error);
  font-weight: var(--fw-medium);
}

.field__error:empty { display: none; }

/* Outside a .field there is no grid gap to separate it from the control. */
:not(.field) > .field__error { margin-block-start: var(--space-2); }

.field__error svg {
  flex: 0 0 auto;
  width: 1em;
  height: 1em;
  margin-block-start: 0.2em;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
}

/* NATIVE VALIDATION ERROR STATE — :user-invalid, never :invalid.
   :invalid matches a required field that is merely still empty, so it paints
   every field red before the visitor has typed a character. :user-invalid
   only matches after the visitor has interacted with the control (or tried to
   submit), which is the moment the message is actually true.

   The treatment is the same border + inset ring as .field--error above,
   deliberately: this stylesheet has one error look. Since D2 that look
   carries --error as well as the weight, and it still works with the colour
   taken away. */
.input:user-invalid,
.select:user-invalid,
.textarea:user-invalid {
  border-color: var(--error);
  box-shadow: inset 0 0 0 1px var(--error);
}

/* The help line under an invalid field carries the emphasis, since the
   browser's own bubble is transient and is gone on the next keystroke. The
   weight change is the non-colour cue and it is what SC 1.4.1 is satisfied
   by; --error is the colour on top of it. */
.field:has(.input:user-invalid) .field__help,
.field:has(.select:user-invalid) .field__help,
.field:has(.textarea:user-invalid) .field__help {
  color: var(--error);
  font-weight: var(--fw-medium);
}

/* THE AUDIT FORM'S SECOND STEP.
   Authored OPEN in the HTML — the wrapper carries no `hidden` attribute in
   the markup — and collapsed by js/byteroc-home.js. With no JavaScript, or
   with JavaScript that threw before it got there, nothing ever sets `hidden`
   and all four fields render: the no-JS path is the default state of the
   document rather than a fallback anyone has to remember to maintain.

   The JS also strips `required` from the controls it collapses, so this is
   never a display:none required field: that combination makes the browser
   refuse to submit AND refuse to report why ("not focusable"), which is a
   silent form failure of exactly the kind this page is being rebuilt to end.

   The wrapper inherits .form's own grid and gap, so wrapping three .field
   children in it is layout-neutral: expanded, the form is pixel-identical to
   the same four fields with no wrapper at all. */
.form__step-2 {
  display: grid;
  gap: var(--space-5);
}

/* [hidden] is already display:none in the UA sheet. This restates it because
   the rule above sets display:grid on the same element, and a later
   display wins over the UA's — without this line the "collapsed" step would
   stay fully visible. */
.form__step-2[hidden] { display: none; }

/* A two-up field row that collapses to one column on mobile. */
.field-row {
  display: grid;
  gap: var(--space-5);
  grid-template-columns: 1fr;
}

@media (min-width: 560px) {
  .field-row { grid-template-columns: 1fr 1fr; }
}

/* THE HONEYPOT. process.php reads website_hp; a filled value means a bot.
   Hidden from sight, from the tab order (tabindex="-1" in the markup) and
   from the accessibility tree (aria-hidden), and deliberately NOT
   display:none — some bots skip display:none fields on purpose.

   Clipped in place rather than parked at left:-9999px. The offset version was
   measured and did NOT overflow the page — content to the left of the origin
   is not scrollable in LTR — so this is not a bug fix. It is the same recipe
   .visually-hidden already uses, which means one hiding technique in this
   stylesheet instead of two, and no magic offset that a positioned ancestor
   or an RTL page could later turn into a real overflow. Clipping is no more
   detectable to a bot than an offset: either takes getComputedStyle to
   notice, and a form-filler that runs the cascade defeats both. */
.hp {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* The form's own reassurance list — booking-2's left column. A <dl>, because
   each item is a term and its explanation. */
.reassure {
  margin: 0;
  display: grid;
  gap: var(--space-5);
}

.reassure > div {
  display: grid;
  grid-template-columns: 1.125em 1fr;
  gap: var(--space-3) var(--space-3);
}

.reassure svg {
  grid-row: span 2;
  width: 1.125em;
  height: 1.125em;
  margin-block-start: 0.3em;
  fill: none;
  stroke: var(--accent-text);
  stroke-width: 2.25;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.reassure dt { font-weight: var(--fw-medium); color: var(--text); }
.reassure dd { margin: 0; color: var(--text-sub); font-size: var(--text-caption); }

/* The split layout: objections left, form right. One column until 880px —
   two 400px columns at 768 leaves neither enough room. */
.form-split {
  display: grid;
  gap: var(--space-7);
  grid-template-columns: 1fr;
  align-items: start;
}

@media (min-width: 880px) {
  .form-split { grid-template-columns: 1fr 1.1fr; gap: var(--space-7); }
}


/* ───────────────────────────────────────────────────────────────────────────
   24  FOOTER NEWSLETTER WIDGET

   Donor: newsletter-1 (olewandowski1) — email input, submit, consent line,
   zero decoration. Explicitly NOT footer-3, which G1 §4.1 rules out by name
   because it imports sonner to toast a fake "You're subscribed".

   Which matters, because that fake toast is live defect 2 (G1 §8): the
   homepage's subscribeNL() sets "Subscribed ✓", disables the input and sends
   the address nowhere. Addresses captured to date: zero.

   So the widget below is a real <form method="post" action="/process.php">
   with form_type=newsletter_signup and a real name= on the field. With no
   JavaScript at all it performs a genuine POST and lands on
   /thank-you.html — i.e. the honest behaviour is the DEFAULT, and any AJAX
   P4-T5 adds is an enhancement on top of something that already works.
   Styling and markup only here; P4-T2 wires nothing.
   ─────────────────────────────────────────────────────────────────────────── */

.newsletter { max-width: 34rem; }

.newsletter__label {
  display: block;
  font-size: var(--text-caption);
  font-weight: var(--fw-medium);
  color: var(--text);
  margin-block-end: var(--space-2);
}

.newsletter__row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.newsletter__row .input { flex: 1 1 14rem; min-width: 0; }
.newsletter__row .btn { flex: 0 0 auto; }

@media (max-width: 479px) {
  .newsletter__row .btn { width: 100%; }
}

.newsletter__note {
  margin-block-start: var(--space-3);
  font-size: var(--text-caption);
  color: var(--text-dim);
}

/* The result line. Empty and aria-live in the markup, so if P4-T5 does add a
   fetch the outcome is announced — and it is only ever written from a real
   server response. */
.newsletter__status:empty { display: none; }

.newsletter__status {
  margin-block-start: var(--space-3);
  font-size: var(--text-caption);
  font-weight: var(--fw-medium);
  color: var(--text);
}

.newsletter__status[data-state="ok"] { color: var(--accent-text); }

/* The footer gains a fourth block, so it needs its own column rule rather
   than relying on space-between with four children of unequal width. */
.footer__newsletter { flex: 1 1 18rem; }


/* ───────────────────────────────────────────────────────────────────────────
   25  REDUCED-MOTION OVERRIDES FOR THE KIT

   §12 collapses every duration token to 1ms, which handles the transitions.
   What it cannot handle is a transform that still RUNS, instantly. A 4px jump
   and a sliding panel are position changes, and a visitor who has asked for
   reduced motion should get neither — so the card lift and the sheet's slide
   are removed outright and the states resolve by colour and opacity alone.

   1ms rather than 0ms throughout, so transitionend still fires: the accordion
   and the sheet both wait on it.
   ─────────────────────────────────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  .card--interactive:hover,
  .card--interactive:focus-within { transform: none; }

  .btn:active { transform: none; }

  .nav__links,
  .nav[data-nav-open="true"] .nav__links { transform: none; }

  .accordion__trigger[aria-expanded="true"] .accordion__icon { transform: rotate(180deg); }

  .nav__toggle-bars::before,
  .nav__toggle-bars::after { transition: none; }
}


/* ═══════════════════════════════════════════════════════════════════════════
   P4-T3 — THE PAGE LAYER

   Sections 26-35 are the layouts the homepage needs that the kit does not
   provide, because they are page compositions rather than reusable
   components: the hero's two columns, the calculator, the flow grid, the
   audience router, the process list, §9's approval-gated panels, §12's
   operator band and the sticky mobile CTA bar.

   SAME RULES AS ABOVE. No hex literal, no second typeface, no layout grid in
   a style= attribute, every colour through a semantic token so a band still
   re-themes on one data-theme attribute.

   Contents
    26  Page scaffolding — anchor offset, band variants, marked placeholders
    27  §2  Hero
    28  §5  The gap calculator
    29  §6  Flow grid
    30  §7  Audience router
    31  §8  Process list
    32  §9  Inside the audit
    33  §12 Operator band, result block and the quote
    34  §14 The audit section
    35  Sticky mobile CTA bar
   ═══════════════════════════════════════════════════════════════════════════ */


/* ───────────────────────────────────────────────────────────────────────────
   26  PAGE SCAFFOLDING
   ─────────────────────────────────────────────────────────────────────────── */

/* The nav is sticky and 64px tall, so an unadjusted in-page anchor lands
   underneath it. scroll-padding-top is the one-line fix and it applies to
   every scroll the browser performs, including the back/forward restore and
   keyboard paging — unlike scroll-margin on each target. */
html { scroll-padding-top: 80px; }

/* The second surface value of whichever theme the band is in: Paper-2 on
   light, Navy Base on dark. One class covers both, which is what keeps §7 and
   §13 (light) and §9, §12 and §14 (dark) on the brief's rhythm without a
   second theming mechanism. */
.section--alt { background: var(--surface-2); }

/* 1040px — §14 narrows from the page's 1200px, and the narrowing IS the
   signal that the argument has ended and the ask has started. */
.wrap--mid { max-width: 1040px; }

/* Hairline-grid cells one surface step higher: §12's promise row sits on a
   Navy Base band, so its cells need Navy Raised to read as cells at all. */
.hgrid--elevated > * { background: var(--surface-raised); }

/* MARKED PLACEHOLDERS, INLINE.
   .placeholder in §17 is the box treatment for a whole empty slot. This is
   the sentence-level equivalent, for a claim that is written but not yet
   signed off — the 14-point scope, the 48-hour promise, the team size. It is
   deliberately conspicuous: a dashed edge and an uppercase tag. G1's standing
   rule is that a placeholder may ship empty and marked but never filled with
   a plausible invention, and an unconfirmed promise printed as fact is the
   same failure in a smaller space. */
.flag {
  display: inline-block;
  border: var(--hairline) dashed var(--border-heavy);
  border-radius: var(--r-xs);
  padding: 0 0.4em;
  font-size: var(--text-caption);
  font-weight: var(--fw-medium);
  color: var(--text-dim);
}

.flag-note {
  display: block;
  margin-block-start: var(--space-4);
  padding: var(--space-3) var(--space-4);
  border: var(--hairline) dashed var(--border-heavy);
  border-radius: var(--r-md);
  font-size: var(--text-caption);
  line-height: 1.45;
  color: var(--text-dim);
  text-wrap: pretty;
}

.flag-note strong {
  display: block;
  font-size: var(--text-label);
  line-height: var(--lh-label);
  letter-spacing: var(--ls-label);
  font-weight: var(--fw-semibold);
  text-transform: uppercase;
  margin-block-end: var(--space-1);
}

/* A centred single action under a band. */
.section__action {
  margin-block-start: var(--space-7);
  display: flex;
  justify-content: center;
}

/* The same action with its reassurance line under it rather than beside it.
   §9 is the one place on this page where a primary CTA carries the line and is
   not inside a card that already stacks them. */
.section__action--stacked {
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
}

/* The small uppercase reassurance line that follows a primary CTA. 12px, not
   the brief's 11px: departure D3 raises the small-text floor. */
.reassure-line {
  display: block;
  font-size: var(--text-label);
  line-height: var(--lh-label);
  letter-spacing: var(--ls-label);
  font-weight: var(--fw-semibold);
  text-transform: uppercase;
  color: var(--text-dim);
}


/* ───────────────────────────────────────────────────────────────────────────
   27  §2 HERO

   Donor: hero-24 (ln-dev7) for the text block — eyebrow, one tight H1, one
   subline, one CTA, zero decoration. The right-hand panel is custom.

   THE H1 IS THE LCP ELEMENT AND IT IS STATIC TEXT. Nothing in this section is
   opacity:0 at rest and nothing waits on a script. That is the whole reason
   Direction A's hero is a deliverables panel rather than a live instrument,
   and it is the bug class P1-T1 found on the live site, where the H1 carries
   an opacity:0 reveal class that a slow CDN can leave visible-then-hidden.
   P4-T4 may animate the panel and the sub; it must not gate the H1.

   The brief's three floating email cards are deleted — departure D5, approved
   at G1. There is no float here and nothing to guard against reduced motion.
   ─────────────────────────────────────────────────────────────────────────── */


/* ───────────────────────────────────────────────────────────────────────────
   FIX 2 and FIX 3 — ruled at G3 §3 (Q2, Q3), approved by Moin 2026-10-08.

   FIX 2. Every two-column grid on this page used `align-items: start` with no
   treatment for the shorter column, so each one left an open gap beside a
   bordered card. Measured at 1440 before the fix: hero 195px, §5 calculator
   207px under the sliders, §12 operator band 446px, §14 368px under the form
   card. An open gap beside a card reads as a mistake; a card's own trailing
   space reads as padding. So at the two-column breakpoint each grid gets
   `align-items: stretch`, and:
     - where the SHORT column holds a card, the card takes `height: 100%` so
       its border closes the shape  (hero panel, §14 form);
     - where it holds no card, there is no border to close, so the surplus is
       distributed instead of being left at the bottom  (§5, §12).
   Each of the four is annotated at its own rule below.

   FIX 3. #hero inherited .section--lg's 128px top padding, so under the 64px
   sticky nav the eyebrow did not start until y=192 and the primary CTA's
   bottom edge sat at 762px at 1440 and 742px at 1280 — cut off by the most
   common desktop window on earth. Halved, and scoped to the hero: the 128px
   rhythm is correct for the other eleven sections.
   ─────────────────────────────────────────────────────────────────────────── */
@media (min-width: 992px) {
  #hero { padding-block-start: 4rem; }
}

.hero__grid {
  display: grid;
  gap: var(--space-7);
  grid-template-columns: 1fr;
  align-items: start;
}

@media (min-width: 992px) {
  /* 7 / 5 of a 12-column grid, which is the wireframe's split. */
  .hero__grid {
    grid-template-columns: 7fr 5fr;
    gap: var(--space-7);
    align-items: stretch;           /* FIX 2 */
  }

  /* FIX 2 — the panel is the card in the short column, so its border closes
     the shape. height, not min-height: the panel must also SHRINK to the copy
     column, which is what makes this survive Q8 taking the flagged note out
     of it. A hand-tuned height would not.

     And it did: closing Q8 removed 185px of note from this panel, which left
     224px of trailing space inside the card at 1440 — nine times its own
     24px padding, and past the point where trailing space still reads as
     padding. So Fix 2's second clause applies here too and the surplus goes
     into the list, which is the one part of the panel that can take it: five
     deliverables, five separate promises, each of which is better for the
     air. A flex rule rather than a measured gap, so it keeps working if the
     copy column or the list changes again. */
  .hero__panel { height: 100%; display: flex; flex-direction: column; }

  .hero__panel .check-list {
    flex: 1 1 auto;
    justify-content: space-between;
  }
}

.hero__copy > * + * { margin-block-start: var(--space-5); }
.hero__copy .lede { max-width: 46ch; }

/* 24ch is a ceiling rather than a working constraint: at 1440 the 7-column
   well is already narrower than that, so the column governs the wrap. The
   wireframe's above-the-fold contract predicted two lines at 1440 for a
   39-character H1; that was an arithmetic error the creative director owns at
   G3 §3 (Q1), and at no size between 54px and 72px does this headline set on
   fewer than four lines in the hero's ~635px well. 52px is the largest size
   that sets it on three — see the clamp ceiling below. */
.hero__h1 {
  max-width: 24ch;

  /* A DELIBERATE DEVIATION AT BOTH ENDS, both ruled on by the lead.

     FLOOR 48 -> 42px. The brand brief gives the hero H1 as 48-72px and
     P4-T1's --text-h1 implements exactly that. But the wireframe's
     above-the-fold contract makes the mobile line budget a HARD constraint —
     four lines maximum at 375px. Measured: Syne 800 at 48px in a 335px
     measure sets this 39-character headline on SIX lines, 305px tall, which
     eats the 375x667 fold on its own and pushes the primary button below it.
     42px is the largest size at which it sets on four lines (178px, measured,
     not estimated).

     CEILING 72 -> 52px. FIX 1, ruled at G3 §3 (Q1) and approved by Moin on
     2026-10-08. The build kept 72px because no size between 54 and 72 reduces
     the line count; the lead's ruling is that four lines was never the real
     defect — the RAG was. At 1440 the four lines filled 86 / 64 / 90 / 73 % of
     the 635px measure, with "email is" alone on line 2 and "revenue." alone on
     line 4. 52px is the largest size that sets three lines in that well, and
     its rag descends 100 / 77 / 53 %. The brief's 72px was written for a
     full-bleed hero, not for seven of twelve columns; Linear, Vercel, Stripe
     and Ramp — the brief's own references — all run their hero H1 at 48-64px.

     Floor and slope are UNCHANGED, which is load-bearing: the clamp's
     preferred value (1.965rem + 2.8169vw) only reaches 52px at 730px wide, so
     every width below 730px renders byte-identically to before this fix.
     The B1 font work was calibrated at 360/375/390/412/414/428/480/540/600
     and measured CLS 0 at each; Lighthouse mobile runs at 412. None of it can
     be disturbed by a ceiling change.

     --text-h1 itself is untouched at both ends; every other h1 on the site is
     unaffected. Shortening the copy instead was tested: nothing under 32
     characters keeps both the benchmark and its attribution, and dropping
     either is the one thing this page may not do. */
  font-size: clamp(2.625rem, 1.965rem + 2.8169vw, 3.25rem);
}

/* The ghost CTA is desktop-only. Two full-width stacked buttons at 375px read
   as two primaries, and the sticky bottom bar is already carrying one. */
@media (max-width: 767px) {
  .hero__ghost { display: none; }
}

/* Keeps a figure and its unit on one line. The hero H1 broke "30–40%" across
   two lines at 1440, which turns the page's one number into two fragments. */
.nowrap { white-space: nowrap; }

.hero__panel { padding: var(--space-5); }

.hero__panel-title {
  font-size: var(--text-body);
  font-weight: var(--fw-semibold);
  color: var(--text);
  margin-block-end: var(--space-4);
}

/* Below 992px the panel follows the copy, which is also the order the
   wireframe's mobile stack asks for. No order: rules needed. */


/* ───────────────────────────────────────────────────────────────────────────
   28  §5 THE GAP CALCULATOR

   Donor: pricing-12 (diarmuradi) for the structure only — inputs left,
   bordered result panel right with one large figure, a line-item breakdown
   and a CTA. G1 §4.2 #4 deletes its per-character spring, its blur-in, its
   rounded-4xl radii, its elevated shadow, its FancyButton and — the binding
   one — its Radix slider thumb, which sets focus-visible:ring-0 and so
   removes its own focus ring. These are native range inputs: keyboard and
   screen-reader operable for free, and §9's focus ring applies.

   Every number in here is rendered server-side from one config array and
   mirrored to the script as JSON on the wrapper, so the default result is
   correct and readable with JavaScript off, and the 27% / 30-40% benchmark is
   a one-line change in one place that cannot drift between markup and script.
   ─────────────────────────────────────────────────────────────────────────── */

.calc {
  display: grid;
  gap: var(--space-6);

  /* minmax(0, 1fr), not 1fr. A 1fr track refuses to shrink below its
     content's min-content width, and the result card's 2x2 KPI grid has a
     min-content of about 250px plus two sets of padding — so at 375px the
     card grew past the viewport and the PAGE scrolled horizontally. Measured
     at 395px against a 375px client width before this line. */
  grid-template-columns: minmax(0, 1fr);
  align-items: start;
}

.calc__inputs,
.calc__result { min-width: 0; }

@media (min-width: 880px) {
  .calc {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: var(--space-7);
    align-items: stretch;           /* FIX 2 */
  }

  /* FIX 2 — the short column here is the three inputs, and they are not a
     card, so there is no border to close: the 207px surplus is distributed
     into the gaps between the three field groups instead. They are three
     separate questions, each with its own label and its own scale, so space
     between them is legitimate — and the last slider's baseline now lands
     with the result card's lower edge rather than 207px above it. The 32px
     margin stays as the floor; space-between only ever adds to it. */
  .calc__inputs {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
  }
}

@media (max-width: 559px) {
  /* One step less padding and one step less gap, so the two KPI columns still
     have room to hold a six-figure number without clipping. */
  .calc__result { padding: var(--space-5); }
  .calc__kpis   { gap: var(--space-4); }
}

.calc__inputs > * + * { margin-block-start: var(--space-6); }

.calc__field > * + * { margin-block-start: var(--space-2); }

.calc__label {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  font-size: var(--text-caption);
  font-weight: var(--fw-medium);
  color: var(--text);
}

/* The live value, mirrored into the label so a keyboard or screen-reader user
   reads the number instead of inferring it from a thumb position. */
.calc__value {
  font-family: var(--font-head);
  font-weight: var(--fw-head);
  font-size: var(--text-body);
  color: var(--accent-text);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* The $ prefix sits inside the field rather than in the label, so the number
   the visitor types reads as currency as they type it. */
.calc__money { position: relative; display: block; }

.calc__money::before {
  content: "$";
  position: absolute;
  inset-inline-start: var(--space-4);
  top: 50%;
  transform: translateY(-50%);
  color: var(--text-dim);
  pointer-events: none;
}

.calc__money .input {
  padding-inline-start: calc(var(--space-4) + 0.75em);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* THE RANGE INPUT. Visible track 4px, thumb 24px, and the control itself 44px
   tall so the hit area is a thumb-sized target on a phone while the track
   stays a hairline instrument. */
.range {
  -webkit-appearance: none;
  appearance: none;
  display: block;
  width: 100%;
  height: 44px;
  margin: 0;
  background: transparent;
  cursor: pointer;
}

.range::-webkit-slider-runnable-track {
  height: 4px;
  border-radius: 2px;
  background: var(--border);
}

.range::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 24px;
  height: 24px;
  margin-block-start: -10px;
  border: 0;
  border-radius: var(--r-pill);
  background: var(--accent);
}

.range::-moz-range-track {
  height: 4px;
  border-radius: 2px;
  background: var(--border);
}

.range::-moz-range-thumb {
  width: 24px;
  height: 24px;
  border: 0;
  border-radius: var(--r-pill);
  background: var(--accent);
}

/* Tick labels. Three cells: floor, the benchmark, ceiling. The middle one
   carries the printed benchmark, which is the only reason the slider is
   persuasive rather than decorative. */
.range__ticks {
  display: flex;
  justify-content: space-between;
  gap: var(--space-3);
  font-size: var(--text-caption);
  color: var(--text-dim);
}

.range__ticks > :nth-child(2) { text-align: center; }
.range__ticks > :last-child { text-align: end; }
.range__ticks b { font-weight: var(--fw-medium); color: var(--text-sub); }

.calc__result { padding: var(--space-6); }

.calc__result-label {
  font-size: var(--text-caption);
  font-weight: var(--fw-medium);
  color: var(--text-dim);
}

.calc__sentence {
  margin-block-start: var(--space-4);
  color: var(--text-sub);
  text-wrap: pretty;
}

.calc__sentence b {
  color: var(--text);
  font-weight: var(--fw-medium);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* THREE TILES, NOT FOUR. The ratio tile is deleted (P5-T0 §6) and nothing
   replaces it: "flows missing" is not computable from three revenue inputs,
   and the annual gap is already tile 3.

   Below 768 the two revenue figures still sit SIDE BY SIDE, because the
   pairing is the comparison and it must not stack; the annual figure, which
   is a conclusion rather than half of a pair, takes the full row under them.
   At 768 and up all three sit in one row. */
.calc__kpis {
  display: grid;
  /* minmax(0, 1fr), not 1fr, for the same reason .calc itself uses it: a 1fr
     track will not shrink below its content's min-content width, and
     "$4,680,000" in Syne is wider than a 360px viewport allows. The page
     scrolling sideways is a worse failure than a figure borrowing two pixels
     of a 16px gap. */
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-5);
  margin-block-start: var(--space-6);
  padding-block-start: var(--space-5);
  border-block-start: var(--hairline) solid var(--border-weak);
}

.calc__kpis > :last-child { grid-column: 1 / -1; }

/* 1200, not 768. From 880 the calculator itself splits into two columns, so
   the result card is only about half the wrap: three tracks there cannot hold
   a six-figure Syne figure and the PAGE scrolls horizontally — measured at
   1013px against a 992px client width. The card is wide enough for three only
   once the wrap has stopped growing. Same defect, same fix as .calc's own
   minmax(0, 1fr) note above. */
@media (min-width: 1200px) {
  .calc__kpis { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .calc__kpis > :last-child { grid-column: auto; }
}

.calc__kpi-figure {
  display: block;
  /* The figure is a <dd>, and the UA stylesheet gives every <dd> a
     `margin-inline-start: 40px`. Nothing here reset it, so each figure was
     indented 40px from its own <dt> label at every width, and at 375px the
     two side-by-side tiles are 97.5px wide — "$17,500" is 83px of Syne, so it
     started 40px in and painted 25.5px past the tile's right edge, over the
     gap and into its neighbour. No page scroll, because the tile is a grid
     item on a minmax(0,1fr) track, which is why it survived to P6. (P6-T3b.)
     margin-block too, since a <dd> inside a flow-root tile has no use for
     one and the label below supplies its own. */
  margin: 0;
  font-family: var(--font-head);
  font-weight: var(--fw-head);
  font-size: var(--text-h3);
  line-height: 1.1;
  letter-spacing: var(--ls-h3);
  color: var(--text);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

.calc__kpi--accent .calc__kpi-figure { color: var(--accent-text); }

.calc__kpi-label {
  display: block;
  margin-block-start: var(--space-2);
  font-size: var(--text-caption);
  line-height: 1.4;
  color: var(--text-dim);
}

/* The handoff. One field and one button at the exact moment the figure is on
   screen. It is NOT a form and it never POSTs — see the wireframe §B.4. */
.calc__handoff {
  margin-block-start: var(--space-6);
  padding-block-start: var(--space-5);
  border-block-start: var(--hairline) solid var(--border-weak);
}

.calc__handoff-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-block-start: var(--space-2);
}

.calc__handoff-row .input { flex: 1 1 12rem; min-width: 0; }
.calc__handoff-row .btn { flex: 0 0 auto; }

@media (max-width: 479px) {
  .calc__handoff-row .btn { width: 100%; }
}

/* The published formula and the honesty line. Not a footnote and not a
   tooltip: the section's credibility is the direction's credibility, so the
   arithmetic is printed. */
.calc__formula {
  margin-block-start: var(--space-5);
  font-size: var(--text-caption);
  line-height: 1.5;
  color: var(--text-dim);
}

.calc__formula code {
  font-family: var(--font-body);   /* no third typeface, ever */
  font-size: 1em;
  color: var(--text-sub);
  font-variant-numeric: tabular-nums;
}

/* The honest alternative to a negative gap. Authored hidden; the script swaps
   the two when target <= current. A clamped $0 gap with an unchanged sentence
   would be a worse answer than no answer. */
.calc__already { display: none; }
.calc[data-state="met"] .calc__gap { display: none; }
.calc[data-state="met"] .calc__already { display: block; }


/* ───────────────────────────────────────────────────────────────────────────
   29  §6 FLOW GRID

   Donor: feature-08 (hirael, MIT) — six cards on hairline rails with square
   bordered icon tiles. Deleted per G1 §4.1: the pointer-follow spotlight, the
   --warm-glow radial tint and shadow-xs. Added: a figure slot, which the
   component has none of and which is the whole difference between a service
   card and a proof card.
   ─────────────────────────────────────────────────────────────────────────── */

.flow > * + * { margin-block-start: var(--space-3); }

.flow__stat {
  margin-block-start: var(--space-4);
  padding-block-start: var(--space-4);
  border-block-start: var(--hairline) solid var(--border-weak);
}

/* The flow figures are a ranked set, so they are sized as a row of peers
   rather than as the one headline number a .stat defaults to. */
.flow__stat .stat__figure { font-size: var(--text-h3); }
.flow__stat .stat__claim { font-size: var(--text-caption); color: var(--text-dim); }


/* ───────────────────────────────────────────────────────────────────────────
   30  §7 AUDIENCE ROUTER

   Confirmed custom at G1 §4.3: the catalogue has no concept of a two-up
   audience router, so this is the .hgrid primitive with one <a> per cell and
   about thirty lines. team-2 (olewandowski1) is the structural donor —
   icon, name, badge, one line.
   ─────────────────────────────────────────────────────────────────────────── */

.router > * + * { margin-block-start: var(--space-3); }

.router__head {
  display: flex;
  flex-wrap: wrap;           /* the badge wraps under the H3 at 375px rather
                                than squeezing it */
  align-items: baseline;
  gap: var(--space-3);
}

.router__go {
  display: inline-block;
  margin-block-start: var(--space-4);
  font-weight: var(--fw-medium);
  color: var(--accent-text);
}

.hgrid > a:hover .router__go { text-decoration: underline; }


/* ───────────────────────────────────────────────────────────────────────────
   31  §8 PROCESS LIST

   Donor: how-it-works-2 (olewandowski1). It is a real <ol>, because the steps
   are ordered and a div grid would say they are not. The numeral goes in the
   tile (the component ships icons) and the connector rail stops at the last
   node rather than running off the end of the list.
   ─────────────────────────────────────────────────────────────────────────── */

.steps {
  list-style: none;
  margin: 0;
  padding: 0;
  counter-reset: step;
}

.steps > li {
  position: relative;
  counter-increment: step;
  display: grid;
  grid-template-columns: 40px 1fr;
  gap: var(--space-5);
  padding-block-end: var(--space-6);
}

.steps > li:last-child { padding-block-end: 0; }

/* The rail. An ::after on each item except the last, so it cannot overrun the
   final node — which is what a single full-height line behind the list does. */
.steps > li:not(:last-child)::after {
  content: "";
  position: absolute;
  inset-inline-start: 20px;
  top: 44px;
  bottom: 4px;
  width: var(--hairline);
  background: var(--border);
}

.steps__tile {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  background: var(--surface);
  border: var(--hairline) solid var(--border);
  border-radius: var(--r-sm);
  font-family: var(--font-head);
  font-weight: var(--fw-head);
  color: var(--accent-text);
  font-variant-numeric: tabular-nums;
}

.steps__body > * + * { margin-block-start: var(--space-3); }

@media (max-width: 559px) {
  .steps > li { grid-template-columns: 36px 1fr; gap: var(--space-4); }
  .steps__tile { width: 36px; height: 36px; }
  .steps > li:not(:last-child)::after { inset-inline-start: 18px; top: 40px; }
}


/* ───────────────────────────────────────────────────────────────────────────
   32  §9 INSIDE THE AUDIT

   Replaces the approval-gated crop boxes. Three panels, each a page of a real
   redacted flow audit beside the callout that argues it.

   IDENTICAL LEFT-TO-RIGHT ORDER IN EVERY ROW — no alternation. Alternating
   sides is decoration that costs scanning; one callout column lets the eye run
   straight down the argument. The two children are the same two children at
   every width, so the mobile reading order and the desktop grid are one order.

   DO NOT TINT, DIM OR OVERLAY THE IMAGES. White pages on Navy Ink need a frame
   or they glare, so they get one — Raised ground, 8px padding, a hairline.
   Reduced brightness would read as something being hidden, which is precisely
   the wrong signal in the one section whose claim is that nothing is.

   NO HOVER LIFT, AND THAT IS NOT AN OMISSION. §16's rule: a card that is not a
   link must not lift, or the page teaches that lift means nothing. With the
   lightbox out (P5-T2a bans it) the panel is not interactive on a fine pointer,
   so it does not move. The coarse-pointer affordance below is a plain link.
   ─────────────────────────────────────────────────────────────────────────── */

/* The honesty statement. .source gives it the 14px Cream Sub treatment; this
   adds the measure and lifts the first sentence to Cream. No icon, no badge,
   no border — it should read as a statement of fact, because it is one. */
.audit__honesty {
  max-width: 72ch;
  margin-block-start: var(--space-5);
}

.audit__honesty strong {
  color: var(--text);
  font-weight: var(--fw-medium);
}

/* The audit's own opening note, quoted as text rather than as type inside a
   screenshot: selectable, indexable and readable by a screen reader. */
.audit__pull {
  max-width: 64ch;
  margin-block-start: var(--space-7);
  font-size: var(--text-lede);
  line-height: var(--lh-lede);
  color: var(--text);
  text-wrap: pretty;
}

.audit-panels {
  margin-block-start: var(--space-7);
  display: grid;
  gap: 48px;
}

@media (min-width: 768px) { .audit-panels { gap: 56px; } }
@media (min-width: 1024px) { .audit-panels { gap: 64px; } }

.audit-panel {
  display: grid;
  gap: var(--space-5);
  grid-template-columns: minmax(0, 1fr);
}

/* 768–1023: still one column, but the callout is held to a reading measure
   rather than running the full width of the image above it. */
@media (min-width: 768px) {
  .audit-panel__callout { max-width: 60ch; }
}

/* 1024 up: image 58%, gap 8%, callout 34%, callout centred against the page. */
@media (min-width: 1024px) {
  .audit-panel {
    grid-template-columns: 58% 34%;
    column-gap: 8%;
    align-items: center;
  }
}

/* The document is rendered at 2x, so 766 CSS px is its native size. The cap
   stops the page ever being blown up past ~1:1, where the type would soften. */
.audit-panel__media { max-width: 760px; }

.audit-panel__num {
  margin: 0 0 var(--space-3);
  font-family: var(--font-head);
  font-weight: var(--fw-head-bold);
  font-size: var(--text-h3);
  line-height: 1;
  letter-spacing: var(--ls-h3);
  color: var(--accent-text);   /* the only emerald in the panel body */
  font-variant-numeric: tabular-nums;
}

.audit-panel__frame {
  padding: 8px;
  background: var(--surface-raised);
  border: var(--hairline) solid var(--border);
  border-radius: var(--r-lg);
}

/* Full brightness, no shadow, no glow, no gradient. */
.audit-panel__frame img { border-radius: var(--r-sm); }

/* Coarse pointers only. At 375px the document's body type is not readable and
   we do not pretend otherwise — but this is a link to the image file, not a
   lightbox: no overlay, no dialog, no script, and no extra bytes unless it is
   actually tapped. */
.audit-panel__zoom { display: none; }

@media (pointer: coarse) {
  .audit-panel__zoom {
    display: inline-block;
    margin-block-start: var(--space-3);
    font-size: var(--text-caption);
    color: var(--text-sub);
  }
}

.audit-panel__callout .card__title + .card__body { margin-block-start: var(--space-3); }

.audit__closing {
  max-width: 64ch;
  margin-block-start: var(--space-7);
  font-size: var(--text-lede);
  line-height: var(--lh-lede);
  color: var(--text);
  text-wrap: pretty;
}


/* ───────────────────────────────────────────────────────────────────────────
   33  §12 OPERATOR BAND, RESULT BLOCK AND THE QUOTE

   Custom per G1 §4.3, composed per P5-T0b §3. Two cards of equal height, then
   one quote in a single centred column.

   THERE IS NO PHOTO SLOT HERE AND THERE IS NO PLACEHOLDER FOR ONE. No avatar,
   no initials circle, no silhouette, no grey disc. The left column is a
   signature card: an emerald rule, a name, a role and a first-person claim. It
   is finished as it stands, because the photo may never arrive. If it ever
   does it goes above the rule at 96px, round, with explicit dimensions, and
   nothing else in the band changes.

   FIX 2's 446px gap is solved by composition rather than by centring. The old
   left column held a 120px photo and two lines of name against half a screen
   of empty navy, so the surplus was split above and below the pair. Both
   columns are now cards, align-items: stretch gives them one height, and the
   surplus no longer exists.
   ─────────────────────────────────────────────────────────────────────────── */

.operator {
  display: grid;
  gap: var(--space-6);
  grid-template-columns: 1fr;
  align-items: start;
}

@media (min-width: 880px) {
  .operator {
    grid-template-columns: 5fr 7fr;
    gap: var(--space-7);
    align-items: stretch;
  }
}

.signature { display: flex; flex-direction: column; }

.signature > * + * { margin-block-start: var(--space-4); }

/* 2px x 24px. The signature's mark — the only emerald in the card, and it is
   a rule rather than a glyph, so the non-numeric-emerald rule is not touched. */
.signature__rule {
  display: block;
  width: 2px;
  height: 24px;
  background: var(--accent);
  border-radius: var(--r-pill);
}

.operator__name { font-size: var(--text-h3); }

.operator__role {
  font-size: var(--text-caption);
  color: var(--text-dim);
}

/* The name and its role are one unit: the 16px card gap between every other
   pair would read as two separate lines. */
.operator__name + .operator__role { margin-block-start: var(--space-1); }

/* The result block. Its figure is the second-largest number on the page after
   the calculator's, and it does NOT count up: .stat__num carries no
   data-count-to, which is the hook js/byteroc-motion.js keys off. */
.result { display: flex; flex-direction: column; }

.result > * + * { margin-block-start: var(--space-4); }

.result__figure {
  margin: 0;
  font-size: clamp(2rem, 1.6479rem + 1.5023vw, 2.75rem);   /* 32 → 44 */
}

.eyebrow + .result__figure { margin-block-start: var(--space-3); }

.result__unit {
  margin-block-start: var(--space-2);
  font-size: var(--text-caption);
  color: var(--text-dim);
}

.result__claim { color: var(--text-sub); text-wrap: pretty; }

/* The one permitted emphasis inside the sentence, in Cream. Emerald on
   non-numeric headline words is a brand-brief ban and this is prose. */
.result__claim b {
  color: var(--text);
  font-weight: var(--fw-medium);
}

.quotes {
  display: grid;
  gap: var(--space-5);
  grid-template-columns: 1fr;
  margin-block-start: var(--space-7);
}

@media (min-width: 720px) {
  .quotes { grid-template-columns: 1fr 1fr; }
}

/* D6 — one voice, one column. With one quote a two-up grid leaves a visible
   hole in the right-hand half, which is the failure mode the donor pick was
   chosen to avoid: one item in a grid reads as broken, one item in a
   single-voice layout reads as deliberate. */
.quotes--one {
  grid-template-columns: minmax(0, 1fr);
  max-width: 68ch;
  margin-inline: auto;
}

@media (min-width: 720px) {
  .quotes--one { grid-template-columns: minmax(0, 1fr); }
}

.quote { margin: 0; }

.quote__text {
  margin: 0;
  font-size: var(--text-lede);
  line-height: var(--lh-lede);
  color: var(--text);
  text-wrap: pretty;
}

.quote__who {
  margin-block-start: var(--space-4);
  font-size: var(--text-caption);
  line-height: 1.45;
  color: var(--text-dim);
}

/* The section's closing source line. It needs real air under the attribution
   or the two read as one block. It is NOT held to the quote's column: 68ch of
   14px type is narrower than 68ch of 17px type, so matching the measure would
   indent it against the quote rather than align it. */
.quotes + .source { margin-block-start: var(--space-6); }


/* ───────────────────────────────────────────────────────────────────────────
   34  §14 THE AUDIT SECTION

   Donor: booking-2 (olewandowski1) via the kit's .form-split. The one thing
   this adds is the mobile order: the reassurance list moves BELOW the form at
   375px, because four reassurance rows above it push the first field off the
   screen, and the objection they answer has already been answered four times
   further up the page. On desktop the list is beside the form and costs
   nothing.
   ─────────────────────────────────────────────────────────────────────────── */

.audit-grid {
  display: grid;
  gap: var(--space-6);
  grid-template-columns: 1fr;
  grid-template-areas:
    "head"
    "form"
    "reassure";
  align-items: start;
}

@media (min-width: 880px) {
  /* The form spans both rows on the right; the heading and the reassurance
     list stack down the left. Two 400px columns at 768px leaves neither
     enough room, which is why the split starts at 880px. */
  .audit-grid {
    grid-template-columns: 1fr 1.1fr;
    gap: var(--space-7);
    grid-template-areas:
      "head form"
      "reassure form";
    align-items: stretch;           /* FIX 2 */
  }

  /* FIX 2 — the form card spans both rows but only filled 314 of the left
     stack's 682px, leaving 368px of open section below a bordered card at the
     page's final ask. It is a card in the short slot, so its own border
     closes the shape. */
  .audit__form { height: 100%; }
}

.audit__head     { grid-area: head; }
.audit__head > * + * { margin-block-start: var(--space-4); }
.audit__reassure { grid-area: reassure; }
.audit__form     { grid-area: form; padding: var(--space-6); }

.audit__privacy {
  font-size: var(--text-caption);
  line-height: 1.45;
  color: var(--text-dim);
}


/* ───────────────────────────────────────────────────────────────────────────
   35  STICKY MOBILE CTA BAR  (<=768px)

   Custom, ~25 lines, per G1 §5.

   IT IS INVISIBLE UNTIL A SCRIPT ASKS FOR IT, and that is the safe way round.
   The bar exists to keep one tap to the form on a long page; with no
   JavaScript there is nothing to evaluate its three hiding rules, and a bar
   that cannot hide itself would cover the footer and float over the form it
   points at. So the default state in the markup is hidden, and
   js/byteroc-home.js reveals it only while all three rules allow it:
     1 the hero's primary button has scrolled out of view
     2 §14 is not intersecting the viewport
     3 the nav sheet is not open
   ─────────────────────────────────────────────────────────────────────────── */

.sticky-cta {
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  z-index: 45;              /* under the nav (50) and its sheet (60) */
  padding: var(--space-2) var(--space-4);
  padding-block-end: calc(var(--space-2) + env(safe-area-inset-bottom, 0px));
  background: color-mix(in srgb, var(--surface-2) 94%, transparent);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-block-start: var(--hairline) solid var(--border);

  visibility: hidden;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity    200ms var(--ease),
              transform  200ms var(--ease),
              visibility 0s 200ms;
}

.sticky-cta[data-state="visible"] {
  visibility: visible;
  opacity: 1;
  transform: translateY(0);
  transition: opacity   200ms var(--ease),
              transform 200ms var(--ease),
              visibility 0s;
}

/* The footer's last row must never sit under the bar. */
.has-sticky-cta body { padding-block-end: calc(56px + env(safe-area-inset-bottom, 0px)); }

@media (min-width: 769px) {
  /* Not rendered at all from 769px up: the nav CTA covers it there, and both
     at once would be two primaries in one viewport. */
  .sticky-cta { display: none; }
  .has-sticky-cta body { padding-block-end: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .sticky-cta,
  .sticky-cta[data-state="visible"] { transform: none; }
}


/* ═══════════════════════════════════════════════════════════════════════════
   P5-T2/T3/T4 — THE THREE INNER PAGES

   Everything below is built for /ecommerce/, /coach/ and /pricing/. Nothing
   above this line changed: these are five new components plus four small
   modifiers on components that already existed, so the homepage's measured
   100/100/100/100 and CLS 0 cannot move.

   The order follows the pages: the priced absence list and the design strip
   are /ecommerce/, the launch timeline is /coach/, the score quiz is both
   avatar pages, and the last block is /pricing/'s table groups, sticky
   feature column and project cards.
   ═══════════════════════════════════════════════════════════════════════════ */


/* ───────────────────────────────────────────────────────────────────────────
   36  THE PRICED ABSENCE LIST  (/ecommerce/ §5)

   The replacement for both the legacy "visual gap analysis" and the legacy
   calculator: six flows, each carrying the Klaviyo revenue-per-recipient
   figure that is published for it.

   THE FIGURE SITS ON THE SAME ROW AS THE FLOW IT BELONGS TO, at every width.
   The wireframe is explicit that it must not drop to a second line, because a
   figure on its own line stops being an attribute of the flow and starts
   reading as a running total. auto/1fr/auto holds all three columns down to
   320px: "$9.14" is 44px of tabular DM Sans and the glyph is 18px.

   THE SIXTH ROW HAS NO FIGURE AND THAT ASYMMETRY IS THE POINT. Post-purchase
   has no published per-recipient number in Klaviyo's set, so it prints the
   words instead — the same honesty signal as .stat--nonumber on the homepage,
   and it must not be "fixed" by borrowing a figure from a neighbouring flow.
   ─────────────────────────────────────────────────────────────────────────── */

.plist {
  list-style: none;
  margin: 0;
  padding: 0;
}

.plist > li {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: var(--space-3);
  align-items: baseline;
  padding-block: var(--space-3);
}

.plist > li + li { border-block-start: var(--hairline) solid var(--border-weak); }

/* The glyph is the check-list's, so the two columns of §5 share one visual
   language. aria-hidden on the SVG; the row's text carries the meaning. */
.plist .check-list__icon {
  width: 18px;
  height: 18px;
  flex: none;
  color: var(--accent-text);
  transform: translateY(3px);
}

.plist__name { color: var(--text); }

.plist__fig {
  font-family: var(--font-head);
  font-weight: var(--fw-head);
  font-size: var(--text-body);
  color: var(--accent-text);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The one row with no figure. Caption size, dim, and it wraps — it is a
   sentence, not a number, so it must not be forced onto one line. */
.plist__fig--none {
  font-family: var(--font-body);
  font-weight: var(--fw-regular);
  font-size: var(--text-caption);
  color: var(--text-dim);
  white-space: normal;
  text-align: end;
  max-width: 11ch;
}

/* The honesty line under the whole block — what revenue per recipient is and
   what it is not. Held to a reading measure and NOT truncated at any width. */
.plist-note {
  margin-block-start: var(--space-5);
  max-width: 68ch;
}


/* ───────────────────────────────────────────────────────────────────────────
   37  THE DESIGN STRIP  (/ecommerce/ §7)

   Two client email designs, named brands, per P5-T0b ruling 2. Two panels
   side by side read as a selection; four read as everything we have.

   WHY THE FRAME IS CAPPED AT 420px AND NOT LET RUN FULL-WIDTH. The crops are
   1000 x ~2000 — a 1:2 portrait. At the 575px a half-wrap would give them
   they stand 1,150px tall each, which turns a two-panel proof strip into two
   full screens of scrolling. Capped and centred in its column, each panel is
   ~820px tall at the widest, and the image is never displayed above its
   1000px intrinsic width, so it is never upscaled.

   NO LIGHTBOX, per P5-T2a. The crop is the proof; a 2,000px overlay is a
   second performance problem. And no hover lift: §16's rule is that a card
   which is not a link must not move.
   ─────────────────────────────────────────────────────────────────────────── */

.designs {
  display: grid;
  gap: var(--space-6);
  grid-template-columns: minmax(0, 1fr);
  margin-block-start: var(--space-7);
}

@media (min-width: 768px) {
  .designs { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-7); }
}

.design { margin: 0; }

.design__frame {
  max-width: 420px;
  margin-inline: auto;
  padding: 8px;
  background: var(--surface-raised);
  border: var(--hairline) solid var(--border);
  border-radius: var(--r-lg);
}

/* Full brightness, no tint, no gradient — the same rule as §9's audit pages.
   A dimmed sample reads as something being hidden. */
.design__frame img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--r-sm);
}

/* P5-T7 §5.7 — THE TWO-UP PAIR SHARES ONE BOX.
   The two crops have unnormalised source ratios (1000x1969 and 1000x2064), so
   in a two-up grid the shorter card ended ~38px above the taller one and the
   two captions sat on different baselines — in the one section whose job is to
   demonstrate design competence.

   The box is the TALLER ratio, so `cover` scales the SHORTER image up and the
   overflow is HORIZONTAL, not vertical. That matters: cropping to the shorter
   ratio instead would have taken 95px off the bottom of the DrinkOver crop,
   which has only 46px of dead space below "Get a good night of sleep" — it
   would have clipped the last line of a client's email. Horizontally it costs
   23px per side off Green Philosophy, measured against 40px and 39px of flat
   gutter, so the crop stays entirely inside blank margin and no content is
   lost. DrinkOver is 1000x2064 exactly, so for that image this is a no-op.

   The img keeps its width/height attributes describing the FILE; this box is
   the layout, applies from the render-blocking stylesheet, and holds CLS at 0.
   Scoped off .design--wide, whose landscape card is alone in its column. */
.designs .design:not(.design--wide) .design__frame img {
  aspect-ratio: 1000 / 2064;
  object-fit: cover;
}

/* Brand + what was built + designed and built by ByteRoc. No metric, no
   result, no implied outcome: this strip answers "will it look like my brand"
   and nothing else. */
.design__cap {
  max-width: 420px;
  margin: var(--space-4) auto 0;
  font-size: var(--text-caption);
  line-height: 1.45;
  color: var(--text-dim);
  text-align: center;
}

/* /coach/ §6b — one LANDSCAPE card in the .operator right-hand column.
   The 420px cap above is sized for the portrait, phone-shaped crops that sit
   two-up on /ecommerce/; a landscape sequence module in a 7fr column would be
   shrunk to a third of the space it is given. The modifier releases the cap and
   lets the column set the width. The figure still carries width/height, so the
   box is reserved before the bytes land and CLS stays 0. */
.design--wide .design__frame,
.design--wide .design__cap { max-width: none; }

/* Captions align to the card's own text edge once the card is column-width;
   centring reads as a caption for the column, not for the image. */
.design--wide .design__cap { text-align: start; }


/* ───────────────────────────────────────────────────────────────────────────
   38  THE LAUNCH TIMELINE  (/coach/ §5)

   Built on §31's .steps rather than as a new component, which is what makes
   the hardest reflow on the page free: .steps is already a vertical rail with
   a left marker column, so the two 14-day columns stack at 375px as two
   vertical timelines and the "Nothing" rows stay visible. The wireframe is
   explicit that they must — they are the content, they may not collapse
   behind a disclosure, and the timeline may not become a horizontal scroller,
   because a horizontally scrolled proof element is one nobody sees.

   Two modifiers, nothing else:
   ─────────────────────────────────────────────────────────────────────────── */

/* A day marker rather than a step number. "Day -7" does not fit a 40px tile,
   so the tile carries the signed day and the body says what it is. Caption
   size because two characters of 20px Syne in a 40px box has no air. */
.steps__tile--day {
  font-size: var(--text-caption);
  letter-spacing: 0;
}

/* The silence rows. The marker loses its emerald: emerald is the accent for
   what EXISTS, and six emerald tiles marking six empty days would read as six
   emails.

   --text-dim, NOT --text-nontext. The non-text token is #5A648C on dark, which
   §2 labels "NON-TEXT only (icons, rules)" for a reason — it is 3.08:1 on Navy
   Ink, and the day marker is 14px type, not a rule. axe caught it at all four
   breakpoints. --text-dim is 5.49:1 on Ink and still reads as recessive
   against the emerald markers beside it, which is the whole job. */
.steps__tile--muted {
  color: var(--text-dim);
  border-color: var(--border-weak);
}

/* The silence row's own title. Dim, and deliberately not italic or
   struck-through — it is a statement of what happened, not an error state. */
.steps__nothing {
  font-family: var(--font-body);
  font-size: var(--text-body);
  font-weight: var(--fw-regular);
  color: var(--text-dim);
}

/* The closing "what happens" line needs real air under the last timeline row,
   or it reads as a note attached to the final email rather than to the whole
   column. */
.steps + .source { display: block; margin-block-start: var(--space-5); }

/* The column heading above each timeline, inside a hairline cell. */
.timeline__head {
  margin-block-end: var(--space-5);
  padding-block-end: var(--space-4);
  border-block-end: var(--hairline) solid var(--border);
}

.timeline__head .card__title + p {
  margin-block-start: var(--space-2);
  font-size: var(--text-caption);
  color: var(--text-dim);
}


/* ───────────────────────────────────────────────────────────────────────────
   39  THE SCORE QUIZ  (/ecommerce/ §9 and /coach/ §9)

   Five questions, then a score, then the form. The ladder the wireframe keeps
   because it is the page's micro-commitment mechanism.

   THREE THINGS ABOUT ITS NO-JAVASCRIPT BEHAVIOUR, BECAUSE THEY ARE THE WHOLE
   DESIGN:

   1  THE FORM IS NEVER GATED BY THE QUIZ. The form is a sibling of this
      component, always rendered, always submittable. The score is an output
      the quiz adds; it is never a door the visitor has to walk through. A
      JS-gated form at the one ask on the page is the defect class this
      project keeps retiring.

   2  STEPS 2-5 ARE AUTHORED hidden AND THE NO-SCRIPT RULE UNHIDES THEM — the
      accordion's pattern, for the accordion's reason. Authoring them open and
      letting the deferred script collapse them would move layout after paint;
      authoring them hidden with no fallback would make four questions
      unreachable. This way the script changes no geometry at init, so the
      component cannot contribute a layout shift.

   3  THE NAV AND THE PROGRESS COUNTER ARE AUTHORED hidden TOO. With no
      JavaScript there is nothing for Back and Next to do, and a dead control
      is worse than an absent one. The five questions then read as a plain
      self-check list, which is what the .quiz__nojs line says they are.

   The options are real radios in a real fieldset — outside the <form>, so
   they POST nothing. process.php has no key for a score and none is invented.
   ─────────────────────────────────────────────────────────────────────────── */

.quiz > * + * { margin-block-start: var(--space-5); }

.quiz__progress {
  margin: 0;
  font-size: var(--text-label);
  line-height: var(--lh-label);
  letter-spacing: var(--ls-label);
  font-weight: var(--fw-semibold);
  text-transform: uppercase;
  color: var(--text-dim);
}

.quiz__steps {
  list-style: none;
  margin: 0;
  padding: 0;
}

.quiz__step + .quiz__step { margin-block-start: var(--space-7); }

/* A <fieldset> with its chrome removed. min-width: 0 is required, not
   cosmetic: a fieldset has an intrinsic min-content width that ignores the
   grid it sits in, and without this the card overflows at 375px. */
.quiz__fs {
  margin: 0;
  padding: 0;
  border: 0;
  min-width: 0;
}

/* A <legend>, not a heading — the question is a form label, and five h3s here
   would put five rows into the document outline that are not sections. */
.quiz__legend {
  padding: 0;
  font-size: var(--text-lede);
  line-height: 1.4;
  font-weight: var(--fw-medium);
  color: var(--text);
  text-wrap: pretty;
}

.quiz__stem {
  margin-block-start: var(--space-3);
  font-size: var(--text-caption);
  line-height: 1.5;
  color: var(--text-dim);
  text-wrap: pretty;
}

.quiz__opts {
  display: grid;
  gap: var(--space-2);
  margin-block-start: var(--space-4);
}

/* The whole row is the label, so the hit area is the row and not the word.
   44px minimum, which is the touch target the wireframe asks for. */
.quiz__opt {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--space-3);
  align-items: center;
  min-height: 44px;
  padding: var(--space-3) var(--space-4);
  background: var(--field-bg);
  border: var(--hairline) solid var(--border);
  border-radius: var(--r-md);
  color: var(--text);
  cursor: pointer;
  transition: border-color var(--dur-hover) var(--ease);
}

.quiz__opt:hover { border-color: var(--border-heavy); }

/* Selected and keyboard states are both drawn on the row. :has() gives the
   checked state without a class the script would have to keep in sync, and
   :focus-within means a radio reached by arrow key shows the same row. */
.quiz__opt:has(input:checked) { border-color: var(--accent-border); }
.quiz__opt:focus-within { border-color: var(--border-heavy); }

/* A native radio, sized up and tinted — the same one-property approach as
   .checkbox, and for the same reason: never replace a native control that
   carries keyboard behaviour you would have to rebuild. */
.radio {
  width: 20px;
  height: 20px;
  margin: 0;
  accent-color: var(--accent);
  cursor: pointer;
  flex: none;
}

.quiz__nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: center;
}

/* [hidden] HAS TO BE RESTATED HERE AND THAT IS NOT BELT-AND-BRACES. The UA
   sheet's `[hidden] { display: none }` is an author-level loss to ANY author
   rule that sets display — so `display: flex` above silently un-hid the
   Back/Next row with JavaScript off, which is exactly the dead-control state
   the no-JS design exists to avoid. Caught by the JS-disabled run; the rule
   below is the fix, and the same trap applies to every component that pairs a
   display declaration with the hidden attribute. */
.quiz__nav[hidden] { display: none; }

/* Next sits at the end on its own, so Back can never be hit by a thumb
   reaching for it. */
.quiz__nav [data-quiz-next] { margin-inline-start: auto; }

@media (max-width: 479px) {
  .quiz__nav [data-quiz-next] { margin-inline-start: 0; }
}

.quiz__result {
  padding-block-start: var(--space-5);
  border-block-start: var(--hairline) solid var(--border);
}

.quiz__result > * + * { margin-block-start: var(--space-4); }

.quiz__score {
  margin: 0;
  font-family: var(--font-head);
  font-weight: var(--fw-head-bold);
  font-size: var(--text-figure);
  line-height: 1;
  letter-spacing: var(--ls-h2);
  color: var(--accent-text);
  font-variant-numeric: tabular-nums;
}

.quiz__score span {
  font-size: var(--text-body);
  font-weight: var(--fw-medium);
  color: var(--text-dim);
}

.quiz__band {
  font-size: var(--text-lede);
  font-weight: var(--fw-medium);
  color: var(--text);
}

/* The heading is focused when the result appears, so it must not draw a focus
   ring — it is a destination, not a control. */
.quiz__band:focus { outline: none; }

/* The form is the next thing after the quiz inside the same card, and the two
   need a rule between them or the ladder reads as one long form whose first
   five rows do not submit. */
.quiz + .form {
  margin-block-start: var(--space-6);
  padding-block-start: var(--space-6);
  border-block-start: var(--hairline) solid var(--border);
}

/* Shown only by the <noscript> rule on the page. */
.quiz__nojs { display: none; }


/* ───────────────────────────────────────────────────────────────────────────
   40  /pricing/ — TABLE GROUPS, THE STICKY FEATURE COLUMN, PROJECT CARDS

   §22 built the comparison table for the homepage's seven-row, four-column
   shape. /pricing/'s is four row groups deep and 24 rows long, which needs
   two things §22 did not: a group heading row, and a feature column that
   stays put while the plans scroll past it.
   ─────────────────────────────────────────────────────────────────────────── */

/* A row-group heading. A real <th scope="colgroup"> spanning the table, so
   the grouping is in the semantics and not only in the paint. */
.ctable__group th {
  padding-block: var(--space-3);
  background: var(--surface-2);
  font-family: var(--font-body);
  font-size: var(--text-label);
  line-height: var(--lh-label);
  letter-spacing: var(--ls-label);
  font-weight: var(--fw-semibold);
  text-transform: uppercase;
  color: var(--text-dim);
}

/* THE STICKY FEATURE COLUMN — OPT-IN, via .ctable--sticky. Without it the row
   label scrolls away and the glyphs in view belong to nothing, which is a real
   problem for a 24-row table on a phone. It is a MODIFIER rather than a change
   to .ctable because the homepage's §10 table is built, measured and signed
   off at G3, and silently altering how it scrolls is not this task's to make.

   The sticky cell needs an opaque background of its own: it paints over the
   cells it passes, and a transparent one would stack two rows of type on each
   other. The 1px right edge is drawn with a box-shadow rather than a border —
   border-collapse resolves adjacent cells' borders against each other, and
   this cell's right border would be contesting its neighbour's left one. */
.ctable--sticky thead td:first-child,
.ctable--sticky tbody th[scope="row"] {
  position: sticky;
  inset-inline-start: 0;
  z-index: 2;
  background: var(--surface);
  box-shadow: var(--hairline) 0 0 0 var(--border);
}

.ctable--sticky thead td:first-child,
.ctable__group th { background: var(--surface-2); }

/* The group heading spans the whole table, so it must not be sticky — it has
   nothing to the right of it to hold a position against. */
.ctable--sticky .ctable__group th {
  position: static;
  box-shadow: none;
}

/* A scope boundary, not a deficiency. The wireframe is explicit: the one-time
   build's Ongoing cells read "Not included — one-time build" in words,
   because a cross glyph reads as something missing rather than as something
   that was never in scope. */
.ctable__na {
  font-size: var(--text-caption);
  color: var(--text-dim);
}

/* THE SERVICES CHECKBOX GROUP — §7's user_services[].
   A real <fieldset> with its chrome removed, so the six boxes are one named
   group rather than six unrelated controls. min-width: 0 is required, not
   cosmetic: a fieldset has an intrinsic min-content width that ignores the
   grid it sits in, and without it the card overflows at 375px. */
.services {
  margin: 0;
  padding: 0;
  border: 0;
  min-width: 0;
}

.services__opts {
  display: grid;
  gap: var(--space-2);
  margin-block-start: var(--space-3);
}

/* Two up once there is room. Six single-file checkboxes is a long scroll in
   the middle of a form that still has a textarea to come. */
@media (min-width: 560px) {
  .services__opts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* 44px minimum hit area per box, and the label is the hit area — the row, not
   the word. */
.services__opts .checkbox-field {
  align-items: center;
  min-height: 44px;
}

.services__opts .checkbox-field label { font-size: var(--text-body); color: var(--text); }

/* The one-off project card's price line, in the card header so it is visible
   without scrolling the card. */
.project__price {
  margin-block-start: var(--space-2);
  font-family: var(--font-head);
  font-weight: var(--fw-head);
  font-size: var(--text-body);
  color: var(--accent-text);
  font-variant-numeric: tabular-nums;
}

.project__price + .card__body { margin-block-start: var(--space-3); }


/* ───────────────────────────────────────────────────────────────────────────
   40  THE FOURTEEN CHECKS  (/ and /ecommerce/ and /need.html)

   WHY THIS EXISTS. The site has claimed a "14-point audit" since launch while
   publishing a ten-item list, so the page contradicted its own headline
   number. G4_PREP decision 1 resolves it the right way round: the fourteen
   checks are real — they are §4 of Moin's own Shopify-Email-Audit template —
   so the claim becomes TRUE rather than being talked down. This is the
   component that prints them.

   IT IS DELIBERATELY NOT .check-list. The two lists now sit next to each
   other and they do two different jobs: these fourteen are WHAT WE CHECK, and
   the .check-list beside them is WHAT YOU GET BACK. Giving them the same tick
   glyph and the same rhythm would read as one list of twenty-four items, which
   is the confusion this change exists to end. So: numerals, not ticks — the
   numeral is the whole point, because it is the number in the claim — denser
   rows, and two columns as soon as there is room for them.

   The numerals are authored in the markup rather than generated with
   `counter-increment`, for one reason: a CSS counter lives in a ::before and
   so is invisible to a text-only reader, and "point 13 of 14" is exactly the
   detail a prospect quotes back. They are also aria-hidden, because <ol>
   already conveys position to a screen reader and announcing both says the
   number twice.
   ─────────────────────────────────────────────────────────────────────────── */
.checks {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-2) var(--space-6);
}

/* Two columns from 560px. Fourteen single-file rows is a long scroll for a
   list whose job is to be scanned, not read. It never goes to three: the
   longest row ("Authentication: SPF / DKIM / DMARC") wraps at a third of
   1200px, and a list with one wrapped row in it stops looking countable. */
@media (min-width: 560px) {
  .checks { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.checks > li {
  display: grid;
  grid-template-columns: 1.9em 1fr;
  gap: var(--space-3);
  align-items: baseline;
  padding-block: var(--space-1);
  color: var(--text-sub);
}

.checks__n {
  font-family: var(--font-head);
  font-weight: var(--fw-head);
  font-size: var(--text-caption);
  font-variant-numeric: tabular-nums;
  color: var(--accent-text);
  text-align: end;
}

/* The two list headings, when the checks and the deliverables stand together.
   .eyebrow sets the type; this only sets the space, so the two blocks read as
   two blocks. */
.checks-head {
  margin-block-end: var(--space-4);
}

.checks + .checks-head,
.check-list + .checks-head {
  margin-block-start: var(--space-6);
  padding-block-start: var(--space-5);
  border-block-start: var(--hairline) solid var(--border-weak);
}
