/* ============================================================================
   FLOW — WEB DESIGN SYSTEM
   Marketing + Information + Support site
   Extends the in-app "Glow System" and public viewers to a full website.

   Two surface modes share ONE token + component vocabulary:
     • DARK   (default)            — marketing, product, download, community
     • LIGHT  [data-theme="light"] — help center, docs, long-form support

   Accent semantics (lime / charge-pink / cyan / live-red / SOC bands) are
   CONSTANT across modes; only surface, ink, and border tokens swap.
   Load order:  this file (fonts self-hosted below)  →  page-specific <style>
   (rare). Plan 4 T8 removed the third-party fonts.googleapis.com/fonts.
   gstatic.com hop — see the @font-face block below.
   ============================================================================ */

/* Self-hosted from fonts.gstatic.com — byte-identical to what Google serves
   macOS/Linux Chrome (the unhinted build; Windows otherwise receives a
   hinting-only variant with identical outlines and metrics).
   See tools/fetch-fonts.mjs for the URLs and their pinned SHA-256.
   Inter and Space Grotesk are variable — one file each carries every weight —
   so the per-weight blocks below all point at the same file, exactly as
   Google's own stylesheet does. font-display: swap is REQUIRED: it exists
   today only because the @import URL asked for it, and losing it turns a swap
   into a blank-text period that makes FCP worse. */
@font-face {
  font-family: 'Inter'; font-style: normal; font-weight: 400; font-display: swap;
  src: url('/assets/fonts/inter-latin.woff2') format('woff2');
  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: 'Inter'; font-style: normal; font-weight: 500; font-display: swap;
  src: url('/assets/fonts/inter-latin.woff2') format('woff2');
  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: 'Inter'; font-style: normal; font-weight: 600; font-display: swap;
  src: url('/assets/fonts/inter-latin.woff2') format('woff2');
  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: 'Inter'; font-style: normal; font-weight: 700; font-display: swap;
  src: url('/assets/fonts/inter-latin.woff2') format('woff2');
  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: 'Space Grotesk'; font-style: normal; font-weight: 300; font-display: swap;
  src: url('/assets/fonts/space-grotesk-latin.woff2') format('woff2');
  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: 'Space Grotesk'; font-style: normal; font-weight: 400; font-display: swap;
  src: url('/assets/fonts/space-grotesk-latin.woff2') format('woff2');
  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: 'Space Grotesk'; font-style: normal; font-weight: 500; font-display: swap;
  src: url('/assets/fonts/space-grotesk-latin.woff2') format('woff2');
  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: 'Space Grotesk'; font-style: normal; font-weight: 600; font-display: swap;
  src: url('/assets/fonts/space-grotesk-latin.woff2') format('woff2');
  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: 'Space Grotesk'; font-style: normal; font-weight: 700; font-display: swap;
  src: url('/assets/fonts/space-grotesk-latin.woff2') format('woff2');
  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: 'Space Mono'; font-style: normal; font-weight: 400; font-display: swap;
  src: url('/assets/fonts/space-mono-400-latin.woff2') format('woff2');
  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: 'Space Mono'; font-style: normal; font-weight: 700; font-display: swap;
  src: url('/assets/fonts/space-mono-700-latin.woff2') format('woff2');
  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;
}

/* ----------------------------------------------------------------------------
   1 · TOKENS
   ---------------------------------------------------------------------------- */
:root {
  /* — Brand accents (constant across modes) — */
  --primary:        #C8E64A;   /* PEVRA Lime — hero accent, CTAs, "you", active */
  --primary-press:  #B4D335;   /* pressed / hover-darken */
  --primary-dim:    rgba(200,230,74,0.18);
  --primary-ink:    #46630E;   /* deep lime — lime-as-text on LIGHT surfaces */
  --primary-bright: #F5FBE0;   /* primary-as-text on DARK grounds. --primary
                                  #C8E64A measures 12.39:1 on --surface before
                                  any glow, which still isn't enough where a
                                  lime .glow sits right under lime text (found
                                  via the I2 sweep: .weight__pct on navigation
                                  landed at 5.5:1). NOT named -ink: that suffix
                                  means darkened-for-LIGHT here (see
                                  --primary-ink above). */

  --charge:         #FF2E93;   /* hot pink — ALL charging UI (markers, stops) */
  --charge-soft:    #FF8CC8;   /* pink glow tint */
  --charge-ink:     #A81965;   /* charge-as-text on LIGHT surfaces. Darkened from
                                  #C81E78 in the Plan 5 final-review fix wave.
                                  The old value composited through the light chip
                                  stack (--chip-scrim white 0.78, then the 0.14
                                  pink tint, then the ink) computes 4.44:1 on
                                  --surface #FFFFFF, 4.37 on --bg #F4F7EF and
                                  4.31 on --bg-deep #ECF1E2 — all UNDER the 4.5
                                  AA floor Jesse ratified for chips, before any
                                  glow is layered on. Zero light-theme charge
                                  chips render today, so this was a latent rule,
                                  not a shipped defect; it would have shipped the
                                  moment anyone put an .eyebrow--charge on a docs
                                  page. #A81965 computes 5.80 / 5.71 / 5.64 on the
                                  same three grounds, which deliberately matches
                                  the margin --cyan-ink carries after its own
                                  darkening (5.89 / 5.80 / 5.74). Relative
                                  luminance 0.1456 -> 0.0996. */
  --charge-bright:  #FFADD5;   /* charge-as-text on DARK grounds. --charge #FF2E93
                                  measures 5.05:1 on --surface before any glow,
                                  which caps a glowing card's intensity. NOT named
                                  -ink: that suffix means darkened-for-LIGHT here
                                  (see --charge-ink above). */

  --cyan:           #5DEDFF;   /* idle / planning / archive accent + glow */
  --cyan-ink:       #0C6A7D;   /* cyan-as-text on LIGHT surfaces. Darkened from
                                  #0E7C92 in Plan 5 fix round 1: the browser gate
                                  measured the old value at 4.23:1 on the /help
                                  hero with the glow AND the chip tint both
                                  suppressed — i.e. it missed WCAG AA (4.5) as a
                                  bare token, before anything was layered on it.
                                  Relative luminance 0.163 -> 0.118. Only two
                                  consumers (.eyebrow--cyan, .chip--cyan, both
                                  light-theme), and both are text on light, so
                                  darkening can only raise their contrast. */
  --live:           #FF3B47;   /* LIVE state, breathing glow */
  --violet:         #B584FF;   /* rider identity */
  --orange:         #FF8C42;   /* rider identity */
  --gold:           #FFE600;   /* rider identity */
  --rose:           #FF7AAB;   /* rider identity */

  /* — Range / SOC semantic bands (shared everywhere) — */
  --success:        #22C55E;   /* in range / safe / ≥50% SOC */
  --warning:        #F59E0B;   /* charge soon / 20–50% SOC */
  --danger:         #EF4444;   /* critical / <20% SOC */
  --danger-bright:  #FCA5A5;   /* danger-as-text on DARK grounds. --danger #EF4444
                                  measures 4.65:1 on --surface before any glow,
                                  which caps a glowing card's intensity. NOT named
                                  -ink: that suffix means darkened-for-LIGHT here. */

  /* — Map (carried from in-app + viewers) — */
  --map-route:        #C8E64A;
  --map-route-casing: #2A3A0F;
  --map-trail:        #EAFF45;
  --map-grid:         rgba(200,230,74,0.07);

  /* — DARK surface tokens (default) — */
  --bg:        #0A0F0A;
  --bg-deep:   #050805;
  --surface:   #121C12;   /* cards */
  --surface-2: #0E160E;   /* recessed */
  --surface-3: #18241704; /* hairline raise */
  --border:    #1E3A1E;
  --border-2:  #2A3A20;
  --ink:       #E8FFE8;   /* primary text */
  --ink-2:     #B6D0B6;   /* secondary text */
  --muted:     #6B9E6B;   /* tertiary / labels */
  --dim:       rgba(232,255,232,0.35);
  --link:      var(--primary);

  /* — Type — */
  --font-display: "Space Grotesk", system-ui, sans-serif;
  --font-body:    "Inter", system-ui, -apple-system, sans-serif;
  --font-mono:    "Space Mono", ui-monospace, "SFMono-Regular", monospace;

  /* — Type scale — desktop tier.
     Added because :root previously defined only families, leaving all 67 font
     sizes as literals with nowhere to introduce a phone tier. All six tokens
     are consumed: the display ladder (--step-hero/h1/h2/h3) by the headings
     below, --step-lead by .lead, and --step-micro by the phone-tier
     micro-label floor in the 720px block. --step-micro's DESKTOP value is
     deliberately unused — the sub-12px micro-label literals keep their own
     sizes above 720px, because routing them through this token would raise the
     9px ones to 11px. Converting the remaining literals would be risk without
     benefit. */
  --step-hero:  clamp(46px, 6.4vw, 92px);
  --step-h1:    clamp(38px, 4.6vw, 60px);
  --step-h2:    clamp(28px, 3.2vw, 42px);
  --step-h3:    clamp(21px, 2vw, 27px);
  --step-lead:  clamp(17px, 1.5vw, 21px);
  --step-micro: 11px;

  /* — Radius — */
  --r-xs: 4px;    /* stop cards, code chips */
  --r-sm: 8px;    /* chips, small controls */
  --r-md: 14px;   /* standard cards */
  --r-lg: 20px;   /* marketing feature cards */
  --r-xl: 28px;   /* hero panels */
  --r-pill: 999px;

  /* — Spacing (8pt) — */
  --s-1: 4px;  --s-2: 8px;  --s-3: 12px; --s-4: 16px; --s-5: 24px;
  --s-6: 32px; --s-7: 48px; --s-8: 64px; --s-9: 96px; --s-10: 128px;

  /* — Elevation / glow — */
  --halo-lime: 0 0 14px rgba(200,230,74,0.35);
  --halo-lime-lg: 0 0 26px rgba(200,230,74,0.45);
  --halo-live: 0 0 20px rgba(239,68,68,0.45);
  --shadow-card: 0 1px 0 rgba(255,255,255,0.03), 0 18px 40px -24px rgba(0,0,0,0.7);
  --shadow-pop:  0 24px 60px -28px rgba(0,0,0,0.85);
  --text-over-map: 0 2px 24px rgba(0,0,0,0.6);

  --container: 1200px;
  --nav-h: 68px;
}

/* — LIGHT mode: help center / docs / long-form support — */
[data-theme="light"] {
  --bg:        #F4F7EF;   /* warm paper, barely-green */
  --bg-deep:   #ECF1E2;
  --surface:   #FFFFFF;
  --surface-2: #FBFCF7;
  --surface-3: #F3F6EC;
  --border:    #E2E9D7;
  --border-2:  #D4DEC6;
  --ink:       #0E1A0E;
  --ink-2:     #3C4A38;
  --muted:     #6E7D69;
  --dim:       rgba(14,26,14,0.42);
  --link:      var(--primary-ink);

  --map-grid:  rgba(70,99,14,0.10);
  --shadow-card: 0 1px 0 rgba(255,255,255,0.6), 0 14px 30px -22px rgba(20,40,20,0.35);
  --shadow-pop:  0 24px 50px -28px rgba(20,40,20,0.30);
  --text-over-map: none;
}

/* ----------------------------------------------------------------------------
   2 · BASE
   ---------------------------------------------------------------------------- */
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.6;
  color: var(--ink);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
a { color: var(--link); text-decoration: none; }
a:hover { text-decoration: none; }
img { max-width: 100%; display: block; }
::selection { background: var(--primary); color: #0A0F0A; }

/* ----------------------------------------------------------------------------
   3 · TYPOGRAPHY
   ---------------------------------------------------------------------------- */
.display { font-family: var(--font-display); font-weight: 700; letter-spacing: -0.02em; line-height: 0.96; }
.hero-display {
  font-family: var(--font-display); font-weight: 700;
  font-size: var(--step-hero); line-height: 0.96;
  letter-spacing: -0.035em; margin: 0; text-wrap: balance;
}
h1, .h1 { font-family: var(--font-display); font-weight: 700; font-size: var(--step-h1); line-height: 1.06; letter-spacing: -0.03em; margin: 0; text-wrap: balance; }
h2, .h2 { font-family: var(--font-display); font-weight: 700; font-size: var(--step-h2); line-height: 1.08; letter-spacing: -0.02em; margin: 0; text-wrap: balance; }
h3, .h3 { font-family: var(--font-display); font-weight: 600; font-size: var(--step-h3); line-height: 1.14; letter-spacing: -0.01em; margin: 0; text-wrap: balance; }
h4, .h4 { font-family: var(--font-display); font-weight: 600; font-size: 19px; line-height: 1.2; margin: 0; }
p { margin: 0 0 1em; text-wrap: pretty; }
/* The token's desktop value is byte-identical to the literal this replaces, so
   this is a no-op above 720px and -0.5px below it. */
.lead { font-size: var(--step-lead); line-height: 1.5; color: var(--ink-2); }
.italic-sub { font-family: var(--font-display); font-weight: 300; font-style: italic; color: var(--ink-2); }
.body-muted { color: var(--muted); }
.small { font-size: 14px; }

/* Eyebrow — first-class chip signposting (Plan 5). NOT a control: no hover,
   no cursor, never wrapped in an anchor. 17px at EVERY viewport — crowding is
   solved by spacing, never by shrinking (guard: build.test.mjs). */
/* THE CHIP CARRIES ITS OWN GROUND (fix round 1, 2026-08-03; owner-ratified AA
   floor). The tint alphas alone made chip legibility a FUNCTION OF WHATEVER THE
   CHIP LANDED ON, and the browser gate measured what that costs: the same lime
   chip read 3.18:1 on /navigation's hero at 1280 and 5.07:1 at 390, because a
   section glow's hotspot sits behind it at one width and not the other. Not the
   tint's fault and not fixable by tuning it — with the glow off entirely that
   ground reads 13.7:1, so the wash IS the whole defect, and clearing it by
   thinning the tint needs alpha ~0.03 (an invisible pill).
   --chip-scrim is a near-black (dark) / near-white (light) layer UNDER the hue
   tint. Where there is no glow it is almost a no-op — it darkens an already
   near-black card by ~6 sRGB units, so 20 of the 26 gated chips render
   essentially as before — and where a glow would otherwise wash the chip out it
   holds the ground down. Chip contrast becomes a near-CONSTANT per hue instead
   of a property of the layout, which is also why the gate can assert it.
   ⚠ Deliberately fixed at the CHIP, not at the glow: dimming --glow-k on every
   chip-bearing section would have changed the site's ambient signature
   everywhere to fix ~40x190px in two places. Two layers, so `background` must
   stay split into -color and -image; a `background:` shorthand in a variant rule
   would silently drop the scrim. */
.eyebrow {
  display: inline-flex; align-items: center; gap: 10px;
  font-family: var(--font-mono); font-weight: 700;
  font-size: 17px; letter-spacing: 0.14em; text-transform: uppercase;
  padding: 6px 16px; border-radius: 999px;
  color: var(--muted);
  --chip-scrim: rgba(11, 15, 11, 0.72);
  --chip-tint: rgba(151, 164, 143, 0.10);
  background-color: var(--chip-scrim);
  background-image: linear-gradient(var(--chip-tint), var(--chip-tint));
}
.eyebrow::before {
  content: ""; width: 9px; height: 9px; border-radius: 50%;
  background: currentColor; flex: none;
}
/* ⚠ --charge-bright, NOT --charge. The token block says it in so many words:
   "--charge #FF2E93 measures 5.05:1 on --surface before any glow". The gate
   measured exactly 5.051 on the / mini-hero card with the glow suppressed, so
   a hot-pink chip cannot clear AA on any dark ground once its own tint and any
   glow are added. --charge-bright is the documented charge-as-text-on-dark
   token and the site already ships one chip using it (/how-it-works). */
.eyebrow--lime  { color: var(--primary);        --chip-tint: rgba(200, 230, 74, 0.12); }
.eyebrow--charge{ color: var(--charge-bright);  --chip-tint: rgba(255, 46, 147, 0.12); }
.eyebrow--cyan  { color: var(--cyan);           --chip-tint: rgba(93, 237, 255, 0.12); }
[data-theme="light"] .eyebrow         { --chip-scrim: rgba(255, 255, 255, 0.78); }
[data-theme="light"] .eyebrow--lime   { color: var(--primary-ink); --chip-tint: rgba(200, 230, 74, 0.14); }
[data-theme="light"] .eyebrow--charge { color: var(--charge-ink);  --chip-tint: rgba(255, 46, 147, 0.14); }
[data-theme="light"] .eyebrow--cyan   { color: var(--cyan-ink);    --chip-tint: rgba(93, 237, 255, 0.14); }

/* Mono helpers — telemetry numbers ONLY */
.mono { font-family: var(--font-mono); }
.stat-num { font-family: var(--font-mono); font-weight: 700; letter-spacing: -0.02em; line-height: 1; }
.stat-unit { font-family: var(--font-mono); font-size: 0.5em; color: var(--muted); margin-left: 0.35em; vertical-align: baseline; }
.stat-label { font-family: var(--font-mono); font-weight: 700; font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--muted); }

/* Circled sequence numerals (the /how-it-works problem cards). Owner rule
   2026-08-03: sequence markers are enclosed in a circle and NEVER zero-padded
   — "01" is banned site-wide (build.test.mjs sweeps every page for it).
   Fill/ink follow the lime chip recipe (.eyebrow--lime) so the circle reads
   as the same family, not a fourth accent. */
.step-num {
  display: inline-flex; align-items: center; justify-content: center;
  width: 52px; height: 52px; border-radius: 50%;
  font-family: var(--font-mono); font-weight: 700; font-size: 24px; line-height: 1;
  color: var(--primary);
  background: rgba(200, 230, 74, 0.12);
  border: 1px solid rgba(200, 230, 74, 0.35);
}
[data-theme="light"] .step-num { color: var(--primary-ink); background: rgba(200, 230, 74, 0.14); }

.text-lime   { color: var(--primary); }
.text-charge { color: var(--charge); }
[data-theme="light"] .text-lime   { color: var(--primary-ink); }
[data-theme="light"] .text-charge { color: var(--charge-ink); }

/* ----------------------------------------------------------------------------
   4 · LAYOUT
   ---------------------------------------------------------------------------- */
.container { width: 100%; max-width: var(--container); margin: 0 auto; padding-inline: 24px; }
.container--wide { max-width: 1360px; }
.container--narrow { max-width: 760px; }
.section { padding-block: clamp(56px, 9vw, 120px); position: relative; }
.section--tight { padding-block: clamp(40px, 6vw, 72px); }
.stack > * + * { margin-top: var(--s-4); }
.row { display: flex; align-items: center; gap: var(--s-3); }
.row-wrap { display: flex; flex-wrap: wrap; gap: var(--s-3); }
.grid { display: grid; gap: var(--s-5); }
.divider { height: 1px; background: var(--border); border: 0; margin: 0; }

/* Skip link. <main id="main"> has been in layout.mjs since the first build with
   nothing pointing at it — WCAG 2.4.1 on every page, and with the mobile panel
   now trapping focus the nav is a longer detour than it was.
   Clipped by offset rather than display:none or visibility:hidden, either of
   which would take it out of the tab order and defeat the point.
   The <main> half of this matters as much as the link: following a fragment to
   a non-focusable element scrolls the page but leaves document.activeElement on
   <body>, so the next Tab resumes inside the nav. layout.mjs therefore emits
   `<main id="main" tabindex="-1">`, and #main suppresses its own ring below —
   the skip link already shows one while focused, and a focus outline around the
   entire document is noise, not feedback. */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  padding: 12px 18px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  background: var(--primary);
  color: #0A0F0A;
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  border-radius: 0 0 var(--r-sm) 0;
}
.skip-link:focus { left: 0; }
#main:focus { outline: none; }

/* ----------------------------------------------------------------------------
   5 · GLOW OVERLAY — the signature state-aware corner spotlight
   ---------------------------------------------------------------------------- */
.glow {
  position: absolute; inset: 0; pointer-events: none; z-index: 0; overflow: hidden;
  --glow-c: 93, 237, 255;        /* cyan default */
  --glow-k: 0.8;                 /* intensity 0–1 */
}
.glow::before { content: ""; position: absolute; }
.glow::before {                 /* outer bloom */
  width: 130%; height: 120%;
  background: radial-gradient(ellipse 130% 110% at var(--glow-x,8%) var(--glow-y,6%),
    rgba(var(--glow-c), calc(var(--glow-k) * 0.40)) 0%,
    rgba(var(--glow-c), calc(var(--glow-k) * 0.20)) 30%,
    rgba(var(--glow-c), calc(var(--glow-k) * 0.06)) 55%,
    transparent 78%);
}
.glow::after {                  /* inner hotspot */
  content: ""; position: absolute;
  width: var(--glow-size, 680px); height: var(--glow-size, 680px);
  left: var(--glow-x,8%); top: var(--glow-y,6%); transform: translate(-50%,-50%);
  background: radial-gradient(circle at center,
    rgba(var(--glow-c), calc(var(--glow-k) * 0.48)) 0%,
    rgba(var(--glow-c), calc(var(--glow-k) * 0.18)) 38%, transparent 72%);
}
.glow--lime   { --glow-c: 200,230,74; }
.glow--charge { --glow-c: 255,46,147; }
.glow--cyan   { --glow-c: 93,237,255; }
.glow--live   { --glow-c: 255,80,90; }
.glow--amber  { --glow-c: 255,160,80; }
.glow--tr { --glow-x: 96%; --glow-y: 4%; }
.glow--tl { --glow-x: 6%;  --glow-y: 6%; }
.glow--br { --glow-x: 94%; --glow-y: 96%; }
.glow--bl { --glow-x: 6%;  --glow-y: 96%; }
.glow--c  { --glow-x: 50%; --glow-y: 38%; }
/* StatusGlow defines corners TL|TR|BL|BR *and* the L|R edge washes; this web
   port only ever implemented the corners plus centre. These complete it. */
.glow--l  { --glow-x: 0%;   --glow-y: 50%; }
.glow--r  { --glow-x: 100%; --glow-y: 50%; }
.glow.is-live::after { animation: glow-breath 3.6s ease-in-out infinite; }
[data-theme="light"] .glow { mix-blend-mode: multiply; opacity: 0.5; }

@keyframes glow-breath { 0%,100% { opacity: 0.85; transform: translate(-50%,-50%) scale(1); }
  50% { opacity: 1; transform: translate(-50%,-50%) scale(1.06); } }

/* Content above the glow */
.section > .container, .above { position: relative; z-index: 1; }

/* section 6 (route backdrop) intentionally omitted — Flow map motif, not used on pevra.org */

/* ----------------------------------------------------------------------------
   7 · NAV
   ---------------------------------------------------------------------------- */
.nav {
  position: sticky; top: 0; z-index: 50; height: var(--nav-h);
  display: flex; align-items: center;
  background: color-mix(in oklab, var(--bg) 78%, transparent);
  backdrop-filter: blur(16px) saturate(1.2);
  border-bottom: 1px solid var(--border);
}
.nav__inner { display: flex; align-items: center; gap: 28px; width: 100%; }
.nav__links { display: flex; align-items: center; gap: 26px; margin-left: 8px; }
.nav__links a {
  font-size: 14px; font-weight: 500; color: var(--ink-2); letter-spacing: 0.005em;
  padding: 6px 0; position: relative; transition: color 0.15s;
}
.nav__links a:hover { color: var(--ink); }
.nav__links a.is-active { color: var(--ink); }
.nav__links a.is-active::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -2px; height: 2px;
  background: var(--primary); border-radius: 2px;
}
.nav__spacer { flex: 1; }

/* Logo */
.logo { display: inline-flex; align-items: center; gap: 10px; min-height: 44px; }
.logo__mark {
  width: 28px; height: 28px; border-radius: 7px; background: var(--primary);
  display: grid; place-items: center; box-shadow: var(--halo-lime);
}
.logo__mark svg { width: 16px; height: 16px; display: block; }
.logo__word { font-family: var(--font-display); font-weight: 700; font-size: 18px; letter-spacing: 0.16em; color: var(--ink); }

/* ----------------------------------------------------------------------------
   8 · BUTTONS / PILLS
   ---------------------------------------------------------------------------- */
.pill {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  font-family: var(--font-mono); font-weight: 700; font-size: 11px;
  letter-spacing: 0.14em; text-transform: uppercase;
  padding: 11px 20px; min-height: 44px; border-radius: var(--r-pill); border: 1px solid transparent;
  cursor: pointer; white-space: nowrap; transition: all 0.16s ease; line-height: 1;
}
.pill--primary { background: var(--primary); color: #0A0F0A; box-shadow: var(--halo-lime); }
.pill--primary:hover { background: var(--primary-press); box-shadow: var(--halo-lime-lg); transform: translateY(-1px); }
.pill--ghost { background: color-mix(in oklab, var(--bg) 55%, transparent); color: var(--ink-2); border-color: var(--border-2); }
.pill--ghost:hover { color: var(--ink); border-color: var(--muted); }
.pill--charge { background: var(--charge); color: #fff; box-shadow: 0 0 14px rgba(255,46,147,0.4); }
.pill--lg { font-size: 12px; padding: 14px 26px; }
.pill--sm { font-size: 10px; padding: 8px 14px; }
[data-theme="light"] .pill--ghost { background: var(--surface); border-color: var(--border-2); color: var(--ink-2); }

/* Plain text link with arrow */
.link-arrow { display: inline-flex; align-items: center; gap: 7px; font-weight: 600; font-size: 14px; color: var(--link); }
.link-arrow svg { width: 14px; height: 14px; transition: transform 0.16s; }
.link-arrow:hover svg { transform: translateX(3px); }

/* ----------------------------------------------------------------------------
   9 · CHIPS / BADGES
   ---------------------------------------------------------------------------- */
.chip {
  display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-mono);
  font-weight: 700; font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase;
  padding: 5px 10px; border-radius: var(--r-pill); border: 1px solid var(--border-2);
  color: var(--ink-2); background: var(--surface-2);
}
.chip--lime   { color: var(--primary); border-color: rgba(200,230,74,0.4); background: var(--primary-dim); }
.chip--charge { color: var(--charge); border-color: rgba(255,46,147,0.4); background: rgba(255,46,147,0.12); }
.chip--cyan   { color: var(--cyan); border-color: rgba(93,237,255,0.35); background: rgba(93,237,255,0.1); }
[data-theme="light"] .chip--lime   { color: var(--primary-ink); }
[data-theme="light"] .chip--charge { color: var(--charge-ink); }
[data-theme="light"] .chip--cyan   { color: var(--cyan-ink); }
.chip__dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }

/* LIVE badge */
.live-badge {
  display: inline-flex; align-items: center; gap: 8px; padding: 7px 14px; border-radius: var(--r-pill);
  background: linear-gradient(135deg, rgba(239,68,68,0.32), rgba(239,68,68,0.14));
  border: 1px solid rgba(255,90,90,0.5); box-shadow: var(--halo-live);
  font-family: var(--font-mono); font-weight: 700; font-size: 11px; letter-spacing: 0.16em; color: #FFD9DC;
}
.live-badge__dot { width: 8px; height: 8px; border-radius: 50%; background: var(--live); box-shadow: 0 0 8px var(--live); }

/* ----------------------------------------------------------------------------
   10 · CARDS
   ---------------------------------------------------------------------------- */
.card {
  border-radius: var(--r-md); padding: var(--s-5);
}
/* Feature card (marketing) */
.feature-card {
  background: var(--surface); border-radius: var(--r-lg);
  padding: var(--s-6); box-shadow: var(--shadow-card); transition: transform 0.18s;
}
.feature-card:hover { transform: translateY(-3px); }
.feature-card:hover .glow { --glow-k: 0.30; }   /* (0,3,0) beats the (0,2,0) surface rule */
.feature-card__icon {
  width: 46px; height: 46px; border-radius: var(--r-sm); display: grid; place-items: center;
  background: var(--primary-dim); color: var(--primary); margin-bottom: var(--s-4);
}
[data-theme="light"] .feature-card__icon { color: var(--primary-ink); }
.feature-card__icon svg { width: 24px; height: 24px; }

/* Micro-scene mini-hero head (Plan 5 Task 4). Bleeds past .feature-card's own
   padding (--s-6) to the card's edges, clipped to the card's own radius
   (--r-lg) on the top two corners only — the card keeps its bottom radius,
   the scene doesn't round it twice. */
.feature-card .glow { --glow-size: 260px; }   /* card-scale hotspot (Task 1 seam) */
.feature-card__scene {
  aspect-ratio: 21 / 9;
  margin: calc(-1 * var(--s-6)) calc(-1 * var(--s-6)) var(--s-4);
  overflow: hidden; position: relative;
  border-radius: var(--r-lg) var(--r-lg) 0 0;
}
.feature-card__scene img { display: block; width: 100%; height: 100%; object-fit: cover; }

/* Official app-store badges (storeBadges component). Height, not width, is the
   knob: both badges scale to one visual height so neither store's mark reads
   subordinate. */
.store-badge { display: block; height: 52px; width: auto; }

/* ----------------------------------------------------------------------------
   10b · CONVERTED SURFACES — definition without an outline
   Defined by fill-vs-page contrast plus the tokenised drop shadow, never by an
   edge. The glow supplies all variation.
   ---------------------------------------------------------------------------- */
.feature-card, .xlink, .callout, .card, .aside {
  position: relative;           /* containing block for the .glow overlay */
  overflow: hidden;             /* clips the outer half of an edge-anchored pool */
  border: 0;
  background: var(--surface);
  box-shadow: var(--shadow-card);
}

/* Content sits above the glow. The existing .above wrapper is NOT used here:
   .xlink is a grid with named areas and .callout is a flex row, so moving their
   children into a wrapper collapses the layout. Direct children carry the same
   two declarations — same stacking, no markup restructuring. */
.feature-card > :not(.glow), .xlink > :not(.glow), .callout > :not(.glow),
.card > :not(.glow), .aside > :not(.glow) { position: relative; z-index: 1; }

/* Body copy leaves the muted tone. This is the enabler, not a preference:
   --muted measures 5.60:1 on --surface, which under the approved glow
   intensities drops to 4.10:1 — under the 4.5 AA floor. On --ink the same
   glow measures 12.13:1. (This comment previously misnamed the 5.60:1 token
   as --ink-2 — --ink-2 actually measures 10.57:1 on --surface, unglowed.)
   Scoped to BODY COPY ONLY. An earlier draft used `> :not(.glow)`, which also
   hit .feature-card__icon and .link-arrow — both direct children — turning
   seven lime icons and nine lime arrows near-white. */
.feature-card p, .xlink .xlink__blurb, .callout .callout__body,
.card p, .card .body-muted, .aside .aside__body { color: var(--ink); }

/* .stat-label (flow-web.css:197) defaults to --muted — the SAME 5.60:1 token
   as body copy above, and its biggest consumer on these surfaces: 63 spans
   carry no inline colour, and on roughly half the converted cards the label
   is the first element in the box, usually directly under the glow hotspot.
   Missing this in the lift above regressed 22 cards, 14 of them below the
   4.5 AA floor (e.g. group-rides card 5.60 -> 2.28 @1280px). A first attempt
   lifted to --ink-2 (10.57:1 unglowed) instead of --ink, hoping to keep a
   label/body tone distinction — measured, it wasn't enough: a `.stat-label`
   sitting right at a glow's anchor corner (route-planning cards 3 and 4, both
   `.card .glow` at k=0.46) still landed as low as 6.48:1, under the 7:1 floor
   with no escape available (--ink-2's 10.57 baseline itself clears 7, so no
   token that starts there gets a relaxed floor). --ink carries the SAME
   glow to 12.13:1 (see the body-copy comment above) with room to spare, so
   the label uses it too — no tone distinction from body copy survives, but
   nothing here gets away with assuming one that isn't measured. */
.feature-card .stat-label, .xlink .stat-label, .callout .stat-label,
.card .stat-label, .aside .stat-label { color: var(--ink); }

/* .weight__pct (site.css) is --primary by default — a lime figure inside a
   lime-glowed .card (navigation's "How a route is scored") is the same
   self-tinting problem C2 fixed for --charge, found by the same I2 sweep.
   --primary alone (12.39:1 unglowed) still isn't enough at this glow's
   anchor corner; --primary-bright is. Only ever used inside this one card
   site-wide, so this override, not a change to the shared token. */
.card .weight__pct { color: var(--primary-bright); }

/* Measured per-surface intensity. (0,2,0) beats site.css's <=720px
   `.glow { --glow-k: 0.45 }` at (0,1,0) — these values were measured per
   surface, so they hold at phone width — and loses to `:hover .glow` at
   (0,3,0), so hover brightening works without !important. */
.callout .glow      { --glow-k: 0.9; --glow-size: 120px; }
.aside .glow        { --glow-k: 0.46; }
.feature-card .glow { --glow-k: 0.18; }
.card .glow         { --glow-k: 0.46; }
.xlink .glow        { --glow-k: 0.16; }
.xlink .glow--v2    { --glow-k: 0.14; }   /* must follow `.xlink .glow` — same
.xlink .glow--v3    { --glow-k: 0.20; }      specificity, source order decides */

/* Both layers are explicitly sized here: ::after is --glow-size (120px),
   ::before is 1.6x that (192px), on a 712x128 callout. ::before's base rule
   defaults to 130%/120% of the CARD, so left unsized its wash alone covers
   the whole left third regardless of ::after's size — shrinking ::after
   alone moved p1 only 4.01 to 4.38, sizing both reached 7.32. Post-override,
   ::before still contributes for real: removing it drops peak boundary
   luminance 0.2818 to 0.2217 (~21% of peak), decaying to ~0 by 90px inward. */
.callout .glow::before {
  width: calc(var(--glow-size) * 1.6); height: calc(var(--glow-size) * 1.6);
  top: 50%; left: 0; transform: translate(-50%, -50%);
}

/* ----------------------------------------------------------------------------
   11 · STAT BLOCKS
   ---------------------------------------------------------------------------- */
.edge-stat { display: flex; flex-direction: column; gap: 4px; }
.edge-stat .stat-num { font-size: clamp(34px, 4vw, 48px); }
.edge-stat .stat-label { order: -1; }
.stat-band { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--s-5); }
/* A band holding exactly two figures. Home's group-rides pair carried this
   inline, which beat every media query — convenient on mobile but it would
   also strand the two figures in the desktop repeat(4, 1fr) if the inline
   style were simply deleted. */
.stat-band--pair { grid-template-columns: 1fr 1fr; max-width: 380px; }
.stat-band .edge-stat .stat-num { color: var(--primary); }
[data-theme="light"] .stat-band .edge-stat .stat-num { color: var(--primary-ink); }

/* ----------------------------------------------------------------------------
   12 · SOC RING — brand battery primitive (carryover)
   ---------------------------------------------------------------------------- */
.soc-ring { position: relative; display: inline-grid; place-items: center; }
.soc-ring svg { transform: rotate(-90deg); }
.soc-ring__track { fill: none; stroke: var(--border); }
.soc-ring__arc { fill: none; stroke: var(--success); stroke-linecap: round; transition: stroke-dashoffset 0.4s; }
.soc-ring__val { position: absolute; font-family: var(--font-mono); font-weight: 700; }
.soc-ring__unit { position: absolute; bottom: 18%; font-family: var(--font-mono); font-size: 9px; letter-spacing: 0.1em; color: var(--muted); }

/* ----------------------------------------------------------------------------
   13 · DEVICE FRAME (already-bezeled screenshots drop straight in)
   ---------------------------------------------------------------------------- */
/* Device frame. The old mockup PNGs had a phone bezel painted INTO the image;
   the real screenshots are raw 585x1280 captures, so the frame lives here now.
   max-height matters: an unconstrained portrait capture would tower over its
   feature row, where the 924x540 landscape mockups sat neatly. */
.device {
  position: relative; display: inline-block; max-width: 100%;
  padding: 8px; border-radius: 30px;
  background: linear-gradient(160deg, #1c1f1c, #0b0e0b);
  border: 1px solid rgba(255,255,255,0.08);
  filter: drop-shadow(0 40px 60px rgba(0,0,0,0.55));
}
/* Definite width + intrinsic aspect-ratio reserves the correct box BEFORE any
   bytes arrive (was width:100% inside an inline-block parent with no
   definite width — a circular reference that resolved to 0, collapsing the
   box to 18x18px until the image landed and shoved the page ~200ms after
   paint; CLS 0.367/0.355/0.239 on nav/route-planning/group-rides). All nine
   phone screenshots are 585x1280 (verified via each file's VP8 header).
   max-width:100% here is belt-and-suspenders with the global
   `img { max-width: 100% }` reset (line 165, which already covers every
   .device/.device--wide image by lower-specificity fallback) — kept
   explicit and local so this rule stays correct even if that global reset
   is ever narrowed or reordered. Verified by direct mutation: removing only
   this declaration, with the global reset intact, still renders phone
   images correctly (266px/280px, zero overflow) at 320px/390px on
   /navigation and /charging — the global reset alone already prevents the
   overflow. Only removing BOTH this declaration and the global reset
   reproduces a 280px box that never shrinks. The real mechanism worth
   keeping in mind: a percentage max-width is treated as `none` during
   intrinsic sizing, so whichever of the two rules is in force still lets
   the box reserve its full 280px while unloaded. */
.device img {
  display: block; width: 280px; max-width: 100%; aspect-ratio: 585 / 1280; border-radius: 23px;
  max-height: 620px; object-fit: contain; background: #000;
}
/* Tablet capture is near-square, not phone-shaped — let it breathe.
   group-ride-fold.svg is 1076x1038 (ratio 1.0366). Width is bounded by the
   500px max-height clamp (518 / 1.0366 = 499.7), not by max-width — the old
   max-width:560px is dead once width is definite, and dropping the old
   max-width:280px inherited from .device img above is safe (build.test.mjs
   pins only the max-height clamp, not that value). max-width:100% here is
   belt-and-suspenders with the global `img { max-width: 100% }` reset
   (line 165, which already covers every .device/.device--wide image by
   lower-specificity fallback) — kept explicit and local so this rule stays
   correct even if that global reset is ever narrowed or reordered. Verified
   by direct mutation: with BOTH this declaration and the global reset gone,
   the image renders 518px at every viewport — right edge at x=545 against a
   390px viewport, exactly as predicted; .phero's inline overflow:hidden
   hides the spill from any scrollWidth test either way. */
.device--wide img { width: 518px; max-width: 100%; aspect-ratio: 1076 / 1038; max-height: 500px; }
.device--float { animation: device-float 6s ease-in-out infinite; }
@keyframes device-float { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-12px); } }
@media (prefers-reduced-motion: reduce) { .device--float { animation: none; } }

/* ----------------------------------------------------------------------------
   14 · PHOTO SLOT — lifestyle photography placeholder
   ---------------------------------------------------------------------------- */
.photo-slot {
  position: relative; border-radius: var(--r-lg); overflow: hidden;
  background:
    repeating-linear-gradient(135deg, rgba(200,230,74,0.05) 0 14px, transparent 14px 28px),
    var(--surface-2);
  border: 1px solid var(--border); display: grid; place-items: center; min-height: 240px;
}
.photo-slot__label {
  font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--muted); text-align: center; padding: 12px 20px; line-height: 1.7;
}
.photo-slot__label b { display: block; color: var(--ink-2); font-weight: 700; }

/* A FILLED lifestyle slot. Unfilled slots render nothing at all — photoSlot()
   returns '' without a src — so .photo-slot above is now only reachable if a
   caller deliberately reinstates the placeholder.

   Hairline dropped, deliberately NO glow added (unlike the other converted
   surfaces): a .photo is filled edge-to-edge by an opaque <img>, so a .glow
   div behind it would paint only where the frame is uncovered — nowhere.
   The photograph's own content is its edge. */
.photo { margin: 0; border-radius: var(--r-lg); overflow: hidden; }
.photo img { display: block; width: 100%; height: 100%; object-fit: cover; }

/* Hero artwork. Anchored right so the composition's empty left third stays
   clear for the copy; the scrim keeps text legible where the two overlap. */
.hero__art { position: absolute; inset: 0; overflow: hidden; z-index: 0; }
.hero__art img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; object-position: 80% center;
}
.hero__art::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(100deg,
    var(--bg-deep) 0%, rgba(10,15,10,0.82) 34%,
    rgba(10,15,10,0.35) 58%, rgba(10,15,10,0.10) 100%);
}
/* NB: .hero and .hero__inner already carry position/z-index in site.css
   (:100-101), which loads after this file — do not redeclare them here. */
/* The old 760px scrim stop is gone — this file no longer has a media block at
   that width. Below 720px site.css's phone tier wins by load order, so the
   block's only live range was 721-760px, and in that window the <picture> above
   still serves the LANDSCAPE file: its mobile <source> is gated at 720px and
   below, so 721px falls through to the landscape <img>. Measured at 740px before
   the change: currentSrc hero-charge-chain.webp, object-position 68% 50%, a
   180deg 0.72 -> 0.92 gradient.
   So the block dressed landscape artwork in a portrait-tuned scrim. Measured
   result, which is the OPPOSITE of the "near-black rectangle" this was long
   assumed to be: near-black at 0.72-0.92 over the BRIGHT region of the landscape
   file yields a flat, washed-out olive-green haze, LIGHTER than the desktop
   treatment, in which the route line and charger nodes are nearly invisible and
   the lower copy loses contrast. Compare the 740px screenshots in the Task 8
   report if you are about to "restore" it.
   721px and up now use the diagonal 100deg desktop scrim, which is what the
   landscape composition was cut for — and, collaterally, the desktop 80% center
   crop, since this block carried the 68% center object-position too. Checked at
   740px: the charge chain stays fully in frame.
   No media-query opener and no `(max-width: …px)`-shaped text is written out in
   this note ON PURPOSE. The breakpoint census in build.test.mjs scans these
   files' RAW text, comments included, so any such phrase here registers as a
   live stop — and because `[^{]*` crosses newlines, one `@media` word anywhere
   in a comment will also reach forward and capture the next width it finds. */

/* Store QR panels (download page). White plate is required — QR scanners need
   the light modules to stay light regardless of page theme. */
.qr { margin: 0; display: grid; justify-items: center; gap: 8px; }
.qr img { display: block; border-radius: 8px; background: #fff; padding: 6px; }
.qr__cap {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--muted);
}

/* ----------------------------------------------------------------------------
   15 · FOOTER
   ---------------------------------------------------------------------------- */
.footer { background: var(--bg-deep); border-top: 1px solid var(--border); padding-block: var(--s-8) var(--s-6); }
.footer__grid { display: grid; grid-template-columns: 1.6fr repeat(4, 1fr); gap: var(--s-6); }
/* h2, not h5: the page's last body heading is an h2, so an h5 here was a
   skipped heading level (a11y). Styled as the tiny mono label it always was;
   the overrides below beat the global h2 display rules. 11px = the ui-text
   floor. */
.footer__col h2 { font-family: var(--font-mono); font-size: 11px; line-height: 1.2; letter-spacing: 0.18em; text-transform: uppercase; color: var(--muted); margin: 0 0 14px; }
.footer__col a { display: flex; align-items: center; min-height: 44px; margin-bottom: 4px; font-size: 14px; color: var(--ink-2); padding: 5px 0; }
.footer__col a:hover { color: var(--ink); }
/* flex-wrap + gap, at every width. This bar never OVERFLOWED — scrollWidth ==
   clientWidth at all seven measured widths — but nowrap with no gap put its two
   spans' edges exactly 0.00px apart at 320/360/390/430 while each wrapped
   internally, so the copyright and the tagline read as one run of text. */
.footer__bottom { display: flex; flex-wrap: wrap; gap: var(--s-3); justify-content: space-between; align-items: center; margin-top: var(--s-7); padding-top: var(--s-5); border-top: 1px solid var(--border); font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.06em; color: var(--muted); }

/* ============================================================================
   16 · SUPPORT / HELP CENTER  (light surfaces)
   ============================================================================ */

/* Search bar */
.search {
  display: flex; align-items: center; gap: 12px; width: 100%;
  background: var(--surface); border: 1px solid var(--border-2); border-radius: var(--r-pill);
  padding: 15px 22px; box-shadow: var(--shadow-card);
}
.search:focus-within { border-color: var(--primary); box-shadow: 0 0 0 4px var(--primary-dim); }
.search svg { width: 20px; height: 20px; color: var(--muted); flex-shrink: 0; }
.search input { flex: 1; border: 0; background: none; outline: none; font-family: var(--font-body); font-size: 16px; color: var(--ink); }
.search input::placeholder { color: var(--muted); }
.search kbd {
  font-family: var(--font-mono); font-size: 11px; color: var(--muted);
  border: 1px solid var(--border-2); border-radius: 5px; padding: 3px 7px; background: var(--surface-2);
}

/* Category card */
.cat-card {
  display: block; background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md);
  padding: var(--s-5); box-shadow: var(--shadow-card); transition: transform 0.16s, border-color 0.16s;
}
.cat-card:hover { transform: translateY(-3px); border-color: var(--primary); }
.cat-card__icon { width: 42px; height: 42px; border-radius: var(--r-sm); display: grid; place-items: center; background: var(--primary-dim); color: var(--primary-ink); margin-bottom: var(--s-4); }
.cat-card__icon svg { width: 22px; height: 22px; }
.cat-card h3 { margin-bottom: 6px; }
.cat-card p { color: var(--muted); font-size: 14px; margin: 0 0 14px; }
.cat-card__count { font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--dim); }

/* Article list rows */
.art-row { display: flex; align-items: center; gap: 14px; padding: 16px 4px; border-bottom: 1px solid var(--border); transition: padding-left 0.16s; }
.art-row:hover { padding-left: 10px; }
.art-row:last-child { border-bottom: 0; }
.art-row__icon { width: 18px; height: 18px; color: var(--muted); flex-shrink: 0; }
.art-row__title { font-weight: 600; font-size: 15px; color: var(--ink); flex: 1; }
.art-row__meta { font-family: var(--font-mono); font-size: 11px; color: var(--dim); }

/* Breadcrumbs */
.crumbs { display: flex; align-items: center; gap: 9px; font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }
.crumbs a { color: var(--muted); } .crumbs a:hover { color: var(--ink); }
.crumbs .sep { opacity: 0.5; }

/* Docs layout: sidebar + content + TOC */
.docs { display: grid; grid-template-columns: 240px minmax(0,1fr) 200px; gap: var(--s-7); align-items: start; }
.docs__nav { position: sticky; top: calc(var(--nav-h) + 24px); font-size: 14px; }
.docs__nav h5 { font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--muted); margin: 0 0 10px; padding-top: 16px; }
.docs__nav a { display: block; padding: 7px 12px; border-radius: 7px; color: var(--ink-2); border-left: 2px solid transparent; }
.docs__nav a:hover { background: var(--surface-3); color: var(--ink); }
.docs__nav a.is-active { color: var(--primary-ink); background: var(--primary-dim); border-left-color: var(--primary); font-weight: 600; }
.docs__toc { position: sticky; top: calc(var(--nav-h) + 24px); font-size: 13px; }
.docs__toc h6 { font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--muted); margin: 0 0 10px; }
.docs__toc a { display: block; padding: 5px 0; color: var(--muted); border-left: 2px solid var(--border); padding-left: 12px; }
.docs__toc a:hover, .docs__toc a.is-active { color: var(--ink); border-left-color: var(--primary); }

/* Article prose */
.prose { max-width: 720px; }
/* 1.9em, not 1.1em. Measured at 16.5px/1.72, the old gap was 18.15px against a
   28.38px internal leading — lines inside a paragraph sat FURTHER apart than the
   paragraphs did, so long-form read as one undifferentiated block. 1.9em is
   31.35px, 1.10x the leading. Headings are unaffected: .prose h2/h3 carry their
   own margin-top at higher specificity (0,1,1) than this selector's (0,1,0). */
.prose > * + * { margin-top: 1.9em; }
.prose h2 { margin-top: 1.6em; margin-bottom: 0.2em; scroll-margin-top: 96px; }
.prose h3 { margin-top: 1.4em; margin-bottom: 0.2em; scroll-margin-top: 96px; }
.prose p, .prose li { font-size: 16.5px; line-height: 1.72; color: var(--ink-2); }
.prose strong { color: var(--ink); font-weight: 700; }
.prose a { color: var(--link); font-weight: 600; text-decoration: underline; text-decoration-color: var(--primary); text-underline-offset: 3px; }
.prose ul, .prose ol { padding-left: 22px; }
.prose li { margin: 6px 0; }
.prose code { font-family: var(--font-mono); font-size: 0.86em; background: var(--surface-3); border: 1px solid var(--border); border-radius: 5px; padding: 2px 6px; color: var(--charge-ink); }
/* Fenced code. Two blocks exist site-wide, both in the whitepaper, both
   untagged (no language class) — the longest is a 90-character line at 772px.
   `white-space: pre` cannot wrap it, so it scrolls inside its own box rather
   than widening the page. The nested <code> drops the inline chip's border,
   background and padding: those style a chip inside a sentence, not a block. */
.prose pre {
  margin: 0;
  padding: 14px 16px;
  overflow-x: auto;
  border-radius: var(--r-xs);
  background: var(--surface-2);
  border: 1px solid var(--border);
  -webkit-overflow-scrolling: touch;
}
.prose pre code {
  display: block;
  border: 0;
  background: none;
  padding: 0;
  border-radius: 0;
  color: var(--ink-2);
  font-size: 13.5px;
  line-height: 1.6;
}
.prose img, .prose .photo-slot { border-radius: var(--r-md); }

/* Tables. Five render site-wide — two 2-column, two 3-column, one 4-column. The
   wrapper scrolls; the table carries no min-width, because automatic table layout
   already refuses to go below its own min-content and the nowrap headers below
   make that floor generous. Measured in a 284px column (320px viewport): the
   2-column tables settle at 284, the 3-column at 334 and 340, the 4-column at 408.
   An earlier blanket 420px floor was calibrated against the 4-column table alone,
   where it bought 12px — and charged the other four up to 136px of needless swipe.
   Cells step DOWN one notch from the 16.5px prose so a dense grid reads as data. */
.prose-table {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
.prose table {
  width: 100%;
  border-collapse: collapse;
  font-size: 15.5px;
  line-height: 1.55;
}
.prose th,
.prose td {
  padding: 10px 12px;
  text-align: left;
  vertical-align: top;
  border-bottom: 1px solid var(--border);
  color: var(--ink-2);
}
.prose th {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 11.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
  border-bottom-color: var(--border-2);
}
.prose tbody tr:last-child th,
.prose tbody tr:last-child td { border-bottom: 0; }

/* Section rule. The user-agent default is a 2px rgb(128,128,128) inset groove —
   a mid-grey line on warm paper. Eleven render, nine of them in the whitepaper. */
.prose hr {
  height: 1px;
  border: 0;
  background: var(--border);
  margin-block: 2.4em;
}

/* Callout / note boxes */
.callout { display: flex; gap: 14px; padding: 16px 18px; border-radius: var(--r-md); --edge-c: 200, 230, 74; }
.callout__icon { width: 22px; height: 22px; flex-shrink: 0; margin-top: 1px; }
.callout__body { font-size: 15px; line-height: 1.6; color: var(--ink-2); }
.callout__body b { color: var(--ink); }
.callout--tip     { --edge-c: 200, 230, 74; }
.callout--tip .callout__icon { color: var(--primary-ink); }
/* I5: these two variants used to override 10b's `background: var(--surface)`
   (flow-web.css ~467) with a translucent wash — rgba(245,158,11,0.07) /
   rgba(255,46,147,0.05) — same specificity as .callout, later in source
   order, so the wash always won. A wash is a page-relative tint, not a card
   fill: 10b's whole contrast premise (fill-vs-page, plus --shadow-card) needs
   an OPAQUE ground under it, and /charging's .callout--charge title (--ink
   #E8FFE8) measured p1 2.96 as a result — confirmed not a glow artefact by
   sweeping seven glow size/intensity combinations, all 2.96.
   Fix: composite each wash over --surface (#121C12, the only value these two
   variants currently render on — both host pages, /charging and
   /group-rides, carry no [data-theme="light"]) with standard source-over
   alpha, then use that opaque hex directly. Keeps the same warning/charge
   tint, just as a real fill instead of a wash:
     rgba(245,158,11,0.07) over #121C12 -> #222512
     rgba(255,46,147,0.05) over #121C12 -> #1E1D18 */
.callout--warning { --edge-c: 245, 158, 11; background: #222512; }
.callout--warning .callout__icon { color: #B57400; }
.callout--charge  { --edge-c: 255, 46, 147; background: #1E1D18; }
/* --charge-bright, not --charge-ink: the rule two lines up gives this callout an
   OPAQUE DARK ground (#1E1D18) in BOTH themes, so the "-as-text-on-LIGHT" token
   was always the wrong one here. It only became load-bearing when --charge-ink
   was darkened for the light chip rule above — that change would otherwise have
   pushed this icon further down against its own near-black fill. */
.callout--charge .callout__icon { color: var(--charge-bright); }

/* Steps */
.steps { counter-reset: step; list-style: none; padding: 0; }
.steps li { position: relative; padding-left: 44px; margin: 18px 0; min-height: 30px; }
.steps li::before {
  counter-increment: step; content: counter(step);
  position: absolute; left: 0; top: -2px; width: 30px; height: 30px; border-radius: 50%;
  display: grid; place-items: center; background: var(--primary-dim); color: var(--primary);
  font-family: var(--font-mono); font-weight: 700; font-size: 13px;
}
/* THREE pages render .steps — /download (6 numerals) and /group-rides (5) are
   dark, /help (3) is light — so this split is live on real pages, not
   defensive. --primary-ink is "lime-as-text on LIGHT surfaces" (see the token
   block); it was the default here, on a bubble whose --primary-dim
   rgba(200,230,74,0.18) composites over the DARK ground to rgb(44,54,22) on
   /download's page background and rgb(51,64,28) on /group-rides' .card. That
   measures 1.859:1 and 1.608:1 against a 4.5:1 requirement — and worse in
   practice: /download's numerals sit under the section's .glow--lime bloom,
   which lifts the bubble progressively down the list, so the SAMPLED sixth
   numeral was 1.301:1. The numerals were effectively invisible.
   --primary measures 9.062:1 and 7.837:1 ANALYTICALLY on those two grounds,
   and 6.340:1 at /download's worst SAMPLED pixel (the sixth numeral, under the
   bloom). On /help's white .card that same --primary would be 1.323:1 — a NEW
   failure, worse than the one being fixed — which is why this is a split and
   not one colour, exactly as .eyebrow--lime / [data-theme="light"]
   .eyebrow--lime already does it. --primary-ink on that light bubble measures
   6.450:1 analytic / 6.427:1 sampled.
   Every figure here is ANALYTIC unless it says SAMPLED. Analytic means
   --primary-dim source-over the token ground with no gradient layer accounted
   for, which makes it ~30% OPTIMISTIC on /download (9.06 analytic vs 6.34
   sampled) — so build.test.mjs gates at >= 6:1, not 4.5:1, to leave the bloom
   room. It recomputes all of this from the tokens rather than quoting it.
   Figures assume the UNROUNDED composite; rounding to integer channels moves
   them by up to 0.03, so do not chase a third decimal. */
[data-theme="light"] .steps li::before { color: var(--primary-ink); }

/* FAQ accordion */
.faq { border-top: 1px solid var(--border); }
.faq__item { border-bottom: 1px solid var(--border); }
.faq__q { display: flex; justify-content: space-between; align-items: center; gap: 16px; cursor: pointer; padding: 20px 4px; font-family: var(--font-display); font-weight: 600; font-size: 18px; color: var(--ink); list-style: none; }
.faq__q::-webkit-details-marker { display: none; }
.faq__icon { width: 20px; height: 20px; flex-shrink: 0; color: var(--muted); transition: transform 0.2s; }
details[open] .faq__icon { transform: rotate(45deg); color: var(--primary-ink); }
.faq__a { padding: 0 4px 22px; color: var(--ink-2); font-size: 15.5px; line-height: 1.7; max-width: 760px; }

/* Helpful feedback */
.helpful { display: flex; align-items: center; gap: 14px; padding: 20px 24px; border: 1px solid var(--border); border-radius: var(--r-md); background: var(--surface-2); }
.helpful span { font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }

/* Support contact card */
.support-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-lg); padding: var(--s-6); text-align: center; box-shadow: var(--shadow-card); }

/* ----------------------------------------------------------------------------
   17 · UTILITIES
   ---------------------------------------------------------------------------- */
.text-center { text-align: center; }
.mx-auto { margin-inline: auto; }
.maxw-xs { max-width: 460px; } .maxw-sm { max-width: 560px; } .maxw-md { max-width: 680px; } .maxw-lg { max-width: 820px; }
.mt-0 { margin-top: 0; } .mt-2 { margin-top: var(--s-2); } .mt-3 { margin-top: var(--s-3); } .mt-4 { margin-top: var(--s-4); } .mt-5 { margin-top: var(--s-5); } .mt-6 { margin-top: var(--s-6); } .mt-7 { margin-top: var(--s-7); } .mt-8 { margin-top: var(--s-8); }
.mb-0 { margin-bottom: 0; } .mb-2 { margin-bottom: var(--s-2); } .mb-3 { margin-bottom: var(--s-3); } .mb-4 { margin-bottom: var(--s-4); } .mb-5 { margin-bottom: var(--s-5); } .mb-6 { margin-bottom: var(--s-6); }
.cols-2 { grid-template-columns: repeat(2,1fr); } .cols-3 { grid-template-columns: repeat(3,1fr); } .cols-4 { grid-template-columns: repeat(4,1fr); }
.gap-4 { gap: var(--s-4); } .gap-5 { gap: var(--s-5); } .gap-6 { gap: var(--s-6); }
.items-center { align-items: center; } .justify-between { justify-content: space-between; }
.hide { display: none; }

/* ----------------------------------------------------------------------------
   TABLET TIER — 721px to 1120px.
   Every display clamp here crosses to its MINIMUM well above 720px (4.6vw < 38px
   below 826px, 3.2vw < 28px below 875px, 2vw < 21px below 1050px, and 6.4vw does
   not climb back to the phone tier's 68px until 1063px), so the band just above
   the phone stop rendered at the desktop FLOOR: measured in headless Chrome, the
   hero went 68.00px at 720px to 46.14px at 721px, a 21.86px / 32% drop across one
   pixel. This is the phone-tier defect Plan 1 fixed, one tier up.
   1120 is an existing stop (site.css tightens the nav there), so no width is
   introduced, and 1121px up is byte-unchanged. This block must stay ABOVE the
   720px block below: both match at 390px and source order decides the tie.
   build.test.mjs pins the ordering and both seams' arithmetic; the per-token
   comments below carry the resulting steps.

   Two things a future edit will want to "simplify", and must not:
   (1) Each vw term sits ~0.05 above the round number the arithmetic suggests.
   9.4/6.1/4.7/3.3vw all land a hair BELOW the phone tier at 721px (-0.226 /
   -0.019 / -0.113 / -0.207), leaving the ladder non-monotonic at the very seam
   this tier exists to smooth.
   (2) h3 is the one step DOWN, and it is forced, not chosen: the desktop 2vw term
   is under the phone tier's 24px ceiling at EVERY width below 1200px, and the
   phone tier already reaches 24px at 515px — so some boundary in 721-1200 must
   step down, decided by the two pre-existing tiers, not by this one. Handing back
   at 1120 puts it where the desktop ramp is highest and the step smallest, and h3
   is fully recovered 80px later. (The old "2vw does not reach 24px until 1350px"
   was wrong twice: 2vw reaches 24px at 1200px; 1350px is h3's 27px MAXIMUM.)
   ---------------------------------------------------------------------------- */
@media (max-width: 1120px) {
  /* Bare custom properties inside a media query are invalid CSS and silently
     discarded — Plan 1 lost six --step-* declarations that way. Keep them in
     :root. Below, the first figure is the RAW vw term at 721px before clamping,
     and "pinned" is the first width in this tier's live range where the max
     applies. */
  :root {
    --step-hero: clamp(46px, 9.44vw, 68px);   /* 721: 68.06 raw -> 68, pinned from 721 (crossing 720.34); 1121 -> 71.74 (+3.74) */
    --step-h1:   clamp(38px, 6.11vw, 48px);   /* 721: 44.05 raw, under the max; pinned 48 from 786; 1121 -> 51.57 (+3.57) */
    --step-h2:   clamp(28px, 4.72vw, 35px);   /* 721: 34.03 raw, under the max; pinned 35 from 742; 1121 -> 35.87 (+0.87) */
    --step-h3:   clamp(21px, 3.34vw, 24px);   /* 721: 24.08 raw -> 24, pinned across the whole range (its 718.56 crossing falls in the phone tier); 1121 -> 22.42 (-1.58) */
  }

  /* Measured 161.03/146.66/146.66/146.66 at 721px: at the base rule's 34px floor
     "150,000+" in Space Mono has a 161.03px min-content against a 150.25px 1fr
     share, so the first column borrows width from its neighbours and the 4-up
     stops reading as a row — the 2x2 phone fix, scaled. Min-content here runs
     ~4.74px per px of font size, so ANY size above ~31.7px overflows the share;
     the phone tier's 48px is impossible in four columns, not merely large.
     Only the FLOOR moves; the 4vw term is the base rule's, untouched. Re-scaling
     it instead (3.4vw was proposed) would put 38.08px at 1120 against the base
     rule's 44.84px at 1121 — a NEW 6.76px cliff, the defect class this tier
     exists to remove. At 4vw: 44.80px at 1120 vs 44.84px at 1121, and the
     override is a no-op from 850px up, where 4vw passes the base 34px floor. */
  .edge-stat .stat-num { font-size: clamp(28px, 4vw, 48px); }
}

@media (max-width: 1000px) {
  /* minmax(0,1fr), not 1fr. `1fr` is minmax(AUTO, 1fr), and an `auto` track
     minimum is the item's min-content width — which includes the whitepaper's
     90-character <pre> at 772px. The track inflated to 720px inside a 354px
     viewport and the whole document scrolled sideways (measured scrollWidth
     790 at 390px), nav and footer dragging with it. The desktop rule at :563
     already carries this guard; the mobile override dropped it. */
  .docs { grid-template-columns: minmax(0, 1fr); }
  .docs__nav, .docs__toc { display: none; }
  .footer__grid { grid-template-columns: 1fr 1fr; }
  /* Same guard as .docs three lines up, and for the same reason — kept
     consistent with the 720px .cols-* rule so build.test.mjs can pin the whole
     class. A forward guard rather than a fix: `class="cols-4"` is emitted by no
     page today, so this rule renders nowhere and the edit changes no pixel. */
  .cols-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 720px) {
  /* PHONE TIER. Every clamp() in this stylesheet crosses to its minimum between
     622px and 1133px, so on a phone the fluid scale was pinned flat at the
     DESKTOP MINIMUM. These values are the phone tier proper: the vw term is
     re-scaled so it does real work between 320px and 720px. */
  :root {
    --step-hero:  clamp(46px, 13.2vw, 68px);   /* 390px -> 51.5, 430px -> 56.8 */
    --step-h1:    clamp(32px, 8.7vw, 44px);    /* 390px -> 33.9 */
    --step-h2:    clamp(25px, 6.9vw, 34px);    /* 390px -> 26.9 */
    --step-h3:    clamp(19px, 5.1vw, 24px);
    --step-lead:  16.5px;
    --step-micro: 12px;

    /* Spacing, re-based. Census re-derived 2026-07-30 (whole-branch review) —
       the previous note here ("all 32 consumers live in this file … so this
       moves the design system and nothing else") was stale in BOTH directions:
         * this file now holds 33, not 32. Task 9 added a --s-3 gap to
           .footer__bottom after the original count was taken.
         * "nothing else" was wrong. site.css does consume zero, but
           src/pages/spot.mjs's page-level <style> block spends the scale on
           .spot-stage__panel and .spot-stage__coords — 5 uses across 3 lines,
           of --s-3, --s-5 (x3) and --s-8 — and :root cascades to the whole
           document, so /spot IS re-based here through --s-5 and --s-8. That is
           intended, not collateral; the suite records it beside
           'no page pins a stat-band gap inline' in build.test.mjs.
       Re-derive rather than trusting these numbers, and STRIP COMMENTS first —
       this note deliberately writes the tokens bare (--s-3, not the var()
       spelling) so a naive grep does not count the prose as consumption, which
       is the same trap the census test at build.test.mjs guards with
       stripComments(). Sources to sweep: this file plus src/pages/*.mjs.
       --s-2..--s-4 stay: 8/12/16 are already phone-scale, and shrinking --s-3
       would pull .row / .row-wrap gaps below a comfortable thumb separation.
       --s-1, --s-9 and --s-10 stay because NOTHING consumes them anywhere —
       re-basing a dead token is theatre, not a fix. (--s-1 isn't "already
       phone-scale" like --s-2..--s-4 — it has zero consumers, full stop,
       same as --s-9/--s-10; T7 minor-round correction.) */
    --s-5: 18px;   /* 24 — .card/.cat-card padding, .grid + .stat-band gaps */
    --s-6: 24px;   /* 32 — .feature-card/.support-card padding, .footer__grid gap */
    --s-7: 32px;   /* 48 — .footer__bottom margin-top, .mt-7 */
    --s-8: 40px;   /* 64 — .footer top padding, .mt-8 */
  }
  /* Both tokens are now consumed. --step-lead feeds .lead, whose desktop value
     is byte-identical to the literal it replaced, so the change is -0.5px on
     phones only; the hero lead's contrast against the artwork was re-measured
     at its new position and still clears 4.5:1. --step-micro feeds the
     micro-label floor below plus its companion at the end of site.css: 18
     selectors between them, the 16 that rendered 287 instances of sub-12px text
     across 14 pages at 390px, plus .prose th and .ccard__dir, which render only
     on article pages and above 420px respectively. build.test.mjs
     derives the floor's selector list from the stylesheets' own sub-12px
     literals, so a new 10px label cannot slip past it. */

  /* MICRO-LABEL FLOOR. Measured 287 rendered instances of sub-12px text across
     14 pages at 390px; the smallest live HTML text on the site is 9px. Below
     12px, mono uppercase at 0.12-0.22em tracking stops being legible at arm's
     length.
     The floor lives here rather than replacing the desktop literals: converting
     them would raise desktop 9px -> 11px as a side effect, and desktop is out
     of scope. Going global later is moving this one rule out of the media query.
     Residual, stated deliberately: desktop still renders .ccard__best and
     .ccard__tag at 9px.
     .prose th is safe to raise even though it is `white-space: nowrap` and so
     widens each table's intrinsic floor: .prose table sits inside
     .prose-table, which is `overflow-x: auto`, so a wider header row scrolls
     inside its own wrapper and adds nothing to the document's min-content. */
  .stat-label,
  .qr__cap,
  .footer__col h2,
  .footer__bottom,
  .search kbd,
  .art-row__meta,
  .pill,
  .chip,
  .prose th { font-size: var(--step-micro); }

  /* The floor pays for itself in the nav bar. The "Get Flow" pill is
     white-space: nowrap, so 11px -> 12px widened it 124.6px -> 132.1px, and at
     320px the nav row was ALREADY 11.3px over its 284px content box, riding on
     the container's 18px right padding. The extra 7.5px pushed the hamburger's
     right edge to 320.8 and gave every page 1px of sideways document scroll.
     Trimming 6px of padding per side makes the row 12px shorter, so it ends at
     308.8 — 11.2px inside the viewport, against 6.7px before. Scoped to
     .nav__inner so the mobile panel's own pill is untouched.
     Measured, NOT fixed here: the row is still over-constrained at 320px, so
     nothing absorbs the 12px — .nav__spacer stays at 0 and the hamburger stays
     crushed at 20px, under the 44px tap floor. logo + pill + a full 44px toggle
     cannot share 284px at any padding, so that one predates this task and needs
     a design call, not a nudge. */
  .nav__inner .pill { padding-inline: 14px; }

  /* /contact's "Join the Flow Telegram (@flowriders)" pill measured 351.8px in
     a 284px content box and was white-space: nowrap, which put the page's
     documentElement.scrollWidth at 370 for BOTH a 320px and a 360px viewport —
     the largest sideways page scroll left on the site. Wrapping closes it at
     both widths (measured 320/320 and 360/360 after).
     line-height comes off 1 so a second line has room; min-height keeps the
     44px floor either way — a single-line .pill still lands on exactly 44.00px,
     because 12px x 1.25 plus 22px of padding is 37px, under the floor.
     TWO measured side effects, both disclosed rather than fixed:
       * .pill--lg grows 44 -> 45px on phones. Its padding is 14px, so
         15 + 28 + 2px of border is 45 — just PAST the 44px min-height. Above
         the floor, so harmless, but it is a real 1px change to the hero's
         "Download free" and both store buttons.
       * the nav pill's computed text-align goes start -> center below 720px.
         Inert TODAY, and only because .pill's base rule centres its content
         with justify-content: center — if that ever changes, the nav pill
         silently gains a centring nobody chose. text-align is what a WRAPPED
         label needs; it does nothing to a single-line one.

     The nav is exempt, and not out of caution. white-space is what sets a flex
     item's automatic minimum size — nowrap makes min-content equal max-content.
     The 320px nav row already overflows its own 284px content box by 6.81px
     (Task 6 bought 11.2px of VIEWPORT headroom by trimming the pill's padding;
     it did not create slack inside the row), so flex shrink is live. Measured
     both ways at 320px: with nowrap the pill holds 120.13px on one line and the
     row ends at 308.81; without it the pill collapses to 99.11px and 54px tall
     with "Get Flow" on a SECOND LINE inside the 68px bar, the row ending at
     302.00. At 360px it does not wrap either way (120.13px, one line), so 320px
     is the only width that shows it. Scoped to .nav, which also covers the
     mobile panel's copy of the pill — it has room, and this keeps it identical
     to what it renders today. */
  .pill {
    white-space: normal;
    text-align: center;
    line-height: 1.25;
  }
  .nav .pill { white-space: nowrap; }

  /* The nav's handover is NOT here. site.css's @media (max-width: 940px) hides
     .nav__links and shows .nav-mobile-toggle 220px earlier, because seven nav
     links collide with the Get Flow pill through the laptop range. A copy of
     that rule lived here and was fully shadowed. */
  /* minmax(0, 1fr), not 1fr — the same guard the .docs mobile override carries
     in the 1000px block above, for the same reason: `1fr` is minmax(AUTO, 1fr),
     and an `auto` track's MINIMUM is the item's min-content width. On /help.html
     the second column holds a card whose `white-space: nowrap` pill measures
     276.6px; plus 2x28 card padding and borders that is a 334.6px min-content,
     so the single 1fr track resolved to 334.625px inside a 284px grid box and
     dragged the whole document sideways (measured scrollWidth 353 at 320px).
     The floor raising that pill 11px -> 12px is what surfaced it; the track has
     been the actual cause since the rule was written. House pattern, not a local
     hack: minmax(0, ...) lets an over-wide item overflow its own box — where a
     scroller or a wrap can deal with it — instead of inflating the track and
     taking the nav and footer with it. */
  .cols-2, .cols-3, .cols-4 { grid-template-columns: minmax(0, 1fr); }
  .stat-band { grid-template-columns: 1fr 1fr; }
  .container { padding-inline: 18px; }

  /* FOUR filled cells — the logo block plus three .footer__col groups, not the
     five the desktop track list implies.
     Two tracks with the intro cell spanning both, rather than a full stack. The
     1000px block takes the grid to two columns and nothing took it further; the
     ONE thing actually wrong with that at phone widths was the intro cell, whose
     blurb had to fit a 130px track at 320px and ran to SEVEN lines. Spanning it
     gives that cell the full 284px and the blurb renders THREE lines — identical
     to a full stack — while the three link columns still pair up.
     Measured .footer offsetHeight at 320 / 360 / 390 / 430px:
       two columns, no span    744 /  725 /  725 /  725   blurb 7 lines at 320
       THIS RULE               921 /  902 /  902 /  880   blurb 3 lines
       full single column     1122 / 1103 / 1103 / 1081   blurb 3 lines
     The span costs +177px against the cramped two-column tier and saves 201px
     against the stack, for the same three-line blurb.
     What this deliberately does NOT fix: at 320px ONLY, "Telegram community"
     still wraps to two lines in a 130px track, making that one link of 13 be
     54.8px tall. That is ABOVE the 44px tap floor, not below it — a floor is not
     a ceiling — so it is a cosmetic wrap, not a gate failure, and it is not worth
     201px of extra scroll on every page of the site to remove. Every footer link
     measures >= 44px at all seven swept widths, and the wrap is gone by 360px.
     minmax(0, ...) rather than bare 1fr on the tracks, matching .docs and
     .cols-* above: `1fr` is minmax(AUTO, 1fr) and an auto track's minimum is the
     item's min-content width, which is how each of those two dragged the whole
     document sideways earlier on this branch. Measured clean either way today —
     the guard is for the next label, not this one.
     Desktop is untouched: .footer is 760/760/506px at 740/900/1200 before and
     after, grid-column reverts to auto/auto above 720px, and the 1000px block
     still owns the 721-1000 band. */
  .footer__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* The intro cell is .footer__grid's first child (layout.mjs emits the logo
     block, then FOOTER.map's three .footer__col divs). build.test.mjs pins that
     ordering against the built HTML, because this selector is positional and a
     reordered footer would silently span the wrong cell. */
  .footer__grid > :first-child { grid-column: 1 / -1; }

  /* align-items: center put the checkmark 27.03px below the first line of text
     that had wrapped to four lines in a 284px column (/navigation.html at
     320px; /charging 26.84px over four lines, /group-rides 16.44px over three).
     Reaches all 59 bare .row elements: the six on /how-it-works that carried an
     inline align-items:center — which no stylesheet rule can override — had it
     removed, and it was a desktop no-op because the base rule says center too. */
  .row { align-items: flex-start; }

  /* The 4vw term in .edge-stat .stat-num's desktop clamp() dies at 850px, so
     below that the numeral sits flat at its 34px floor on every phone. A
     figure like "150,000+" in Space Mono has a 161px min-content width at
     34px, which the 1fr stat-band column can't hold below ~382px — the
     neighbouring column donates it width and the grid stops reading as a
     2x2 square. Re-basing the vw term here keeps the numeral inside half
     the column from 320px up. */
  .edge-stat .stat-num { font-size: clamp(26px, 7.5vw, 48px); }

  /* A line break that only exists where the line has room for it. Display
     headings carried hand-balanced desktop breaks that overflowed the 354px
     phone column and re-ragged into four lines with one-word orphans. */
  .brk { display: none; }
}
