/**
 * Global base styles loaded on every page.
 * Uses CSS custom properties defined in this file for all colors and spacing.
 * Follows CUBE CSS methodology: Composition, Utility, Block, Exception.
 *
 * Page-specific styles belong in page CSS files (e.g., home.css), not here.
 * Add styles here only when a pattern is shared across two or more pages.
 *
 * Semi-transparent colors — mostly tokenized (defined in :root below); call sites use var():
 *   Translucent borders → --border-hairline (ink) / --border-hairline-light (cream) / --border-strong-light
 *   Shadows             → --shadow-1…6 (ink elevation ladder) / --shadow-image{,-hover} (black media depth)
 *   Brand tints         → --tint-primary-{faint,soft,edge,glow} (B9);  focus rings → --focus
 *   Surface backdrops   → color-mix(var(--color-bg-dark) …%, transparent)
 *   Remaining raw literals (deliberately NOT tokenized — one-offs / non-anchor):
 *     #00000099 footer overlay ~60% ; #ffffff1a hairline on dark ~10% (pure white, not cream)
 *
 * Responsive Breakpoints (Desktop-First):
 * - Desktop: 1025px+ (base styles, no media query)
 * - Laptop:  max-width: 1024px
 * - Tablet:  max-width: 768px
 * - Mobile:  max-width: 480px
 */

/* ==========================================================================
   Font Declarations
   ========================================================================== */

/* FONT DM Sans - latin - variable 400-800
   Google's latin subset ships the full 100-1000 wght axis; the file here has the
   axis pinned to 400-800 with fontTools' partial instancer
   (py -3.12 -m fontTools.varLib.instancer -o DMSans-normal-400-800.woff2 <google-file> wght=400:800)
   so the name tells the truth and the file is a third smaller. Nothing on the
   site asks for a weight outside that range (01-prd-marketing-restyle.md FR 1). */
@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('../fonts/DMSans-normal-400-800.woff2') format('woff2');
}

/* FONT DM Serif Display - latin */
@font-face {
  font-family: 'DM Serif Display';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/DMSerifDisplay-normal-400.woff2') format('woff2');
}

/* FONT Courier Prime - latin */
@font-face {
  font-family: 'Courier Prime';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/CourierPrime-normal-400.woff2') format('woff2');
}

/* FONT Unbounded - latin - wordmark subset, variable 600-800
   Glyph-limited to the wordmark as rendered ("BIG BANG VISIBILITY": space and
   A B G I L N S T V Y) - 2.4 KB instead of ~60 KB. The unicode-range below is
   the same eleven codepoints, so no other character can ever resolve to this
   face: anything outside it falls through to the sans fallback rather than
   rendering as a missing glyph. Regenerate from google/fonts ofl/unbounded
   Unbounded[wght].ttf (v1.701; wght axis 200-900) in two steps:
     py -3.12 -m fontTools.varLib.instancer -o Unbounded-600-800.ttf "Unbounded[wght].ttf" wght=600:800
     py -3.12 -m fontTools.subset Unbounded-600-800.ttf --text="BIG ANVSLTY " --flavor=woff2 --layout-features='*' --output-file=assets/fonts/Unbounded-normal-600-800.woff2
   (01-prd-marketing-restyle.md FR 2) */
@font-face {
  font-family: 'Unbounded';
  font-style: normal;
  font-weight: 600 800;
  font-display: swap;
  src: url('../fonts/Unbounded-normal-600-800.woff2') format('woff2');
  unicode-range: U+0020, U+0041-0042, U+0047, U+0049, U+004C, U+004E, U+0053-0054, U+0056, U+0059;
}

/* ==========================================================================
   CSS Custom Properties
   ========================================================================== */

:root {
  /* Color Palette — two-tier model (standard: css-conventions.md § Theming)
   *
   * Tier 1 — PRIMITIVES (raw values; never consumed directly by components):
   *   brand hues (--color-primary/-secondary/-tertiary), the 4-theme surface
   *   ladders (-light/-cool/-dark/-navy), text primitives, and the link
   *   primitives (light/ink/navy rest+hover pairs, defined in Text Colors below).
   * Tier 2 — ROLE TOKENS (what components consume): surfaces
   *   (--surface-canvas/recessed/raised, in § Elevation & Surfaces below), the
   *   SIX foreground roles (--text, --text-muted, --link, --link-hover,
   *   --text-accent, --focus), and the CONTROL roles for buttons/form fields
   *   (--field-*, --select-*, --btn-*). All are minted here at :root with warm
   *   (light) defaults and remapped per theme context (§ Theme contexts). Pick a
   *   theme → every role resolves → WCAG AA holds (verified by
   *   tools/scripts/contrast.js, which models the surface × foreground matrix
   *   AND the control-role pairs, incl. composited translucent inks).
   *
   * The 4 themes: warm (default / .section--light), cool (.section--soft), ink
   * (.section--dark), navy (.section--navy). */
  --color-primary: #ff6a00;
  --color-primary-dark: #ad4500; /* Dark-orange accent — the single value for orange-as-TEXT on light surfaces (eyebrows, link rest, admin sort, the light-family --btn-outline-ink and --select-chevron) AND button hover/active fills (light-family --btn-fill-* / --btn-outline-hover-bg). ≥4.80:1 AA on every light/cool Canvas/Raised; link HOVER is the darker #8f3a00. Darkened from #c65100 (regressed to ≤4.20:1 after the warm-canvas repaint). */
  --color-primary-light: #ff8533;
  --color-secondary: #00702d;
  --color-secondary-dark: #005623;
  --color-tertiary: #0375b4; /* Azure graphics accent (repurposed in task 6.0; the navy theme now paints from --color-bg-navy, not this token). */
  --color-neutral: #faf9f5;
  --color-white: #ffffff;
  --color-black-1: #4d4545;
  --color-black-10: #383131;
  --color-black-40: #252524;
  --color-black-70: #1a1a19;
  --color-black-100: #000000;

  /* Brand tint scale (B9) — translucent --color-primary rungs for decorative fills,
   * hover tints, and glow shadows. color-mix() keeps every rung derived from the one
   * brand hue (re-pick the hue once → all rungs follow; no stray alpha hexes to desync).
   * Theme-invariant: composited over whatever surface they sit on, so defined once here
   * and never remapped. Standard: css-conventions.md § Semi-Transparent Colors. */
  --tint-primary-faint: color-mix(in srgb, var(--color-primary) 5%, transparent);
  --tint-primary-soft:  color-mix(in srgb, var(--color-primary) 10%, transparent);
  --tint-primary-edge:  color-mix(in srgb, var(--color-primary) 30%, transparent);
  --tint-primary-glow:  color-mix(in srgb, var(--color-primary) 40%, transparent);

  /* Decorative tokens — texture and ornament only, NEVER an ink: nothing here is
   * ever text, a border that carries meaning, or a surface text sits on, so none
   * of these carry a contrast-pair obligation and none appear in contrast.js's
   * palette. Theme-invariant (composited over whatever surface they sit on).
   * Promoted from the founding-client elevation and the restyle prototype
   * (01-prd-marketing-restyle.md FR 3). */
  /* The dawn hero's sun core (Reserved, home only). Deliberately its own token and
     not --color-warning-main (#ffed29): semantic state tokens stay uncoupled from
     decoration even where hues neighbour, and this gold is warmed toward the brand
     orange so it reads sunrise, not hazard. */
  --color-sun-gold: #ffc31f;
  /* Starscape tile — a sparse static field of ffffeb points, tiled on ink sections
     (.starscape) at roughly the founding hero's density. 260px period. */
  --stars: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='260' height='260'%3E%3Cg fill='%23ffffeb'%3E%3Ccircle cx='13' cy='37' r='1' opacity='.3'/%3E%3Ccircle cx='89' cy='12' r='.7' opacity='.22'/%3E%3Ccircle cx='160' cy='58' r='1.2' opacity='.34'/%3E%3Ccircle cx='210' cy='140' r='.8' opacity='.26'/%3E%3Ccircle cx='60' cy='180' r='1' opacity='.3'/%3E%3Ccircle cx='140' cy='222' r='.6' opacity='.2'/%3E%3Ccircle cx='236' cy='236' r='1' opacity='.18'/%3E%3Ccircle cx='30' cy='110' r='.7' opacity='.24'/%3E%3C/g%3E%3C/svg%3E");
  /* Denser, brighter star field for the dawn hero only (Reserved) — twelve points
     per 340px period against --stars' eight per 260px, so the night sky above the
     sunrise reads fuller than a section backdrop. */
  --stars-hero: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='340' height='340'%3E%3Cg fill='%23ffffeb'%3E%3Ccircle cx='22' cy='30' r='1.3' opacity='.5'/%3E%3Ccircle cx='120' cy='14' r='.8' opacity='.3'/%3E%3Ccircle cx='210' cy='52' r='1.5' opacity='.55'/%3E%3Ccircle cx='300' cy='120' r='1' opacity='.35'/%3E%3Ccircle cx='70' cy='150' r='1.2' opacity='.45'/%3E%3Ccircle cx='170' cy='200' r='.7' opacity='.28'/%3E%3Ccircle cx='300' cy='300' r='1.3' opacity='.4'/%3E%3Ccircle cx='40' cy='250' r='.9' opacity='.3'/%3E%3Ccircle cx='250' cy='260' r='.6' opacity='.25'/%3E%3Ccircle cx='140' cy='310' r='1.1' opacity='.4'/%3E%3Ccircle cx='330' cy='30' r='.7' opacity='.3'/%3E%3Ccircle cx='95' cy='90' r='.6' opacity='.22'/%3E%3C/g%3E%3C/svg%3E");
  /* Terrain mottling for the dawn hero's planet limb (Reserved) — a desaturated
     fractal-noise tile (500px, stitched) that the hero soft-light-blends and masks
     to the sun-grazed crest so the texture dissolves into the night side. */
  --noise-terrain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='500' height='500'%3E%3Cfilter id='t'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.04' numOctaves='4' seed='11' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='500' height='500' filter='url(%23t)' opacity='.5'/%3E%3C/svg%3E");

  /* Translucent border tokens — hairline/field borders derived from the text
   * anchors via color-mix (same rationale as the tint scale: one source hue, no
   * stray alpha hexes to desync). These primitives are NOT theme-remapped
   * themselves: the hairlines are call-site-picked to match the surface
   * (ink-derived on light, cream-derived -light on dark), while
   * --border-strong-light is normally reached through the theme-remapped
   * --field-border control role (its dark/navy value) — direct use is the
   * exception (admin list toolbar). Standard: css-conventions.md
   * § Semi-Transparent Colors. Hairline opacities normalize the prior
   * approximate hexes (0x1a≈10%). */
  --border-hairline:       color-mix(in srgb, var(--color-text-dark) 10%, transparent);  /* hairline border/bg on light surfaces (was #1a1b1c1a) */
  --border-hairline-light: color-mix(in srgb, var(--color-text-light) 10%, transparent); /* hairline border/bg on dark surfaces (was #ffffeb1a) */
  --border-strong-light:   color-mix(in srgb, var(--color-text-light) 55%, transparent); /* heavier form-field border on dark surfaces. 55% is the floor for WCAG 1.4.11 (3:1 non-text) against EVERY dark-family adjacency — worst case 3.30:1 composited over the navy well vs navy raised (contrast.js); the old 30% (#ffffeb4d) bottomed out at 1.61:1. */

  /* Elevation & Surfaces — 4-level stacking system, 4 themes
   * (standard: css-conventions.md § Surfaces & Elevation)
   *
   * Four stacking levels, four themes (warm `-light`, cool `-cool`, ink
   * `-dark`, navy `-navy`). Within a theme, luminance tracks elevation: Canvas is the
   * darkest / most-tinted floor (and the largest area), each level above it
   * steps lighter, up to the reserved Floating level.
   *
   *   Floating  (reserved)         — popovers / menus that float above cards
   *   Raised    --color-surface-*  — cards, panels, modals sitting ON the canvas
   *   Recessed  --color-recessed-* — controls cut INTO a surface (form wells)
   *   Canvas    --color-bg-*       — the page / section floor
   *
   * INVARIANT: Recessed is always darker than Raised in every theme, so a field
   * nested in a card reads as inset. (Verified text contrast: dark text on the
   * light/cool wells ≥ 12:1; light text on the dark well = 12:1.)
   *
   * Components reference the THEME-NEUTRAL role tokens (--surface-canvas /
   * --surface-recessed / --surface-raised) defined at the end of this block,
   * NEVER the raw -light/-cool/-dark palette tokens directly. The role tokens
   * remap per theme context (see .section--dark / .section--soft / .surface--*),
   * so one declaration is correct in every theme.
   */

  /* Canvas — the page/section floor (largest area, most tinted) */
  --color-bg-light: #ffe7cc;
  --color-bg-cool:  #dcedef;
  --color-bg-dark:  #1f1a15; /* warm ink — slight brown bias harmonizes the canvas with the already-warm recessed/raised ink surfaces and follows the "canvas = most-tinted floor" rule (was #1a1b1c). Distinct from --color-text-dark, which stays #1a1b1c. */
  --color-bg-navy:  #044662; /* navy theme canvas (cream text 10.09:1, muted 7.07:1) */

  /* Recessed — wells cut into a surface (form inputs) */
  --color-recessed-light: #ffeedb;
  --color-recessed-cool:  #eef6f7;
  --color-recessed-dark:  #34312e;
  --color-recessed-navy:  #0d4f6f; /* navy recessed (cream text 8.78:1) */

  /* Raised — cards, panels, modals on the canvas */
  --color-surface-light: #fff3e6;
  --color-surface-cool:  #f5f9fa;
  --color-surface-dark:  #44403c;
  --color-surface-dark-hover: #44403ce6;
  --color-surface-navy:  #10597d; /* navy raised — the binding surface (cream 7.56:1, muted 5.30:1, link/accent 4.89:1) */

  /* Colored surfaces — brand-green accents, outside the neutral elevation ladder */
  --color-surface-secondary:       #005623;
  --color-surface-secondary-hover: #007323;

  /* Role tokens — what components consume. Default to the warm light theme;
   * theme-context classes remap them (see § Theme contexts below). */
  --surface-canvas:   var(--color-bg-light);
  --surface-recessed: var(--color-recessed-light);
  --surface-raised:   var(--color-surface-light);

  /* Text Colors */
  --color-text-light: #ffffeb;
  --color-text-dark: #1a1b1c;
  /* -muted darkened from #6b6b6b → #5f5f5f so muted text clears WCAG AA (4.5:1)
   * on the tinted Canvas tokens (e.g. 5.35:1 on --color-bg-light #ffe7cc; the old
   * #6b6b6b was 4.46:1 on it). -muted-dark is for dark backgrounds, unaffected. */
  --color-text-muted: #5f5f5f;
  /* Placeholder ink for the light families (12.1 verdict 2026-07-09): the
   * pre-darkening muted gray, retired from muted text when it fell below AA on
   * the warm canvas — placeholders render only in the lighter WELLS, where it
   * clears 4.5:1 (4.70 warm / 4.86 cool, contrast.js) while still reading
   * dimmer than typed text. Ships at full opacity, like the dark families'
   * (the old #5f5f5f @ 0.6 composited to ≈2.5:1 in the wells). */
  --color-text-placeholder: #6b6b6b;
  /* Unified dark-family muted (A4) — serves BOTH ink and navy. Raised from
   * #c8c8b0 → #d8d8c5 so it clears AA on navy raised (#c8c8b0 = 4.49:1 there,
   * effectively a fail); the lighter value is also contrast-positive on ink
   * (raised 6.04 → 7.12). Consequence: ink/portal muted text shifts a touch
   * lighter — intended. Clears every ink (11.95/8.95/7.12) and navy (7.07/6.15/5.30) surface. */
  --color-text-muted-dark: #d8d8c5;

  /* Link primitives (Tier 1) — per-family link rest/hover values, consumed ONLY
   * by the --link / --link-hover / --text-accent / --focus role tokens (Tier 2),
   * never directly by components. Verified AA on every surface incl. raised
   * (tools/scripts/contrast.js). Three families:
   *   Light (warm + cool): rest = --color-primary-dark (#ad4500, above — shared with
   *     eyebrow/button-hover); hover = #8f3a00.
   *   Ink:  rest #ffa366; hover #ffc299.
   *   Navy: rest reuses ink hover #ffc299; hover #ffd6b8. */
  --color-link-light-hover: #8f3a00; /* light/cool link hover (6.28–6.33:1) */
  --color-link-dark:        #ffa366; /* ink link rest (5.23–8.77:1); also ink + navy --focus */
  --color-link-dark-hover:  #ffc299; /* ink link hover; ALSO ink + navy --text-accent AND navy link rest — one hex, several roles, deliberately shared */
  --color-link-navy-hover:  #ffd6b8; /* navy link hover (5.67–7.56:1) — net-new primitive */

  /* Foreground role tokens (Tier 2) — what text/link/label components consume.
   * Defaults = the WARM (light) family; each theme context remaps all six (see
   * § Theme contexts below). Every role consumes a Tier-1 primitive by var(),
   * never a literal hex. Pick a theme → these resolve → AA holds (contrast.js). */
  --text:        var(--color-text-dark);        /* body text */
  --text-muted:  var(--color-text-muted);       /* secondary / muted text */
  --link:        var(--color-primary-dark);     /* inline link rest */
  --link-hover:  var(--color-link-light-hover); /* inline link hover */
  --text-accent: var(--color-primary-dark);     /* orange labels / eyebrow */
  --focus:       var(--color-primary-dark);     /* focus ring / UI accent */

  /* Control role tokens (Tier 2) — buttons and form controls. These retire the
   * former B5 carve-out: controls consume roles on their BASE rules, so they
   * theme by cascade like everything else — no `.section--X .btn--*` descendant
   * rules, which leaked dark-family values into light islands (the 2026-07-03
   * invisible-button/textarea bugs). Defaults = warm (light) family; each theme
   * context remaps the full set. `--select-chevron` / `--select-check` hold
   * complete url() images (a data URI can't interpolate a var(), but a custom
   * property can carry the whole image). */
  --field-border:              var(--color-text-muted);      /* input/select/radio border */
  --field-placeholder:         var(--color-text-placeholder); /* placeholder ink */
  --field-placeholder-opacity: 1;                             /* BOTH families ship pre-dimmed ink at 1 (12.1 verdict 2026-07-09) */
  --select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cpath fill='%23ad4500' d='M4 6l4 4 4-4z'/%3E%3C/svg%3E");
  --select-check:   url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='none' stroke='%23008009' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' d='M4 10l4 4 8-8'/%3E%3C/svg%3E");
  --btn-outline-ink:            var(--color-primary-dark);   /* .btn--secondary rest label + 2px outline (5.30:1 on raised light) */
  --btn-outline-rest-opacity:   1;                           /* dark families dim rest to 0.75 for hierarchy */
  --btn-outline-hover-bg:       var(--color-primary-dark);   /* .btn--secondary hover/active fill */
  --btn-outline-hover-ink:      var(--color-white);          /* label on that fill (5.79:1) */
  --btn-outline-active-opacity: 1;                           /* dark families press to 0.9 */
  --btn-fill-hover-bg:  var(--color-primary-dark);           /* .btn--primary hover/active fill */
  --btn-fill-hover-ink: var(--color-text-light);             /* label on that fill (5.72:1) */
  --btn-fill-active-bg: var(--color-primary-dark);
  --badge-outline-ink:  var(--color-info-dark);              /* .status-badge--outline ink + border (transparent backdrop; tokenized 2026-07-10) */

  /* State Colors — 3-tier tonal system
   * (standard: css-conventions.md § State Colors)
   *
   * Each state has three tiers: -dark (foreground / text on light card),
   * -main (non-text graphics ONLY — icons, chart fills, decorative accents),
   * -light (background / tinted card). The -dark and -light pair clears WCAG 2
   * AA contrast (≥4.5:1) for normal text, regardless of what surface sits
   * underneath — backgrounds are solid hex, not alpha-blended, so they render
   * predictably on any page.
   *
   * THE TINTED RECIPE (the one rule for state-colored components — badges,
   * flashes, chips, inline validation):
   *   -light background + -dark text + -dark border/accent
   * The contrast guarantee lives in the token pair, so every component that
   * follows the recipe inherits AA for free. Never use -main as a text
   * background: warning-main (#ffed29) structurally cannot carry light text
   * (~1.1:1), so filled-with-light-text can never be a uniform rule.
   *
   * Verified pairs (WebAIM):
   *   success: #008009 on #ebf3eb = 4.53:1
   *   error:   #d12424 on #fbeaea = 4.52:1
   *   warning: #797800 on #faffd9 = 4.52:1
   *   info:    #1565c0 on #e3f2fd = 5.03:1
   *
   * Rule of thumb: state colors reference -dark unless styling the inside of a
   * tinted card (where -light is the background). (Focus is NOT a state color:
   * it is the per-family --focus role token, remapped per theme — see § Theme
   * contexts. The old untiered --color-focus #ff6a00 failed 3:1 on light surfaces.)
   */
  --color-success-dark: #008009;
  --color-success-main: #388e3c;
  --color-success-light: #ebf3eb;
  --color-error-dark:   #d12424;
  --color-error-main:   #d32f2f;
  --color-error-light:  #fbeaea;
  --color-warning-dark: #797800;
  --color-warning-main: #ffed29;
  --color-warning-light: #faffd9;
  --color-info-dark:    #1565c0;
  --color-info-main:    #2196f3;
  --color-info-light:   #e3f2fd;

  /* Shadows — whole-shadow tokens. ALWAYS use var(--shadow-*) in box-shadow
   * declarations rather than hardcoding raw values, so depth stays consistent
   * and a global tweak is one line here. Standard: css-conventions.md § Semi-Transparent Colors.
   *
   * Two families:
   *  • Ink elevation ladder (--shadow-1…6) — cards/panels on LIGHT surfaces.
   *    Ink-tinted via color-mix on the text anchor; ordered 1 (resting) → 6 (highest
   *    lift). Geometry preserved verbatim from the prior inline shadows. Rungs 2–5 are
   *    near-duplicate lifts inherited from per-component drift, kept to avoid visual
   *    regression — candidates for a future consolidation to ~3 rungs.
   *  • Image depth (--shadow-image / -hover) — media on DARK / photographic surfaces.
   *    Heavier and black-tinted (not anchor-derived: pure black reads on any backdrop). */
  --shadow-1: 0 2px 8px   color-mix(in srgb, var(--color-text-dark) 6%, transparent);  /* resting card (was 0 2px 8px #1a1b1c0f) */
  --shadow-2: 0 4px 12px  color-mix(in srgb, var(--color-text-dark) 8%, transparent);  /* subtle hover depth (was 0 4px 12px #1a1b1c14) */
  --shadow-3: 0 4px 16px  color-mix(in srgb, var(--color-text-dark) 12%, transparent); /* floating menu/panel (was 0 4px 16px #1a1b1c1f) */
  --shadow-4: 0 6px 16px  color-mix(in srgb, var(--color-text-dark) 12%, transparent); /* active raised (was 0 6px 16px #1a1b1c1f) */
  --shadow-5: 0 8px 24px  color-mix(in srgb, var(--color-text-dark) 12%, transparent); /* card hover depth (was 0 8px 24px #1a1b1c1f) */
  --shadow-6: 0 12px 32px color-mix(in srgb, var(--color-text-dark) 12%, transparent); /* large depth (was 0 12px 32px #1a1b1c1f) */
  --shadow-image:       0 8px 24px  #0000004d; /* gallery/featured image, resting — black ~30% */
  --shadow-image-hover: 0 16px 40px #00000066; /* gallery/featured image, hover depth — black ~40% (was --shadow-card-hover) */

  /* Spacing Scale */
  --space-xs: 0.5rem;
  --space-sm: 1rem;
  --space-md: 2rem;
  --space-lg: 3rem;
  --space-xl: 4rem;
  --space-2xl: 6rem;
  --space-3xl: 10rem;
  --space-4xl: 16rem;
  --space-5xl: 24rem;

  /* Section Padding */
  --section-padding-desktop: 10rem;
  --section-padding-laptop: 8rem;
  --section-padding-tablet: 6rem;
  --section-padding-mobile: 4rem;

  /* Typography Scale */
  --font-size-micro: 1.2rem;
  --font-size-tiny: 1.5rem;
  --font-size-small: 1.7rem;
  --font-size-base: 1.8rem;
  --font-size-medium: 1.9rem;
  --font-size-large: 2rem;
  --font-size-xl: 2.2rem;
  --font-size-2xl: 2.5rem;
  --font-size-3xl: 3rem;
  --font-size-4xl: 3.5rem;
  --font-size-5xl: 4rem;
  --font-size-6xl: 5rem;
  --font-size-7xl: 6rem;
  --font-size-8xl: 8rem;

  /* Font Families */
  /* DM Sans Variable (400–800) — workhorse: body text, nav, buttons, UI, and most headings; DM Serif Display's designed sibling */
  --font-primary: 'DM Sans', sans-serif;
  /* DM Serif Display — accent: section headings, column/block labels, editorial callouts; use sparingly for visual contrast */
  --font-accent: 'DM Serif Display', serif;
  /* Courier Prime — the typewriter "honesty voice": meta lines, prices, deadline metas, decorative typographic accents */
  --font-typewriter: 'Courier Prime', monospace;
  /* Unbounded — the wordmark only. The face is a glyph-limited subset (the letters of BIG BANG VISIBILITY); never running text */
  --font-wordmark: 'Unbounded', sans-serif;
  /* Wordmark weights — a deliberate split, not an inconsistency (optical sizing): heavy strokes need room,
     so display size (footer) takes 800 and small chrome (nav bar, overlay) takes 600 */
  --font-weight-wordmark-chrome: 600;
  --font-weight-wordmark-display: 800;
}

/* ==========================================================================
   Base & Reset Styles
   ========================================================================== */

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

html {
  font-size: 62.5%;
  scroll-behavior: smooth;
}

body {
  font-family: var(--font-primary);
  font-size: var(--font-size-base);
  font-weight: 400;
  line-height: 1.5;
  color: var(--color-black-1);
  background-color: var(--color-bg-light);
}

strong,
b {
  font-weight: 700;
}

/* ==========================================================================
   Composition — Layout Structure
   ========================================================================== */

/* ========== Section System — Layout, Themes, Typography ========== */

/* Skip to main content link — hidden by default, visible on keyboard focus */
.skip-link {
  position: absolute;
  top: -100px;
  left: 50%;
  transform: translateX(-50%);
  background: var(--color-bg-dark);
  color: var(--color-text-light);
  --focus: var(--color-link-dark); /* dark chrome outside the theme system — dark-family focus ring (B4) */
  padding: var(--space-sm) var(--space-md);
  font-size: var(--font-size-base);
  font-weight: 600;
  text-decoration: none;
  border-radius: 0 0 0.4rem 0.4rem;
  z-index: 1000;
  transition: top 200ms ease-in-out;
}

.skip-link:focus {
  top: 0;
  outline: 3px solid var(--focus);
  outline-offset: 3px;
}

/* Screen-reader-only content — visually hidden, available to assistive tech.
   Used for SR-announcement discriminators (timestamps that force a live-region
   re-announce on identical content) and offscreen labels. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Shared SVG filter defs (includes/svg-defs.php) — the wavy-divider filters
   .wave references by id. Must be in the DOM to be url(#…)-addressable, must
   never take space: out of flow and zero-size, on the class rather than an
   inline style the CSP's style-src would block. Promoted from
   founding-client.css .fc-svg-defs (01-prd-marketing-restyle.md FR 24). */
.svg-defs {
  position: absolute;
  width: 0;
  height: 0;
}

/* Wavy section divider — a .wave section paints its own canvas colour over the
   bottom of the section above it, displaced into a wavy edge by the SVG
   filters in includes/svg-defs.php (#wave-a seed 7 / #wave-b seed 13). Two
   layers: a solid wave in front, a translucent offset wave behind it — a
   layered horizon. The filters displace by up to ~11px, so both layers
   overhang the viewport 2% each side to keep displaced edges from opening
   gaps. Colour consumes the theme-resolved role token (--surface-canvas):
   inside .section--light it resolves to the warm canvas, inside .section--dark
   to ink; the --light/--dark modifiers carry only what genuinely differs
   between themes (the back wave's translucency). Promoted from the founding
   page (01-prd-marketing-restyle.md FR 8); the page emitting it must include
   svg-defs.php, or the filter reference resolves to nothing and the edge is
   straight. */
.wave {
  position: relative;
}

.wave::before {
  content: "";
  position: absolute;
  top: -2.2rem;
  left: -2%;
  width: 104%;
  height: 5rem;
  z-index: 1;
  background-color: var(--surface-canvas);
  filter: url(#wave-a);
  /* Own compositing layer (restyle 17.3, 2026-09-03): iOS Safari painted a
     small black square at the page's top-left corner on exactly the pages
     that carry .wave — home and about, on production too — the known WebKit
     artefact of an SVG displacement filter applied to HTML content through
     filter: url(); hiding or moving the defs element changed nothing, so the
     source is this filtered layer. Promoting it to its own layer is the
     commonly reported cure; harmless elsewhere (a 5rem strip). */
  transform: translateZ(0);
  pointer-events: none;
}

.wave::after {
  content: "";
  position: absolute;
  top: -3.4rem;
  left: -2%;
  width: 104%;
  height: 5rem;
  z-index: 0;
  filter: url(#wave-b);
  transform: translateZ(0); /* see ::before */
  pointer-events: none;
}

.wave--light::after {
  background-color: color-mix(in srgb, var(--surface-canvas) 35%, transparent);
}

.wave--dark::after {
  background-color: color-mix(in srgb, var(--surface-canvas) 30%, transparent);
}

/* Starscape — the cosmos continues past the hero: a sparse static star tile
   (--stars) on ink sections. Texture by surface rule: every ink .section--dark
   carries it; navy, the raised dark surface (.section--dark-alt) and
   photographic surfaces are other surface types and do not (design.md
   § Treatment Assignments). Promoted from the founding page
   (01-prd-marketing-restyle.md FR 9). */
.starscape {
  background-image: var(--stars);
}

/* The wave layers' 2% overhang must be clipped by an ancestor or it widens the
   page into horizontal scroll. main carries overflow-x: clip — clip, not
   hidden: hidden would turn main into a scroll container and swallow the
   waves' vertical overhang into the section above. Sitewide because .wave is
   baseline; on a page with no .wave it clips nothing (no page relies on
   content escaping main horizontally — 7.1's full sweep is the check). */
main {
  overflow-x: clip;
}

/* Base section — vertical padding for generous whitespace */
.section {
  padding-top: var(--section-padding-desktop);
  padding-bottom: var(--section-padding-desktop);
}

/* Section container — constrains content width and centers */
.section__container {
  max-width: 1200px;
  margin-left: auto;
  margin-right: auto;
  padding-left: var(--space-md);
  padding-right: var(--space-md);
}

/* Centered modifier — text-aligns all content within the container */
.section__container--centered {
  text-align: center;
}

/* ==========================================================================
   Theme contexts (B1) — the heart of the theme-resolved color system.
   Each family sets the FULL role set in ONE grouped rule: the 3 surface tokens
   + the 6 foreground roles (--text, --text-muted, --link, --link-hover,
   --text-accent, --focus) + the control roles (--field-*, --select-*, --btn-*)
   + color:. The section and its matching island share the selector; the section
   paints the canvas, the island marker paints var(--surface-raised) — so
   adopting `.surface--X` yields a complete, VISIBLE card (the 2026-07-03
   unpainted-island bugs came from markers that only remapped tokens).
   Re-asserting the full set means a context nested in a different context resets
   correctly (e.g. a .surface--light card on a .section--dark canvas). Pick a
   theme → every surface, foreground, and control resolves → WCAG AA holds
   (contrast.js). See css-conventions.md § Theming. (The former `.X .text-muted`
   zombie overrides that lived here were collapsed in Stage 3; the button/form
   B5 carve-out rules were collapsed into the control roles on 2026-07-03 —
   base rules consume the tokens, and the per-context descendant blocks are gone.)
   ========================================================================== */

/* Warm theme (default) + its island — light family. Foreground roles equal the
   :root defaults, re-asserted so a warm context nested in a darker one resets. */
.section--light,
.surface--light {
  color: var(--text);
  --surface-canvas:   var(--color-bg-light);
  --surface-recessed: var(--color-recessed-light);
  --surface-raised:   var(--color-surface-light);
  --text:        var(--color-text-dark);
  --text-muted:  var(--color-text-muted);
  --link:        var(--color-primary-dark);
  --link-hover:  var(--color-link-light-hover);
  --text-accent: var(--color-primary-dark);
  --focus:       var(--color-primary-dark);
  --field-border:              var(--color-text-muted);
  --field-placeholder:         var(--color-text-placeholder);
  --field-placeholder-opacity: 1;
  --select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cpath fill='%23ad4500' d='M4 6l4 4 4-4z'/%3E%3C/svg%3E");
  --select-check:   url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='none' stroke='%23008009' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' d='M4 10l4 4 8-8'/%3E%3C/svg%3E");
  --btn-outline-ink:            var(--color-primary-dark);
  --btn-outline-rest-opacity:   1;
  --btn-outline-hover-bg:       var(--color-primary-dark);
  --btn-outline-hover-ink:      var(--color-white);
  --btn-outline-active-opacity: 1;
  --btn-fill-hover-bg:  var(--color-primary-dark);
  --btn-fill-hover-ink: var(--color-text-light);
  --btn-fill-active-bg: var(--color-primary-dark);
  --badge-outline-ink:  var(--color-info-dark);
}
.section--light {
  background-color: var(--color-bg-light);
}
/* Light canvas step — a flat, slightly raised canvas for ONE of two
   consecutive light sections, so same-register neighbours separate by tone
   rather than by a wash or a divider (the washes were retired on home:
   a gradient on a flat surface reads as a stain). Which one: whichever of
   the pair does not carry raised cards — their --surface-raised IS this
   canvas, so the step under them would swallow its own cards (home puts it
   on What I Do, the first of its pair, for exactly that reason; restyle
   10.3). It re-points
   --surface-canvas so anything that paints "the canvas" (a .wave, a card
   floor) follows. Pair of .section--dark-alt (01-prd-marketing-restyle.md FR 12). */
.section--light-alt {
  --surface-canvas: var(--color-surface-light);
  background-color: var(--surface-canvas);
}
.surface--light {
  background-color: var(--surface-raised);
}

/* Cool theme (.section--soft) — cool surfaces, same light-family foregrounds.
   .surface--soft stays reserved (mint-on-first-use; see marker below). */
.section--soft {
  color: var(--text);
  background-color: var(--color-bg-cool);
  --surface-canvas:   var(--color-bg-cool);
  --surface-recessed: var(--color-recessed-cool);
  --surface-raised:   var(--color-surface-cool);
  --text:        var(--color-text-dark);
  --text-muted:  var(--color-text-muted);
  --link:        var(--color-primary-dark);
  --link-hover:  var(--color-link-light-hover);
  --text-accent: var(--color-primary-dark);
  --focus:       var(--color-primary-dark);
  --field-border:              var(--color-text-muted);
  --field-placeholder:         var(--color-text-placeholder);
  --field-placeholder-opacity: 1;
  --select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cpath fill='%23ad4500' d='M4 6l4 4 4-4z'/%3E%3C/svg%3E");
  --select-check:   url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='none' stroke='%23008009' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' d='M4 10l4 4 8-8'/%3E%3C/svg%3E");
  --btn-outline-ink:            var(--color-primary-dark);
  --btn-outline-rest-opacity:   1;
  --btn-outline-hover-bg:       var(--color-primary-dark);
  --btn-outline-hover-ink:      var(--color-white);
  --btn-outline-active-opacity: 1;
  --btn-fill-hover-bg:  var(--color-primary-dark);
  --btn-fill-hover-ink: var(--color-text-light);
  --btn-fill-active-bg: var(--color-primary-dark);
  --badge-outline-ink:  var(--color-info-dark);
}

/* Ink theme (.section--dark) + its island (.surface--dark = INK, NOT navy) —
   dark surfaces, light-on-dark foregrounds; ink links keep brand orange. */
.section--dark,
.surface--dark {
  color: var(--text);
  --surface-canvas:   var(--color-bg-dark);
  --surface-recessed: var(--color-recessed-dark);
  --surface-raised:   var(--color-surface-dark);
  --text:        var(--color-text-light);
  --text-muted:  var(--color-text-muted-dark);
  --link:        var(--color-link-dark);
  --link-hover:  var(--color-link-dark-hover);
  --text-accent: var(--color-link-dark-hover);
  --focus:       var(--color-link-dark);
  --field-border:              var(--border-strong-light);
  --field-placeholder:         var(--color-text-muted-dark); /* pre-dimmed ink — full opacity */
  --field-placeholder-opacity: 1;
  --select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cpath fill='%23ffffeb' d='M4 6l4 4 4-4z'/%3E%3C/svg%3E");
  --select-check:   url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='none' stroke='%234caf50' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' d='M4 10l4 4 8-8'/%3E%3C/svg%3E");
  /* Outline buttons: cream at 0.75 rest opacity (hierarchy — primary is the
     action); hover inverts to a cream fill with ink text. */
  --btn-outline-ink:            var(--color-text-light);
  --btn-outline-rest-opacity:   0.75;
  --btn-outline-hover-bg:       var(--color-text-light);
  --btn-outline-hover-ink:      var(--color-bg-dark);
  --btn-outline-active-opacity: 0.9;
  /* Filled buttons hover LIGHTER against the dark canvas; ink label — the old
     cream-on-#ff8533 hover was 2.24:1 (fixed 2026-07-03 in the token migration). */
  --btn-fill-hover-bg:  var(--color-primary-light);
  --btn-fill-hover-ink: var(--color-text-dark);
  --btn-fill-active-bg: var(--color-primary);
  /* Outline badge: brighter info-main — the badge's backdrop is the surface
     itself, and in the dark families it renders only on the CANVAS (detail
     headers; lists/cards are light islands). info-main reads 5.5:1 there but
     under 4.5 on the ink wells/cards — the canvas-only geometry is what the
     contrast gate models (contrast.js § badge-outline). */
  --badge-outline-ink:  var(--color-info-main);
}
.section--dark {
  background-color: var(--color-bg-dark);
}
/* Dark canvas step — the mirror of .section--light-alt: a flat, raised dark
   canvas between consecutive ink sections (first user: the services ladder,
   13.0). It is a different SURFACE TYPE from ink, not a darker ink — so the
   starscape's surface rule does not fire on it: never combine with .starscape
   (design.md § Treatment Assignments). Re-points --surface-canvas like its
   pair (01-prd-marketing-restyle.md FR 12). */
.section--dark-alt {
  --surface-canvas: var(--color-surface-dark);
  background-color: var(--surface-canvas);
}
.surface--dark {
  background-color: var(--surface-raised);
}

/* Navy theme (.section--navy, the renamed accent CTA section) + its island (.surface--navy) —
   navy surfaces, light-on-dark foregrounds. Navy links use the peach pair; note --link
   (peach #ffc299) and --text-accent share one primitive (also ink's --link-hover). */
.section--navy,
.surface--navy {
  color: var(--text);
  --surface-canvas:   var(--color-bg-navy);
  --surface-recessed: var(--color-recessed-navy);
  --surface-raised:   var(--color-surface-navy);
  --text:        var(--color-text-light);
  --text-muted:  var(--color-text-muted-dark);
  --link:        var(--color-link-dark-hover);
  --link-hover:  var(--color-link-navy-hover);
  --text-accent: var(--color-link-dark-hover);
  --focus:       var(--color-link-dark);
  --field-border:              var(--border-strong-light);
  --field-placeholder:         var(--color-text-muted-dark);
  --field-placeholder-opacity: 1;
  --select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cpath fill='%23ffffeb' d='M4 6l4 4 4-4z'/%3E%3C/svg%3E");
  --select-check:   url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='none' stroke='%234caf50' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' d='M4 10l4 4 8-8'/%3E%3C/svg%3E");
  --btn-outline-ink:            var(--color-text-light);
  --btn-outline-rest-opacity:   0.75;
  --btn-outline-hover-bg:       var(--color-text-light);
  --btn-outline-hover-ink:      var(--color-bg-dark);
  --btn-outline-active-opacity: 0.9;
  --btn-fill-hover-bg:  var(--color-primary-light);
  --btn-fill-hover-ink: var(--color-text-dark);
  --btn-fill-active-bg: var(--color-primary);
  /* No badge renders in a navy context today; info-light is the completeness
     value — it clears 4.5:1 on every navy surface, so a future badge here can
     never regress the gate (ink's info-main would read 3.3:1 on this canvas). */
  --badge-outline-ink:  var(--color-info-light);
}
.section--navy {
  background-color: var(--color-bg-navy);
}
.surface--navy {
  background-color: var(--surface-raised);
}

/*   .surface--soft  (reserved) — cool-family island; not minted (no cool card exists yet)
 * Mint-on-first-use: add a reserved variant by appending its selector to the matching
 * grouped rule above (e.g. `.section--soft, .surface--soft { … }`). Note .surface--dark
 * is the INK family, not navy — distinct dark themes. See css-conventions.md § Theming. */

/* Section eyebrow — small label above headings; theme-resolved accent via --text-accent
   (warm dark-orange #ad4500 on light/cool, peach #ffc299 on ink/navy). */
/* Spark eyebrow — the uppercase kicker above a section heading, promoted from
   the founding page to the baseline (01-prd-marketing-restyle.md FR 6): tiny,
   bold, widely tracked, with a spark glyph before it. The glyph is a CSS mask
   over currentColor, so it themes with the eyebrow's role token and never
   enters the accessibility tree (a pseudo-element, no markup, no alt). */
.section__eyebrow {
  font-size: var(--font-size-tiny);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--text-accent);
  margin-bottom: var(--space-sm);
}

.section__eyebrow::before {
  content: "";
  display: inline-block;
  width: 1em;
  height: 1em;
  margin-right: 0.6em;
  vertical-align: -0.12em;
  background-color: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' d='M12 2v5M12 17v5M2 12h5M17 12h5M5 5l3.5 3.5M15.5 15.5L19 19M19 5l-3.5 3.5M8.5 15.5L5 19'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' d='M12 2v5M12 17v5M2 12h5M17 12h5M5 5l3.5 3.5M15.5 15.5L19 19M19 5l-3.5 3.5M8.5 15.5L5 19'/%3E%3C/svg%3E") center / contain no-repeat;
}

/* Section heading (h2) — strong hierarchy, tight tracking */
.section__heading {
  /* Serif section heading — the editorial register promoted from the founding
     page to the baseline (01-prd-marketing-restyle.md FR 5): DM Serif Display
     at its single 400 weight, with a hair of positive tracking (the former
     −0.01em was tuned for a bold sans). Size and margin are unchanged from the
     sans era; the prototype's larger home headings are a home decision (10.0). */
  font-family: var(--font-accent);
  font-size: var(--font-size-4xl);
  font-weight: 400;
  line-height: 1.15;
  letter-spacing: 0.01em;
  margin-bottom: var(--space-lg);
}

/* Meta line — the small typewriter aside under a CTA or heading (a close date,
   a cohort size, a one-line promise): the site's honesty voice, in the mono face
   at the tiny size, muted. Deliberately describes the slot rather than quoting the
   copy — the previous example quoted a close date that changed out from under it.
   Promoted from the founding page (01-prd-marketing-restyle.md FR 7). */
.meta-line {
  font-family: var(--font-typewriter);
  font-size: var(--font-size-tiny);
  color: var(--text-muted);
  letter-spacing: 0.02em;
}

/* Section body — comfortable reading, normal weight (no bold body text) */
.section__body {
  font-size: var(--font-size-base);
  font-weight: 400;
  line-height: 1.7;
  margin-bottom: var(--space-md);
}

/* Section lead — slightly larger intro paragraph, still normal weight */
.section__lead {
  font-size: var(--font-size-large);
  font-weight: 400;
  line-height: 1.6;
  margin-bottom: var(--space-md);
}

/* ==========================================================================
   Utility — Single-Purpose Classes
   ========================================================================== */

/* Base anchor — safe-by-default link color in every theme. Without this, an
   anchor that misses its utility class falls through to the UA default
   (blue/visited-purple — near-invisible on the ink canvas; two portal links
   shipped that way before 2026-07-03). Scoped to :not([class]) so it matches
   ONLY the unstyled stragglers — classed links (and their hover/focus designs)
   are untouched. Underline stays (UA default) for the same reason. */
a:not([class]) {
  color: var(--link);
}

a:not([class]):visited {
  color: var(--link);
}

a:not([class]):hover {
  color: var(--link-hover);
}

a:not([class]):focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 3px;
}

/* Link utilities: .link-body, .link-plain */

/* Body text link with animated underline */
.link-body {
  font-family: var(--font-primary);
  font-weight: 500;
  font-size: var(--font-size-base);
  color: var(--link);
  text-decoration: none;
  transition: all 0.5s ease;
  position: relative;
  z-index: 3;
  pointer-events: auto;
}

.link-body::after {
  content: '';
  position: absolute;
  bottom: -4px;
  left: 0;
  width: 0;
  height: 2px;
  background: var(--link-hover);
  transition: width 0.5s ease;
}

.link-body:hover {
  color: var(--link-hover);
}

.link-body:hover::after {
  width: 100%;
}

/* Plain link with color transition only */
.link-plain {
  color: var(--link);
  text-decoration: none;
  transition: color 0.3s ease;
}

.link-plain:hover {
  color: var(--link-hover);
}

/* Muted/secondary text utility — consumes the theme-resolved --text-muted role token,
   so it resolves per context (warm/cool/ink/navy) and inside nested islands. Replaces
   the former .section--{light,soft,dark} .text-muted zombie overrides (B1+B2, task 4.2). */
.text-muted {
  color: var(--text-muted);
}

/* Button reset — render a <button> with no native chrome so it can adopt
   a sibling link class (e.g. .nav-bar__link, .footer__link) for the visual
   style. Use on POST-action controls (e.g. sign-out) that should visually
   match adjacent link styling but must remain <button> elements because they
   submit a form rather than navigate. See coding-conventions.md
   § CSS Architecture → Button-as-link styling. */
.button-reset {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  cursor: pointer;
}

/* Sign-out form wrapper — keeps the <form> inline-flowing so the rendered
   button sits in a horizontal nav row alongside <a> siblings. Emitted by
   render_signout_form() in includes/auth-nav.php. */
.signout-form {
  display: inline;
}

/* ==========================================================================
   Block — Announcement Bar
   ========================================================================== */

/* Founding Client Program campaign bar (01-prd-founding-client-offer.md FR 5/6) — rendered by header.php
   above the nav on every header page except /founding-client itself. Static
   site chrome for the ~6-week campaign: normal flow, no JS positioning, so no
   layout shift on load. The markup carries .surface--dark, which paints the
   raised ink surface (visibly lighter than the nav-bar's canvas below) and
   remaps every foreground role AA-verified — this block consumes role tokens
   only. Removed at campaign close-out (task list § Close-out checklist). */
.announcement-bar {
  padding: var(--space-xs) var(--space-md);
  text-align: center;
}

.announcement-bar__line {
  font-size: var(--font-size-tiny);
  letter-spacing: 0.02em;
  color: var(--text);
}

/* Campaign name — accent ink pulls the eye without introducing a second font */
.announcement-bar__label {
  font-weight: 600;
  color: var(--text-accent);
}

/* CTA — underlined so the actionable part is distinguishable from the line by
   more than color alone (WCAG 1.4.1); nowrap keeps "Learn more →" one unit
   when the line wraps at narrow widths */
.announcement-bar__link {
  margin-left: var(--space-xs);
  font-weight: 600;
  color: var(--link);
  text-decoration: underline;
  text-underline-offset: 0.2em;
  white-space: nowrap;
  transition: color 200ms ease;
}

.announcement-bar__link:hover {
  color: var(--link-hover);
}

/* ==========================================================================
   Block — Buttons
   ========================================================================== */

/* CTA group — left-aligned horizontal pair matching section text; collapses to constrained column on narrow screens */
.cta-group {
  display: flex;
  justify-content: flex-start;
  gap: 1.5rem;
  flex-wrap: wrap;
}

/* Base button styles shared by all variants */
.btn {
  display: inline-block;
  font-family: var(--font-primary);
  font-size: var(--font-size-large);
  font-weight: 600;
  letter-spacing: 0.02em;
  text-decoration: none;
  padding: 1.4rem 3rem;
  border-radius: 0.4rem;
  min-height: 44px;
  cursor: pointer;
  position: relative; /* anchors the comet orbit (::before) */
  border: 2px solid transparent;
  transition: background-color 300ms cubic-bezier(0.4, 0, 0.2, 1),
              color 300ms cubic-bezier(0.4, 0, 0.2, 1),
              border-color 300ms cubic-bezier(0.4, 0, 0.2, 1),
              opacity 300ms cubic-bezier(0.4, 0, 0.2, 1);
}

/* Hover feedback — the comet orbit (01-prd-marketing-restyle.md FR 11; design.md
   § Component Behavior: hover is the color response plus this orbit, never
   movement — the lift retired 2026-08-23). A small glowing body circles the
   control's border while it is pointed at. It rides a CSS motion path
   (offset-path: border-box + offset-distance) rather than a rotated conic
   gradient: a rotation crawls along the long edges and whips round the corners,
   a motion path gives uniform perimeter speed; offset-rotate: auto keeps the
   tail trailing the direction of travel. Ported from the approved prototype's
   Buttons panel with one change — ::before rather than ::after, because
   .is-loading::after (below) owns that slot for the submit spinner. Existing
   tokens only: head --color-text-light, body --color-primary-light, tail
   --tint-primary-glow. Fires on :hover only, never on focus, so the 3px/3px
   focus ring is never overlapped (FR 25 c); under reduced motion it is absent
   (the gate below). offset-path: border-box was verified in Safari on the
   iPhone 11 at the 6.8 device review (iOS 26.6, 2026-08-26): it orbits, so no
   @supports fallback ships. Recorded motion-rule exception (design.md
   § Cross-Cutting Motion Rules): offset-distance is compositor-only, and the
   period is deliberately slow — 2.6 s at the prototype, slowed to 4 s at the
   6.8 Firefox review (operator, 2026-08-26), where the body also grew from
   1.8 x 0.5 rem to 2.4 x 0.6 rem; not values to tune down. One comet for every
   button variant: an ink-coloured variant for the orange primary was tried at
   that review and withdrawn. */
@keyframes comet-orbit {
  to { offset-distance: 100%; }
}

.btn::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 2.4rem;
  height: 0.6rem; /* 3 px overhang each side of the edge — abuts the 3 px focus-ring gap, never crosses it (6.4) */
  border-radius: 999px;
  background: linear-gradient(to right,
    transparent,
    var(--tint-primary-glow) 35%,
    var(--color-primary-light) 75%,
    var(--color-text-light));
  box-shadow: 0 0 8px 1px var(--tint-primary-glow);
  offset-path: border-box;
  offset-rotate: auto;
  offset-distance: 0%;
  /* The orbit is attached here, paused, and hover only runs it: if the hover
     rule owned the animation, mouse-out would drop it and snap the comet back
     to 0% while the opacity fade was still playing — a visible flash to the
     corner (seen at the 6.8 review). Paused, it fades out where it is and the
     next hover resumes from there. */
  animation: comet-orbit 4000ms linear infinite paused;
  opacity: 0;
  transition: opacity 300ms cubic-bezier(0.4, 0, 0.2, 1);
  pointer-events: none;
}

.btn:hover::before {
  opacity: 1;
  animation-play-state: running;
}

/* A control that cannot be activated gets no orbit (the hover rule above still
   matches a disabled button, so the pseudo-element itself is removed). */
.btn:disabled::before,
.btn.is-disabled::before {
  display: none;
}

/* Reduced motion (root class — OS setting or the site toggle): no comet at all.
   The prototype and FR 11 froze it at its start as a static highlight; at the
   6.8 device review a motionless comet read as something stuck on the button
   (operator, 2026-08-26), so it is removed and the button's colour hover
   response stands alone — hover feedback survives, only the motion goes. */
html.motion-reduce .btn::before {
  display: none;
}

/* Primary — solid orange fill; dark text on orange gives 6.01:1 contrast (WCAG AA).
   Rest is theme-invariant; hover/active fills + label come from the --btn-fill-*
   control roles (light: darken; dark/navy: lighten), so the button themes by
   cascade in any context, including islands. */
.btn--primary {
  background-color: var(--color-primary);
  color: var(--color-text-dark);
}

.btn--primary:hover {
  background-color: var(--btn-fill-hover-bg);
  color: var(--btn-fill-hover-ink);
}

.btn--primary:focus {
  outline: 3px solid var(--focus);
  outline-offset: 3px;
}

.btn--primary:active {
  background-color: var(--btn-fill-active-bg);
}

.btn--primary:disabled,
.btn--primary.is-disabled {
  background-color: var(--color-text-muted);
  cursor: not-allowed;
  opacity: 0.6;
}

/* Secondary — outline variant. All color/opacity comes from the --btn-outline-*
   control roles (light: dark-orange ink; dark/navy: cream at 0.75 rest opacity
   for hierarchy, removed on hover to signal interactivity) — one base rule,
   themed by cascade; no per-context variants. */
.btn--secondary {
  background-color: transparent;
  color: var(--btn-outline-ink);
  border: 2px solid var(--btn-outline-ink);
  opacity: var(--btn-outline-rest-opacity);
}

.btn--secondary:hover {
  background-color: var(--btn-outline-hover-bg);
  border-color: var(--btn-outline-hover-bg);
  color: var(--btn-outline-hover-ink);
  opacity: 1;
}

.btn--secondary:focus {
  outline: 3px solid var(--focus);
  outline-offset: 3px;
}

.btn--secondary:active {
  background-color: var(--btn-outline-hover-bg);
  border-color: var(--btn-outline-hover-bg);
  color: var(--btn-outline-hover-ink);
  opacity: var(--btn-outline-active-opacity);
}

/* Brand-glow CTA — composed onto .btn--primary as a Moment-tier spend: a page
   carries at most two (design.md § Treatment Assignments; /founding-client's
   are its hero "Apply Now" and the form submit). The glow is reinforcement on
   the already-styled primary button, never the only interactive cue, and its
   hover intensification sits above the comet orbit (6.3). Promoted from the
   founding page (01-prd-marketing-restyle.md FR 10). */
.glow-btn {
  box-shadow: 0 0 36px var(--tint-primary-glow);
}

.glow-btn:hover {
  box-shadow: 0 0 48px var(--tint-primary-glow);
}

/* Loading state — spinner replaces button text.
   Generic (not scoped to .btn) so it can be applied to any submit button
   styled as a link (e.g. the sign-out button rendered by render_signout_form()).
   The spinner border uses --color-white, which reads on dark backgrounds
   (.btn--primary orange, nav-bar dark, footer dark). If a future caller
   needs a different spinner color, override .is-loading::after locally. */
.is-loading {
  position: relative;
  color: transparent;
  pointer-events: none;
}

.is-loading::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 2rem;
  height: 2rem;
  margin-top: -1rem;
  margin-left: -1rem;
  border: 2px solid var(--color-white);
  border-top-color: transparent;
  border-radius: 50%;
  animation: spinner 0.8s linear infinite;
}

@keyframes spinner {
  to { transform: rotate(360deg); }
}

/*
 * Flash banner — PRG-pattern destination message. Single class family used by
 * every page that emits flashes (portal, admin, auth). Backgrounds are solid
 * (not alpha-blended) so the card is self-contained and renders with the same
 * AA-compliant contrast regardless of the page surface beneath it — including
 * the dark auth-card surface.
 */
.flash {
  border-radius: 0.4rem;
  padding: 1.2rem 1.6rem;
  margin-bottom: 2rem;
  font-size: var(--font-size-small);
  font-weight: 600;
  line-height: 1.5;
}

.flash--success {
  color: var(--color-success-dark);
  background-color: var(--color-success-light);
  border-left: 4px solid var(--color-success-dark);
}

.flash--info {
  color: var(--color-info-dark);
  background-color: var(--color-info-light);
  border-left: 4px solid var(--color-info-dark);
}

.flash--error {
  color: var(--color-error-dark);
  background-color: var(--color-error-light);
  border-left: 4px solid var(--color-error-dark);
}

/* ==========================================================================
   Block — Contact Form
   ========================================================================== */

/* Form fields, validation states, and submit button */

/* Form container */
.contact-form {
  max-width: 720px;
  margin: 0 auto;
}

/* Form group - wrapper for each field */
.form-group {
  margin-bottom: var(--space-md);
}

/* Form labels */
.form-label {
  display: block;
  font-size: var(--font-size-base);
  font-weight: 600;
  color: var(--text);
  margin-bottom: 0.6rem;
}

/* Required indicator */
.required {
  color: var(--color-error-dark);
  font-weight: 700;
}

/* Optional indicator — subordinate to label text */
.optional {
  color: var(--text-muted);
  font-weight: 400;
  font-size: 0.85em;
}

/* Field hint — supplementary guidance shown below an input. Ink comes from the
   theme-resolved --text-muted role (NOT the raw --color-text-muted literal, which
   is light-family ink and reads ~2.7:1 on the dark auth card). */
.form-hint {
  display: block;
  margin-top: 0.6rem;
  font-size: var(--font-size-small);
  color: var(--text-muted);
}

/* Base input styling - text, email, tel */
/* Inputs sit in the theme's recessed well (§ Surfaces & Elevation) and take
   all color from the role/control tokens — no per-context variants. */
.form-input {
  width: 100%;
  height: 4.8rem;
  padding: 1.2rem 1.6rem;
  font-family: var(--font-primary);
  font-size: var(--font-size-base);
  font-weight: 400;
  color: var(--text);
  background-color: var(--surface-recessed);
  border: 2px solid var(--field-border);
  border-radius: 0.4rem;
  transition: border-color 200ms ease-in-out,
              box-shadow 200ms ease-in-out;
}

/* Textarea specific styling */
.form-textarea {
  width: 100%;
  min-height: 12rem;
  padding: 1.2rem 1.6rem;
  font-family: var(--font-primary);
  font-size: var(--font-size-base);
  font-weight: 400;
  color: var(--text);
  background-color: var(--surface-recessed);
  border: 2px solid var(--field-border);
  border-radius: 0.4rem;
  resize: vertical;
  line-height: 1.6;
  transition: border-color 200ms ease-in-out,
              box-shadow 200ms ease-in-out;
}

/* Placeholders — ink from the control roles; both families ship pre-dimmed ink
   at full opacity (12.1 verdict 2026-07-09 — the old light-family 0.6 dim
   composited to ≈2.5:1 in the wells). The -opacity token stays for the day a
   family needs the dimming mechanism again. */
.form-input::placeholder,
.form-textarea::placeholder {
  color: var(--field-placeholder);
  opacity: var(--field-placeholder-opacity);
}

/* Select dropdown styling — the chevron image comes whole from the
   --select-chevron control role (url() can't interpolate a var(), but a custom
   property can carry the entire image). */
.form-select {
  width: 100%;
  height: 4.8rem;
  padding: 1.2rem 3rem 1.2rem 1.6rem;
  font-family: var(--font-primary);
  font-size: var(--font-size-base);
  font-weight: 400;
  color: var(--text);
  background-color: var(--surface-recessed);
  background-image: var(--select-chevron);
  background-repeat: no-repeat;
  background-position: right 1.2rem center;
  background-size: 1.6rem;
  border: 2px solid var(--field-border);
  border-radius: 0.4rem;
  appearance: none;
  cursor: pointer;
  transition: border-color 200ms ease-in-out,
              box-shadow 200ms ease-in-out;
}

/* Focus state for all input types.
 *
 * The visible focus ring is the box-shadow (a soft 3px primary-color halo).
 * The transparent outline is intentional — Windows High Contrast Mode discards
 * box-shadow entirely but preserves outline, so this declaration is invisible
 * under normal rendering and forced-visible under HCM. Without it, HCM users
 * have no focus indicator on form fields. Removing or shorthanding either the
 * outline or the box-shadow line breaks one of the two rendering modes. */
.form-input:focus,
.form-textarea:focus,
.form-select:focus {
  outline: 2px solid transparent;
  outline-offset: 2px;
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px var(--focus); /* solid per-family focus ring (B4) — no alpha (composited alpha fell sub-3:1 on light surfaces) */
}

/* Error state - invalid field after blur */
.form-input--error,
.form-textarea.form-input--error,
.form-select.form-input--error {
  border-color: var(--color-error-dark);
}

/* Success state - valid field after blur. The checkmark comes whole from the
   --select-check control role (green stroke tuned per family). */
.form-input--success,
.form-textarea.form-input--success,
.form-select.form-input--success {
  border-color: var(--color-success-dark);
  background-image: var(--select-check);
  background-repeat: no-repeat;
  background-position: right 1.2rem center;
  background-size: 2rem;
  padding-right: 4rem;
}

/* Success state for select - replace chevron with checkmark */
.form-select.form-input--success {
  background-image: var(--select-check);
  background-size: 2rem;
  padding-right: 3rem;
}

/* Password field — positioning wrapper for the show/hide visibility toggle.
   The button is injected by shared/password-toggle.js into markup carrying
   `data-password-toggle`; without JS no button exists, so the reserved input
   padding is gated behind the JS-added --has-toggle class rather than the
   wrapper itself. Ink and focus come from the role tokens, so the control
   themes by cascade on any surface (§ Surfaces & Elevation). */
.password-field {
  position: relative;
}

/* Suppress Edge/Chromium's built-in reveal eye inside the wrapper — with the
   custom toggle present it duplicates the control (and, unlike ours, it is
   invisible to assistive tech). Scoped to .password-field so bare password
   inputs elsewhere keep the native affordance. */
/* Known Firefox console warning, by design: the pseudo-element exists only in
   Chromium/Edge, and Firefox reports every selector it cannot parse ("Unknown
   pseudo-class or pseudo-element '-ms-reveal'. Ruleset ignored due to bad
   selector") on every page that loads this file. It cannot be silenced from
   CSS — Firefox parses the contents of @supports / @media blocks regardless of
   their condition (verified 2026-08-28: the warning survived an
   @supports selector() wrap) — and the rule stays here rather than in auth.css
   because .password-field is a shared primitive the portal may use without
   auth.css. Harmless: the ruleset is meant to be ignored where the pseudo-
   element does not exist. */
.password-field .form-input::-ms-reveal {
  display: none;
}

.password-field--has-toggle .form-input {
  padding-right: 4.8rem; /* clears the 3.6rem button + breathing room */
}

.password-toggle {
  position: absolute;
  top: 50%;
  right: 0.6rem;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 3.6rem;
  height: 3.6rem;
  padding: 0;
  background: none;
  border: none;
  border-radius: 0.4rem;
  color: var(--text-muted);
  cursor: pointer;
}

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

/* :focus-visible (not the site's usual :focus) — this control is mouse-clicked
   repeatedly, and a ring that persists after every click reads as a stuck
   state. Keyboard focus still gets the full ring. */
.password-toggle:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.password-toggle__icon {
  width: 2rem;
  height: 2rem;
}

/* Icon swap keyed off the toggle state: eye while hidden ("press to show"),
   eye-off while revealed. Both icons are aria-hidden; state is announced via
   aria-pressed on the button. */
.password-toggle__icon--hide,
.password-toggle[aria-pressed="true"] .password-toggle__icon--show {
  display: none;
}

.password-toggle[aria-pressed="true"] .password-toggle__icon--hide {
  display: block;
}

/* Error message display */
.form-error {
  display: none;
  font-size: var(--font-size-small);
  color: var(--color-error-dark);
  margin-top: 0.4rem;
  align-items: center;
  gap: 0.4rem;
}

.form-error:not([hidden]) {
  display: flex;
}

/* Error icon */
.form-error::before {
  content: '⚠';
  font-size: 1.6rem;
  line-height: 1;
}

/* Error summary at top of form */
.form-error-summary {
  background-color: var(--color-error-light);
  border-left: 4px solid var(--color-error-dark);
  padding: 1.6rem;
  margin-bottom: 2rem;
  border-radius: 0.4rem;
}

.form-error-summary p {
  margin: 0;
  color: var(--color-error-dark);
  font-weight: 600;
  display: flex;
  align-items: center;
  gap: 0.8rem;
}

.form-error-summary p::before {
  content: '⚠';
  font-size: 2rem;
  line-height: 1;
}

/* Form-level POST validation error — single-message banner rendered above the
   form. Separate from the .flash PRG channel (set_flash / render_flash).
   Always rendered in DOM; :empty collapses it when there is no error so
   aria-describedby references and focus scripts resolve unconditionally. */
.form-error-banner {
  display: flex;
  align-items: flex-start;
  gap: 0.8rem;
  border-radius: 0.4rem;
  padding: 1.2rem 1.6rem;
  margin-bottom: 1.6rem;
  font-size: var(--font-size-small);
  font-weight: 600;
  line-height: 1.5;
  color: var(--color-error-dark);
  background-color: var(--color-error-light);
  border-left: 4px solid var(--color-error-dark);
}

/* Warning icon — error is not signalled by color alone */
.form-error-banner::before {
  content: '';
  display: block;
  flex-shrink: 0;
  width: 1.8rem;
  height: 1.8rem;
  margin-top: 0.1rem;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23d12424' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z'/%3E%3Cline x1='12' y1='9' x2='12' y2='13'/%3E%3Cline x1='12' y1='17' x2='12.01' y2='17'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-size: contain;
}

/* Collapsed when empty — present in DOM so focus/aria targets always resolve */
.form-error-banner:empty {
  display: none;
}

/* Success message after submission */
.form-success {
  background-color: var(--color-success-light);
  color: var(--color-success-dark);
  border-left: 4px solid var(--color-success-dark);
  padding: 2rem;
  margin-bottom: 2rem;
  border-radius: 0.4rem;
}

.form-success__message {
  margin: 0 0 1rem 0;
  color: var(--color-success-dark);
  font-weight: 600;
  font-size: var(--font-size-large);
  display: flex;
  align-items: center;
  gap: 0.8rem;
}

.form-success__message::before {
  content: '✓';
  font-size: 2.4rem;
  line-height: 1;
}

.form-success__next {
  margin: 0;
  color: var(--color-success-dark);
}

/* Form actions - button group */
.form-actions {
  display: flex;
  gap: 1.5rem;
  margin-top: var(--space-lg);
}

/* The optional honesty meta line under the actions (contact-form.php's
   'meta_line' slot, 01-prd-marketing-restyle.md FR 14) — the .meta-line
   register plus the breathing room the button row needs beneath it. */
.contact-form__meta {
  margin-top: var(--space-sm);
}

/* Radio button group styling */
.form-group--radio {
  border: none;
  padding: 0;
  margin-bottom: var(--space-md);
}

.form-group--radio legend {
  padding: 0;
  margin-bottom: 0.6rem;
}

.radio-group {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

/* Radio label container */
.radio-label {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  cursor: pointer;
  font-size: var(--font-size-base);
  font-weight: 400;
  color: var(--text);
  position: relative;
  padding: 0.8rem;
  border-radius: 0.4rem;
  transition: background-color 200ms ease-in-out;
}

.radio-label:hover {
  background-color: var(--tint-primary-faint);
}

/* Hide native radio button */
.form-radio {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}

/* Custom radio button circle */
.radio-custom {
  position: relative;
  width: 2rem;
  height: 2rem;
  border: 2px solid var(--field-border);
  border-radius: 50%;
  background-color: var(--surface-recessed);
  flex-shrink: 0;
  transition: border-color 200ms ease-in-out;
}

/* Checked state - orange inner circle */
.form-radio:checked + .radio-custom {
  border-color: var(--color-primary);
}

.form-radio:checked + .radio-custom::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 1rem;
  height: 1rem;
  border-radius: 50%;
  background-color: var(--color-primary);
}

/* Focus state for keyboard navigation */
.form-radio:focus + .radio-custom {
  outline: 3px solid var(--color-primary);
  outline-offset: 2px;
}

/* Error state for radio group */
.radio-group.form-group--error .radio-custom {
  border-color: var(--color-error-dark);
}

/* Form controls carry NO per-context overrides — every color they need comes
 * from the --field-* / --select-* control roles and --text/--text-muted on the
 * base rules above, so any theme context (or nested island) themes them by
 * cascade. The former "Dark Surface Form Overrides" and "Light islands"
 * descendant blocks were collapsed into those tokens on 2026-07-03 — their
 * (0,2,0) selectors leaked across island boundaries (see css-conventions.md
 * § Theming). Components MAY still layer Exception overrides on top —
 * `.auth-card .form-input` repaints the input background to --color-bg-dark
 * for an inset "well" against the card surface, for example. */

/* ==========================================================================
   Block — Expandable (FAQ Accordion)
   ========================================================================== */

/* Expandable container */
.expandable {
  margin-bottom: 0;
}

/* Expandable header - button that toggles visibility */
.expandable__header {
  width: 100%;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1.6rem 2rem;
  border: 1px solid var(--border-hairline);
  border-radius: 0.4rem;
  cursor: pointer;
  font-family: var(--font-primary);
  font-size: var(--font-size-large);
  font-weight: 600;
  text-align: left;
  color: var(--text);
  transition: background-color 300ms cubic-bezier(0.4, 0, 0.2, 1),
              border-color 300ms cubic-bezier(0.4, 0, 0.2, 1);
}

/* Header background/border per context — color comes from base (var(--text)). */
.section--light .expandable__header {
  background-color: var(--color-surface-light);
  border-color: var(--border-hairline);
}

.section--dark .expandable__header {
  background-color: var(--color-surface-dark);
  border-color: var(--border-hairline-light);
}

/* Expandable title — the h3 that WRAPS the header button (a heading is not
   permitted inside a button; the APG accordion pattern is heading > button) */
.expandable__title {
  margin: 0;
  font-size: var(--font-size-large);
  font-weight: 600;
  line-height: 1.3;
}

/* Icon indicator (chevron) */
.expandable__icon {
  flex-shrink: 0;
  width: 2rem;
  height: 2rem;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1);
}

/* Chevron down icon using CSS */
.expandable__icon::before {
  content: '';
  width: 0.8rem;
  height: 0.8rem;
  border-right: 2px solid var(--color-primary);
  border-bottom: 2px solid var(--color-primary);
  transform: rotate(45deg);
  transform-origin: center;
}

/* Rotated state when expanded */
.expandable__icon--rotated {
  transform: rotate(180deg);
}

/* Header hover state - subtle orange tint */
.expandable__header:hover {
  background-color: var(--tint-primary-faint);
}

.section--dark .expandable__header:hover {
  background-color: var(--tint-primary-soft);
}

/* Header focus state - keyboard accessibility */
.expandable__header:focus {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}

/* Header expanded state - border accent */
.expandable__header[aria-expanded="true"] {
  border-color: var(--color-primary);
}

/* Expandable content - hidden by default, animated open/closed by JS */
.expandable__content {
  overflow: hidden;
  max-height: 0;
  opacity: 0;
  padding-top: 0;
  padding-bottom: 0;
  padding-left: 2rem;
  padding-right: 2rem;
  border-left: 1px solid var(--border-hairline);
  border-right: 1px solid var(--border-hairline);
  border-bottom: 1px solid var(--border-hairline);
  border-radius: 0 0 0.4rem 0.4rem;
  transition: max-height 350ms cubic-bezier(0.4, 0, 0.2, 1),
              opacity 350ms cubic-bezier(0.4, 0, 0.2, 1),
              padding 350ms cubic-bezier(0.4, 0, 0.2, 1);
}

/* Content on light backgrounds */
.section--light .expandable__content {
  background-color: var(--color-surface-light);
  border-left-color: var(--border-hairline);
  border-right-color: var(--border-hairline);
  border-bottom-color: var(--border-hairline);
}

/* Content on dark backgrounds */
.section--dark .expandable__content {
  background-color: var(--color-surface-dark);
  border-left-color: var(--border-hairline-light);
  border-right-color: var(--border-hairline-light);
  border-bottom-color: var(--border-hairline-light);
}

/* Expanded content border accent when header is expanded. The button sits
   inside the .expandable__title heading, so the content panel is the
   heading's sibling, not the button's — :has() reaches down to the state. */
.expandable__title:has(.expandable__header[aria-expanded="true"]) + .expandable__content {
  border-color: var(--color-primary);
}

/* List styling within expandable content */
.expandable__content ul {
  margin: 1.5rem 0;
  padding-left: 0;
  list-style: none;
}

.expandable__content li {
  margin-bottom: 1rem;
  padding-left: 2rem;
  position: relative;
  line-height: 1.6;
}

.expandable__content li:last-child {
  margin-bottom: 0;
}

/* Custom bullet point using em dash */
.expandable__content li::before {
  content: '—';
  position: absolute;
  left: 0;
  color: var(--color-primary);
  font-weight: 700;
}

/* Paragraph spacing within expandable content */
.expandable__content p {
  margin-bottom: 1rem;
}

.expandable__content p:last-child {
  margin-bottom: 0;
}

.expandable__content p + ul {
  margin-top: 1rem;
}

/* ==========================================================================
   Block — Wordmark
   ========================================================================== */

/**
 * Shared base class for every wordmark instance:
 * nav bar (.nav-bar__wordmark), overlay (.nav-overlay__wordmark), footer (.footer__wordmark),
 * and the portal chrome (.portal-sidebar__brand, .portal-topbar__brand in portal.css).
 * Size, color, line-height, weight and spacing overrides live in each component's own
 * class — weight deliberately so: the chrome instances take --font-weight-wordmark-chrome
 * and the footer --font-weight-wordmark-display (optical sizing, see the token comments).
 * Tracking is 0: the former 0.05em was tuned for Montserrat; Unbounded's expansive set
 * carries its own air, and added tracking reads as gappy (01-prd-marketing-restyle.md FR 2).
 */
.wordmark {
  font-family: var(--font-wordmark);
  letter-spacing: 0;
  text-transform: uppercase;
}

/* ==========================================================================
   Block — Footer
   ========================================================================== */

/* Background image with dark overlay for text legibility */
.footer {
  position: relative;
  background-image: url('../images/footer/nebula.jpg');
  background-size: cover;
  background-position: center top;
  overflow: hidden;
  min-height: 70vh;
  min-height: 70svh;
  display: flex;
  flex-direction: column;
}

/* Ink scrim over the nebula — keeps the text legible and ties the photo to the
   ink canvas above it. A uniform 82 % warm-ink mix (restyle 17.2 / 17.3,
   operator direction 2026-09-03). Why 82 % and not the old flat 60 % black:
   the photo's brightest stars under 17 px link text measured 3.04:1 (a 9 × 10
   px spot under "About") and under the 22 px tagline 2.90:1 (a 7 × 8 px spot)
   — rule 7's ordinary case on a photograph, never measured before 17.3's
   footer pixel record; at 82 % the same spots clear 4.5:1. Why uniform: a
   graded scrim with a lighter window behind the wordmark was tried first,
   keyed to the footer's height in percent — but the footer's height changes
   with the column layout, so the window landed on different elements at each
   width and the footer read differently at phone, tablet and desktop
   (operator, 2026-09-03: make it the same everywhere). No scrim at all would
   fail the link and tagline contrast over the photo. */
.footer::before {
  content: '';
  position: absolute;
  inset: 0;
  background-color: color-mix(in srgb, var(--color-bg-dark) 82%, transparent);
  z-index: 0;
}

/* ---------- The ember bank ----------
   A static bank of orange forms along the footer's bottom edge — the "big
   bang" glow at the end of every page — framing the copyright line, which
   sits on the bank's deep field. Chrome, so Foundation tier by construction
   (every page carries the footer; design.md § Placing a New Treatment, q. 3);
   static, so nothing to gate. Built at restyle 17.2 (01-prd-marketing-restyle.md
   FR 21, operator direction 2026-09-03 — the footer redesign is an As-Built
   entry, beyond FR 21's plan) from a reference the operator supplied; the
   tuning trail (eight rendered rounds) is in the task list under 17.1.

   Mechanics: two inline SVG scenes in includes/footer.php — a 1440 × 520
   desktop scene and a 480 × 400 phone scene — bottom-anchored with
   `xMidYMax slice`, so the forms never stretch: a wider viewport reveals more
   of the scene's edges, a narrower one crops toward the centre. Each lobe is
   the reference's own construction: a gold circle and a light-orange circle
   sit BEHIND the orange body, offset up-left, so their crescents show over
   its upper rim; the body is nested toward the lobe's lower right at 0.79 of
   the lobe radius (the operator's split, round 8). Colour ladder, all
   existing tokens: the back mix → --color-primary-dark (the field the
   copyright sits on) → --color-primary bodies → --color-primary-light rims →
   --color-sun-gold tips (the dawn's decorative token, the bank's third tone).
   Toned down on purpose: the deep rung carries most of the area so the bank
   glows under home's contact glow-CTA rather than competing with it.

   Contrast: the copyright is cream on --color-primary-dark — 5.72:1 measured
   at the rendered pixels under the line at 1440 and 480 (restyle 17.1 / 17.3;
   the pair is not in contrast.js's palette, so the rendered record is the
   evidence). No text sits on the orange, light or gold forms.

   The glow group is a blur() filter on a full-width SVG group — painted once,
   never animated; 17.3's paint-cost run is the check that it adds no long
   frames on scroll. */
.footer__bank {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 0; /* above the ::before scrim (DOM order), below .footer__container (z 1) */
  width: 100%;
  height: 52rem;
  pointer-events: none;
}

/* The phone scene is swapped in at the tablet step (below) */
.footer__bank--phone {
  display: none;
}

.footer__bank-glow {
  fill: var(--tint-primary-glow);
  filter: blur(26px);
}

.footer__bank-back {
  fill: color-mix(in srgb, var(--color-primary-dark) 70%, var(--color-bg-dark));
}

.footer__bank-deep {
  fill: var(--color-primary-dark);
}

.footer__bank-body {
  fill: var(--color-primary);
}

.footer__bank-light {
  fill: var(--color-primary-light);
}

.footer__bank-gold {
  fill: var(--color-sun-gold);
}

.footer__container {
  position: relative;
  z-index: 1;
  max-width: 1200px;
  margin-left: auto;
  margin-right: auto;
  padding: var(--space-2xl) var(--space-md) var(--space-md);
  text-align: center;
  display: flex;
  flex-direction: column;
  flex: 1;
}

/* 4-column nav grid: Reach (wider) + Explore, Support, Connect */
.footer__nav-grid {
  display: grid;
  grid-template-columns: 1.5fr 1fr 1fr 1fr;
  gap: var(--space-lg);
  align-items: start;
  text-align: center;
  margin-bottom: var(--space-2xl);
}

/* Column headings use DM Serif Display at 2.2rem */
.footer__nav-group-heading {
  font-family: var(--font-accent);
  font-size: var(--font-size-xl);
  color: var(--color-text-light);
  margin-bottom: var(--space-sm);
}

/* Reach column body text — muted cream, smaller than headings */
.footer__nav-group p:not(.footer__nav-group-heading) {
  font-size: var(--font-size-small);
  color: var(--color-text-muted-dark);
}

/* Strip default list styling from nav column lists */
.footer__nav-group ul {
  list-style: none;
  padding: 0;
  margin: 0;
}

.footer__nav-group li {
  margin-bottom: var(--space-xs);
}

/* Large brand wordmark — the display-weight instance */
.footer__wordmark {
  font-size: clamp(3.5rem, 6.5vw + 0.4rem, 8rem);
  font-weight: var(--font-weight-wordmark-display);
  color: var(--color-text-light);
  line-height: 1;
  margin-bottom: var(--space-md);
  margin-top: var(--space-4xl);
}

/* Footer wordmark line break — same device as the overlay's: present in the
   markup, hidden by default, revealed at ≤768px to force "BIG BANG" /
   "VISIBILITY" exactly instead of leaving the wrap to the browser.
   Measured 2026-08-25 with Unbounded 800 (01-prd-marketing-restyle.md FR 4,
   task 3.7): under the 6.5vw slope the one-line mark fits its container by
   only 0–6px from 640 to 760 wide (600/600 at 640, 703/703 at 760) and wraps
   from 620 down, so Blink was already breaking it and a slightly wider engine
   metric would break it anywhere in that band. Forcing the break at the tablet
   step removes the fragile zone; above it the margin is 20px at 780, ~56 at
   1024, ~110 at 1440. The two-line mark's widest line is 234px at the 3.5rem
   floor — 46px to spare at 320. Re-measure if the face, weight, clamp or the
   footer container's width changes. */
.footer__wordmark-break {
  display: none;
}

@media (max-width: 768px) {
  .footer__wordmark-break {
    display: inline;
  }
}

/* The comet at rest — the accent between the wordmark and the tagline. The
   button family's hover comet (§ Buttons) come to rest under the mark: the
   same tail gradient and glow tokens, drawn as an inline SVG arc in
   includes/footer.php so the tail can curve (a rising arc echoes the orbit's
   corner). Replaced the 120 × 5 px gradient rule at restyle 17.2 — chosen
   from three rendered options at the 17.1 checkpoint (operator, 2026-09-03).
   Static; the stop colours are set here on classed <stop>s so the palette
   stays single-sourced (no literal hex in the markup). */
.footer__comet {
  width: 32rem;
  height: 3.7rem; /* the 240 × 28 viewBox at 32rem wide */
  margin: 0 auto var(--space-md);
}

.footer__comet svg {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible; /* the head's halo reaches past the viewBox */
  filter: drop-shadow(0 0 6px var(--tint-primary-glow));
}

.footer__comet-tail-start { stop-color: var(--color-primary); stop-opacity: 0; }
.footer__comet-tail-mid   { stop-color: var(--color-primary); stop-opacity: 0.4; }
.footer__comet-tail-body  { stop-color: var(--color-primary-light); }
.footer__comet-tail-head  { stop-color: var(--color-text-light); }

.footer__comet-head {
  fill: var(--color-text-light);
}

.footer__comet-halo {
  fill: var(--tint-primary-glow);
}

/* Short centered orange rule between wordmark and tagline — the pre-17.2
   accent. Kept ONLY for services/starter-website-packages.php, whose standalone
   footer (its own markup on these base classes, FR 19's hybrid) still emits it;
   the shared footer replaced it with the comet above. Retire this rule with
   that page's refresh PRD (to-do § Site Pages & Content). */
.footer__accent-bar {
  width: 120px;
  height: 5px;
  background: linear-gradient(to right, transparent, var(--color-primary), transparent);
  border-radius: 2px;
  margin: 0 auto var(--space-md);
  box-shadow: 0 0 12px var(--tint-primary-glow);
}

/* Tagline sits directly below the comet (or the packages page's rule) */
.footer__tagline {
  font-size: var(--font-size-xl);
  color: var(--color-text-muted-dark);
  letter-spacing: 0.05em;
  margin: 0 0 var(--space-3xl) 0;
}

/* Under the ember bank the tagline's bottom margin is the sky the bank rises
   into — measured at restyle 17.2 so the tagline's bottom clears the bank's
   tallest inner form (the inner lobes' gold rims) at 1440, ending ~346 px
   above the footer's edge. Keyed off the modifier so the packages page's
   bank-less footer keeps its rhythm. */
.footer--ember .footer__tagline {
  margin-bottom: calc(var(--space-4xl) + var(--space-3xl));
}

/* Bottom row: copyright + all rights reserved */
.footer__bottom {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  align-items: center;
  padding-bottom: var(--space-md);
}

/* …and under the bank it sits IN the bank, on the deep field, a step higher */
.footer--ember .footer__bottom {
  padding-bottom: var(--space-xl);
}

/* Cream, not the muted cream: on the bank's --color-primary-dark field the
   muted tone measures ~4.9:1 and the cream 5.72:1 (rendered pixels, 17.1) —
   the headroom is worth the one-step change. */
.footer__copyright {
  font-size: var(--font-size-small);
  color: var(--color-text-light);
}

.footer__link {
  font-size: var(--font-size-small);
  color: var(--color-text-muted-dark);
  text-decoration: none;
  transition: color 300ms cubic-bezier(0.4, 0, 0.2, 1);
}

/* Hover goes brand orange — the same hover the nav bar and overlay links use,
   one hover language across the chrome (nav-overlay-expansion draft,
   2026-08-19; was --color-text-light). #ff6a00 on the dark canvas = 6.01:1
   (contrast.js formula, measured 2026-08-19), AA-clear for this small text. */
.footer__link:hover {
  color: var(--color-primary);
}

.footer__link:focus {
  outline: 3px solid var(--color-primary);
  outline-offset: 3px;
}

/* Placeholder: links not yet active — visually subdued, non-interactive */
.footer__link--placeholder {
  opacity: 0.45;
  cursor: default;
  pointer-events: none;
}

/* ==========================================================================
   Block — Motion Toggle
   ========================================================================== */

/* Site-wide "Reduce motion" switch (01-prd-founding-client-offer.md FR 32) — two placements (nav
   overlay + footer Support column), both <button role="switch"> elements
   wired and kept in sync by assets/js/shared/motion/motion-toggle.js.
   State is styled off aria-checked, so the visual can never disagree with
   what assistive tech announces. Both placements sit on dark chrome outside
   the theme system, so the block remaps the role tokens it consumes to the
   dark family itself (skip-link / site-header precedent).
   Focus ring: the global button:focus-visible exception supplies the outline
   ring from --focus — outline (unlike box-shadow) survives Windows High
   Contrast Mode, so no transparent-outline pairing is needed here.
   Ratios on the dark chrome: ring 8.78:1, track border and label 11.96:1,
   on-state thumb-on-fill 8.78:1. */
.motion-toggle {
  /* Dark-chrome role remaps — both placements are permanently dark surfaces */
  --text: var(--color-text-light);
  --text-muted: var(--color-text-muted-dark);
  --focus: var(--color-link-dark);

  display: none; /* JS-driven control — hidden until .js confirms the module runs (gate below) */
  align-items: center;
  gap: var(--space-sm);
  font-size: var(--font-size-small);
  color: var(--text-muted);
  cursor: pointer;
  transition: color 300ms cubic-bezier(0.4, 0, 0.2, 1);
}

/* JS confirmed (pre-paint .js class) — reveal the control. The footer renders
   for no-JS visitors, who must never see a dead switch; the overlay placement
   is unreachable without JS either way, so one uniform gate covers both. */
.js .motion-toggle {
  display: inline-flex;
}

/* The label must not wrap. On iOS Safari "Reduce motion" breaks onto two lines
   at 375px where desktop Chromium keeps it on one — the switch is 21px taller on
   a real phone than in any headless render, which is exactly the kind of gap
   emulation cannot show you. Measured against a real iPhone 11, 2026-08-22. */
.motion-toggle__label {
  white-space: nowrap;
}

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

/* Track — bordered pill; the border carries the off-state affordance */
.motion-toggle__track {
  position: relative;
  flex-shrink: 0;
  width: 4.4rem;
  height: 2.4rem;
  border: 2px solid var(--text-muted);
  border-radius: 999px;
  transition: background-color 150ms ease, border-color 150ms ease;
}

/* Thumb — the 150ms slide sits under the 200ms reduced-motion threshold
   (html-conventions.md § Accessibility Requirements → Reduced Motion), and
   instant state feedback on this control is essential UI feedback besides. */
.motion-toggle__thumb {
  position: absolute;
  top: 0.2rem;
  left: 0.2rem;
  width: 1.6rem;
  height: 1.6rem;
  border-radius: 50%;
  background-color: var(--text-muted);
  transition: transform 150ms ease, background-color 150ms ease;
}

/* On state — accent-filled track, ink thumb slid right */
.motion-toggle[aria-checked="true"] .motion-toggle__track {
  background-color: var(--focus);
  border-color: var(--focus);
}

.motion-toggle[aria-checked="true"] .motion-toggle__thumb {
  background-color: var(--color-bg-dark);
  transform: translateX(2rem);
}

/* ==========================================================================
   Block — Navigation
   ========================================================================== */

/* Dark site chrome lives OUTSIDE the section/surface theme system (the header is
   an unthemed wrapper over the dark nav-bar/toggle/overlay), so its focusable
   children would fall back to the :root light-family --focus (#ad4500 ≈ 2:1 on
   the dark nav — a fail). Establish the dark-family focus ring here so the global
   :focus-visible resolves correctly for all header chrome (#ffa366 = 8.77:1). */
.site-header {
  --focus: var(--color-link-dark);
}

/* ---------- Nav Bar ---------- */

/* Horizontal bar — static, no z-index; sits in normal document flow */
.nav-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 var(--space-md);
  height: 6rem;
  background-color: var(--color-bg-dark);
}

/* Wordmark in bar — brand anchor and home link; chrome weight (small size) */
.nav-bar__wordmark {
  font-size: var(--font-size-small);
  font-weight: var(--font-weight-wordmark-chrome);
  color: var(--color-text-light);
  line-height: 1;
  text-decoration: none;
  transition: color 200ms ease;
}

.nav-bar__wordmark:hover {
  color: var(--color-primary);
}

/* Left link group: primary nav destinations */
.nav-bar__links {
  display: flex;
  align-items: center;
  gap: var(--space-md);
}

/* Right group: Full Menu trigger + Sign In */
.nav-bar__actions {
  display: flex;
  align-items: center;
  gap: var(--space-md);
}

/* Nav bar link base */
.nav-bar__link {
  font-size: var(--font-size-small);
  font-weight: 600;
  color: var(--color-text-light);
  text-decoration: none;
  letter-spacing: 0.02em;
  transition: color 200ms ease;
}

.nav-bar__link:hover {
  color: var(--color-primary);
}

/* Active page link */
.nav-bar__link--active {
  color: var(--color-primary);
}

/* Sign-out modifier — visually muted so a destructive action (ending the
   session) doesn't read as a peer of the navigation links beside it
   (Sign In, My Account). Recovers full opacity on hover to remain a clear
   target. Contrast at 70% white on the dark nav-bar (#1f1a15) is ~9:1 —
   well clear of WCAG AA 4.5:1. */
.nav-bar__link--signout {
  opacity: 0.7;
}

.nav-bar__link--signout:hover {
  opacity: 1;
}

/* ---------- Toggle Button (hamburger / close) ---------- */

/**
 * Fixed top-right. Hidden on desktop by default; shown on mobile via media query.
 * On desktop, JS adds .nav__toggle--visible when the overlay opens to show the × close icon.
 * z-index 250 keeps it clickable above the overlay (z-index 200) at all times.
 *
 * A raised disc with a caption (restyle 17.2, 01-prd-marketing-restyle.md FR 21;
 * direction settled at the 7.7 review, 2026-08-28): at ≤ 768 the nav bar is hidden
 * and this is the page's only nav entry, and a bare 24 px glyph floating over the
 * hero read as decoration to anyone who does not know the hamburger. The 4.4 rem
 * disc is the touch target (≥ 44 px); the MENU caption beneath it is the eyebrow's
 * type treatment (tiny / 700 / 0.14em — the type only, no spark) on its own raised
 * pill, because a fixed control floats over whatever a page puts beneath it (ink,
 * cream, the nebula) and a bare caption would vanish on one of them. The caption
 * hides while the overlay is open (the × alone, so the FR 4 overlay clearances
 * measured from the toggle's box still hold). Deliberately excluded (7.7): an
 * orbit ring (Reserved), a glow (the Moment CTA treatment), a pulse, the hover
 * comet (the control exists only where hover is rare). Contrast: the orange glyph
 * on --color-surface-dark is 3.53:1 (non-text, ≥ 3:1); the cream caption 8.8:1.
 */
.nav__toggle {
  position: fixed;
  top: var(--space-sm);
  right: var(--space-sm);
  z-index: 250;
  display: none;
  flex-direction: column;
  align-items: center;
  gap: 0.4rem;
  width: 4.4rem;  /* pinned to the disc — the wider caption pill below it overflows
                     the box symmetrically (centred flex item) instead of widening
                     it, so the disc keeps the announcement bar's measured 6rem
                     clearance (its text ends 6 px from the disc at 375). The pill
                     stays IN the box's flow: the box is disc + pill tall, so the
                     scroll-hide slide (-100 %) takes the whole assembly off the
                     top, and tapping the caption opens the menu too. */
  padding: 0;
  background: none;
  border: none;
  cursor: pointer;
  color: var(--color-primary);
  transition: transform 300ms ease, color 200ms ease;
}

/* JS adds this class in openOverlay to show × on desktop */
.nav__toggle--visible {
  display: flex;
}

/* The disc — the raised dark surface, hairline edge, soft shadow (a small moon on
   the starfield, a plain round button on cream). Sized as the whole old button was. */
.nav__toggle-disc {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 4.4rem;
  height: 4.4rem;
  border-radius: 50%;
  background-color: var(--color-surface-dark);
  border: 1px solid var(--border-hairline-light);
  box-shadow: var(--shadow-4);
}

/* SVG icon — 24 px, as before, now centred in the disc rather than padded to it */
.nav__toggle-icon {
  display: block;
  width: 2.4rem;
  height: 2.4rem;
}

/* The caption pill — eyebrow type on the same raised surface as the disc,
   centred beneath it in the button's flow. Tracking is a step under the
   eyebrow's 0.14em and the padding tight, so the pill is 56 px wide: centred
   under the 44 px disc at right 1rem it ends 4 px inside a 480 viewport
   (operator, 2026-09-03: the first cut's 63 px pill touched the screen edge). */
.nav__toggle-caption {
  flex-shrink: 0;
  white-space: nowrap;
  font-size: var(--font-size-tiny);
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  line-height: 1;
  color: var(--color-text-light);
  padding: 0.3rem 0.2rem;
  border-radius: 999px;
  background-color: var(--color-surface-dark);
  border: 1px solid var(--border-hairline-light);
  box-shadow: var(--shadow-4);
}

.nav__toggle[aria-expanded="true"] .nav__toggle-caption {
  display: none;
}

/* Hamburger lines — visible in closed state */
.nav__toggle-icon--hamburger {
  transition: opacity 200ms ease, transform 200ms ease;
}

/* × (close) lines — hidden in closed state */
.nav__toggle-icon--close {
  opacity: 0;
  transition: opacity 200ms ease, transform 200ms ease;
}

/* Open state: fade out hamburger, fade in × */
.nav__toggle[aria-expanded="true"] .nav__toggle-icon--hamburger {
  opacity: 0;
  transform: rotate(-45deg);
}

.nav__toggle[aria-expanded="true"] .nav__toggle-icon--close {
  opacity: 1;
}

/* ---------- Overlay ---------- */

/**
 * Full-screen overlay — closed: invisible and inert.
 * z-index 200 positions it above all page content, below the toggle button (250).
 * Dark background serves as the CSS-only fallback when canvas is unavailable.
 *
 * Column flex (restructured 2026-08-22): the overlay lays its four content
 * children out as stacked rows in markup order — wordmark / scrolling nav /
 * utility links / reduce-motion switch. Only the canvas stays absolutely
 * positioned, as a backdrop.
 *
 * This replaced a full-height scroll container with three absolutely-positioned
 * siblings pinned over it, where every gap between them was a hand-computed
 * reservation (.nav-overlay__nav's padding-top vs. the wordmark, its
 * padding-bottom vs. the utility row). Both reservations had drifted smaller
 * than the thing they reserved for — 12rem vs. a 12.6rem two-line wordmark,
 * 8rem vs. a 13.9rem utility row — so content collided on every short viewport.
 * In flow no row can overlap another, the scroll band resizes itself as nav
 * items are added, and the four magic numbers are gone.
 */
.nav-overlay {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  flex-direction: column;
  background-color: var(--color-bg-dark);
  opacity: 0;
  visibility: hidden;
  transition: opacity 300ms ease, visibility 300ms ease;
}

/* Open state — JS adds this class to activate the overlay */
.nav-overlay--active {
  opacity: 1;
  visibility: visible;
}

/* Secondary tier — row 3. Utility links above the reduce-motion switch,
   mirroring the footer's "utility lives at the bottom" grammar. As a row it can
   no longer be overlapped by the scrolling nav, which is exactly what the old
   @media (max-height: 650px) backdrop pill existed to paper over (that block is
   gone; see .nav-overlay). */
.nav-overlay__utility {
  position: relative;
  z-index: 2;
  flex: 0 0 auto;
  display: flex;
  justify-content: center;
  /* Keeps the outermost link off the viewport edge once the row is wide enough
     to fill it (PRIVACY met the right edge at 414px). */
  padding: 0 var(--space-md);
  margin-bottom: var(--space-lg);
}

.nav-overlay__utility-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0 var(--space-xl);
}

/* Secondary-tier link — small uppercase register. Color declared explicitly:
   a classed link has no styled fallback behind it (css-conventions.md
   § Classed Links Must Declare Color). Standalone nav items identified by
   position, not prose — no at-rest underline needed (SC 1.4.1). */
.nav-overlay__sublink {
  display: flex;
  align-items: center;
  min-height: 44px; /* WCAG 2.5.8 target floor — matches .nav-overlay__link */
  font-size: var(--font-size-small);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-text-muted-dark);
  text-decoration: none;
  transition: color 200ms ease;
}

.nav-overlay__sublink:hover {
  color: var(--color-primary);
}

/* Sign-out modifier — same "destructive actions don't read as nav peers"
   muting as .nav-bar__link--signout; recovers full opacity on hover. */
.nav-overlay__sublink--signout {
  opacity: 0.7;
}

/* Narrow widths — keep Accessibility / Cookies / Privacy on ONE line.
   Each item holds the WCAG 2.5.8 44px target floor, so a wrapped row costs
   88px instead of 44px — the single largest recoverable cost in the overlay's
   height budget. Font and tracking come down, target size does not.
   Its own threshold, not the wordmark break's: this row's budget is the width
   of the three links, the break's is the mark.
   Measured 2026-08-25 with DM Sans 600 (01-prd-marketing-restyle.md FR 4):
   at the default 1.7rem / 0.08em / --space-xl gap the row's natural width is
   380px against a container of viewport − 40px, so it wraps at ≤420px (was
   375px under Montserrat). The former reduction (1.5rem / 0.05em / --space-md,
   293px) no longer held at 320px; these values give 252px — 28px to spare at
   320, enough for cross-engine text-width variance. Re-measure if the face,
   the link labels or the utility-row padding change. */
@media (max-width: 420px) {
  .nav-overlay__sublink {
    font-size: 1.4rem;
    letter-spacing: 0.04em;
  }

  .nav-overlay__utility-list {
    gap: 0 var(--space-sm);
  }
}

.nav-overlay__sublink--signout:hover {
  opacity: 1;
}

/* Overlay wordmark — row 1. Full-width row centred via text-align, so it reads
   the same whether it renders on one line or wraps to two at ≤400px;
   line-height 1.1 gives the two-line case breathing room. margin-top: 6rem
   clears the fixed × toggle (top: 1rem, 4.4rem tall).

   position: relative is load-bearing, not decorative: the starscape canvas is
   absolutely positioned at z-index 1, and a positioned element paints above
   static in-flow content regardless of source order — without this the wordmark
   disappears behind the stars. Every content row here carries relative +
   z-index 2 for that reason. */
.nav-overlay__wordmark {
  position: relative;
  z-index: 2;
  flex: 0 0 auto;
  margin-top: 6rem;
  font-size: clamp(3rem, 5.5vw, 4rem);
  font-weight: var(--font-weight-wordmark-chrome);
  color: var(--color-text-light);
  line-height: 1.1;
  text-align: center;
  white-space: nowrap;
}

/* Reduce-motion switch — row 4, last in the column. align-self centres it
   without a transform, which keeps `transform` free: the shared entrance
   animation animates translateY on sibling rows, and a centring transform here
   would be the same conflict class recorded on the utility row.

   The pill backdrop is now decorative — nothing scrolls behind it any more —
   kept so the control still reads as docked rather than floating text.
   The switch itself is the shared .motion-toggle block (§ Block — Motion Toggle). */
.nav-overlay__motion {
  position: relative;
  z-index: 2;
  flex: 0 0 auto;
  align-self: center;
  margin-bottom: var(--space-md);
  padding: var(--space-xs) var(--space-sm);
  background-color: var(--color-bg-dark);
  border-radius: 999px;
}

/* Wordmark line break — present in the markup but hidden by default, so the
   mark renders on one line at all but phone widths. Revealed at ≤480px (the
   site's phone step) to force the break to "BIG BANG" / "VISIBILITY" exactly,
   rather than leaving the wrap point to the browser.
   Measured 2026-08-25 with Unbounded 600 (01-prd-marketing-restyle.md FR 4):
   at the clamp floor (3rem = 30px) the one-line mark is 384px wide; its right
   end passes under the × control from 480 down (gap to the control −6px at
   480, +4px at 500, +14px at 520) and the row reads cramped at 480, so the
   break moves up from the former 400px one-off (Montserrat's mark was ~330px)
   to the canonical 480 step. Re-measure if the face, weight or clamp changes. */
.nav-overlay__wordmark-break {
  display: none;
}

@media (max-width: 480px) {
  .nav-overlay__wordmark-break {
    display: inline;
  }
}

/* Nav container — row 2, and the only row that flexes. It takes whatever height
   is left between the wordmark and the utility row and scrolls internally once
   the link list outgrows that band. Adding nav items now just makes this band
   scroll; it can no longer push into the rows below.

   min-height: 0 is required, not stylistic: a flex item defaults to
   min-height: auto, which refuses to shrink below its content, so this row
   would grow past the overlay and overflow-y: auto would never engage.

   Vertical centring is delegated to the auto margins on .nav-overlay__list —
   NOT align-items: center, which splits overflow evenly and parks the first
   links above the scroll container's start edge where scrolling cannot reach
   them (that put HOME under the wordmark at 375x670).

   z-index 2 keeps the row above the canvas backdrop (z-index 1). */
.nav-overlay__nav {
  position: relative;
  z-index: 2;
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  justify-content: center;
  padding: var(--space-md) 0;
  /* Gutter between the scroll band and the utility row. The band's own padding
     scrolls with its content, so mid-scroll a link can sit flush against the
     band edge; this margin is outside the scroll area and therefore always
     empty. Without it SERVICES touched ACCESSIBILITY at 740x370. */
  margin-bottom: var(--space-md);
  overflow-y: auto;
}

/* Primary link list — the scroll band's only child, and the flex item that
   carries the vertical centring (2026-08-22).

   margin: auto centres the list while .nav-overlay__nav has free space, then
   collapses to 0 once the list is taller than the band — so overflow goes only
   DOWNWARD, where the scroll container can reach it. align-items: center on the
   band would split the overflow evenly instead, parking the first links above
   the container's start edge where scrolling can never reach them (that put
   HOME under the wordmark at 375x670). Use this pattern for any centred
   scrollable region. */
.nav-overlay__list {
  list-style: none;
  padding: 0;
  margin: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-md);
}

/* Overlay nav links — large, bold, uppercase */
.nav-overlay__link {
  display: flex;
  align-items: center;
  font-size: clamp(2rem, 5vw, 3.5rem);
  font-weight: 700;
  color: var(--color-text-light);
  text-decoration: none;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  min-height: 44px;
  transition: color 200ms ease, opacity 200ms ease;
}

.nav-overlay__link:hover {
  color: var(--color-primary);
}

/* Active page link in overlay */
.nav-overlay__link--active {
  color: var(--color-primary);
}

/* Showcase: not yet launched — muted and non-interactive */
.nav-overlay__link--muted {
  opacity: 0.35;
  pointer-events: none;
}

/* Short viewports — a deliberate max-HEIGHT scale, kept from the pre-restructure
   layout for the reason recorded there: the failure is vertical, so a width
   breakpoint cannot express it. The job is now the inverse of the old block's.
   Rows can no longer collide, so these rules mitigate no overlap — they reclaim
   fixed chrome height for the one row that scrolls, keeping the whole link list
   visible without scrolling on as many devices as possible.

   720px is the threshold because it is roughly an 812pt phone once Safari's
   toolbars are subtracted (~140px) — the configuration that exposed this
   (iPhone 11 with Display Zoom "Larger Text"). Untightened, SIGN IN sat 173px
   below the visible band there; tightened, only the campaign pill overflows
   (43px). Measured 2026-08-22 at 375x670, 375x812, 360x740, 360x640, 320x568,
   414x896, 740x370. Devices too short to fit the list at any spacing (320x568,
   landscape) scroll, which is correct — the band is a scroll container by
   design, not by accident. */
@media (max-height: 720px) {
  .nav-overlay__wordmark {
    /* Clear the fixed × toggle rather than sitting beside it: a one-line mark
       reaches under the control at phone widths — the exact crowding the ≤480px
       forced break was added to prevent. Expressed as the toggle's own offset +
       height so it tracks that control instead of becoming another
       hand-computed constant. */
    margin-top: calc(var(--space-sm) + 4.4rem);
    /* One line here costs 21px instead of the 66px a two-line 3rem mark costs —
       the largest single reclaim available without dropping the brand from the
       surface entirely. Shrinking beats hiding: a size change as the budget
       tightens reads as responsive, whereas an element appearing and
       disappearing reads as a glitch.
       Measured 2026-08-25 with Unbounded 600 (01-prd-marketing-restyle.md FR 4):
       the former 2.2rem (sized for Montserrat, ~242px) renders 281px in
       Unbounded and ran under the × at 375x670 / 360x640 / 320x568; 1.9rem
       renders ~243px — the proportion the block was designed around — leaving
       ~66 / 59 / 39px to each edge at 375 / 360 / 320 wide, fully below the
       control. Re-measure if the face, weight or control size changes. */
    font-size: 1.9rem;
  }

  /* The forced ≤480px break is unnecessary at 1.9rem (the mark fits one line
     under the control) and would cost back the height just saved. Equal
     specificity to the width rule above, so later source order wins it — no
     !important needed. */
  .nav-overlay__wordmark-break {
    display: none;
  }

  .nav-overlay__nav {
    padding: var(--space-sm) 0;
  }

  .nav-overlay__list {
    gap: var(--space-sm);
  }

  .nav-overlay__utility {
    margin-bottom: var(--space-md);
  }
}

/* Landscape phones — the band is small enough that the wordmark's own size is
   worth trading down as well. At 740x370 its 6rem top margin alone was 16% of
   the viewport.
   Re-verified 2026-08-25 with Unbounded 600 (01-prd-marketing-restyle.md FR 4):
   at 2.6rem the one-line mark is 332px, sitting beside the × with 150px to
   spare at 740x370 and 100px at 640x360; the link band scrolls by design at
   these heights (list 365px in a 190px band). Value unchanged. */
@media (max-height: 520px) {
  .nav-overlay__wordmark {
    margin-top: var(--space-sm);
    font-size: 2.6rem;
  }
}

/* ---------- Canvas backdrop ---------- */

/* Star field canvas — behind all overlay content, above the dark background */
.nav-overlay__canvas {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  width: 100%;
  height: 100%;
}

/* ---------- Scroll lock and scroll-hide ---------- */

/* Prevent page scroll when overlay is open */
body.nav-open {
  overflow: hidden;
}

/**
 * Scroll-hide: JS adds .nav--hidden to slide the hamburger off the top of the screen
 * when the user scrolls down more than 80px on mobile. Transition is on the base
 * .nav__toggle so the reveal (removing nav--hidden) is also animated.
 */
.nav__toggle.nav--hidden {
  /* The box's own height, its 1rem offset from the top, and the focus ring's
     3 px + 3 px reach, so the disc, the caption pill AND a focus ring on the
     control leave the screen entirely — a bare -100 % left the pill's bottom
     10 px peeking, and -100 % − 1rem − 2px left the ring's bottom sliver
     (found on the iPhone, 2026-09-03; the old transparent button peeked the
     same 10 px, but painted nothing there). */
  transform: translateY(calc(-100% - var(--space-sm) - 8px));
}

/* ---------- Stagger entrance animation for overlay links ---------- */

@keyframes nav-link-fade-in-up {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/**
 * animation-fill-mode: backwards applies the `from` state during the delay period
 * so links are invisible before their turn. After the animation, normal styles take
 * over — including .nav-overlay__link--muted { opacity: 0.35 } for Showcase.
 */
.nav-overlay--active .nav-overlay__link {
  animation: nav-link-fade-in-up 250ms ease-out backwards;
}

/* Six per-link delays (70ms steps keep the last primary link under 500ms),
   then the utility row enters as one grouped unit — a per-link stagger across
   all ~9 overlay links would push the tail past 700ms and read as slow. */
.nav-overlay--active .nav-overlay__list > li:nth-child(1) .nav-overlay__link { animation-delay: 50ms; }
.nav-overlay--active .nav-overlay__list > li:nth-child(2) .nav-overlay__link { animation-delay: 120ms; }
.nav-overlay--active .nav-overlay__list > li:nth-child(3) .nav-overlay__link { animation-delay: 190ms; }
.nav-overlay--active .nav-overlay__list > li:nth-child(4) .nav-overlay__link { animation-delay: 260ms; }
.nav-overlay--active .nav-overlay__list > li:nth-child(5) .nav-overlay__link { animation-delay: 330ms; }
.nav-overlay--active .nav-overlay__list > li:nth-child(6) .nav-overlay__link { animation-delay: 400ms; }

.nav-overlay--active .nav-overlay__utility {
  animation: nav-link-fade-in-up 250ms ease-out backwards;
  /* 470ms keeps the 70ms step after the last primary link (400ms); it was 540ms
     when the campaign pill held the 470ms slot. */
  animation-delay: 470ms;
}

/* ==========================================================================
   Block — Service Card
   ========================================================================== */

/* Service card — light variant (used in "Everything Else" section) */
.service-card {
  display: flex;
  flex-direction: column;
  background-color: var(--color-surface-light);
  border: 1px solid var(--border-hairline);
  border-radius: 8px;
  padding: var(--space-lg);
  box-shadow: var(--shadow-1);
  transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1);
}

.service-card:hover {
  box-shadow: var(--shadow-6);
}

/* Service card — dark green variant (used in pricing section) */
.service-card--dark {
  display: flex;
  flex-direction: column;
  background-color: var(--color-surface-secondary);
  border: 1px solid var(--border-hairline-light);
  border-radius: 8px;
  padding: var(--space-lg);
  transition: background-color 300ms cubic-bezier(0.4, 0, 0.2, 1),
              border-color 300ms cubic-bezier(0.4, 0, 0.2, 1);
}

.service-card--dark:hover {
  background-color: var(--color-surface-secondary-hover);
  border-color: var(--tint-primary-edge);
}

/* Service card title */
.service-card__title {
  font-size: var(--font-size-xl);
  font-weight: 600;
  color: var(--color-text-dark);
  margin-bottom: var(--space-sm);
  text-align: center;
}

.service-card--dark .service-card__title {
  font-size: var(--font-size-large);
  color: var(--color-text-light);
  text-align: center;
}

/* Center button within flex column card */
.service-card .btn,
.service-card--dark .btn {
  align-self: center;
}

/* Service card description */
.service-card__description {
  font-size: var(--font-size-base);
  font-weight: 400;
  line-height: 1.6;
  color: var(--color-text-muted);
  margin-bottom: var(--space-md);
}

.service-card--dark .service-card__description {
  color: var(--color-text-muted-dark);
}

/* Price display — large, centered, orange value */
.service-card__price {
  font-size: var(--font-size-3xl);
  font-weight: 800;
  text-align: center;
  margin: var(--space-md) 0;
  color: var(--color-text-dark);
}

.service-card--dark .service-card__price {
  margin-top: auto;
  color: var(--color-text-light);
}

.service-card__price .price {
  color: var(--color-primary);
}

.service-card__price .price-label {
  font-size: var(--font-size-small);
  font-weight: 500;
  color: var(--color-text-muted);
}

.service-card--dark .service-card__price .price-label {
  color: var(--color-text-muted-dark);
}

/* Small note below price */
.service-card__note {
  font-size: var(--font-size-small);
  color: var(--color-text-muted);
  font-style: italic;
  text-align: center;
  margin: 0 0 var(--space-md);
}

.service-card--dark .service-card__note {
  color: var(--color-text-muted-dark);
}


/* ==========================================================================
   Exception — Accessibility
   ========================================================================== */

/* Global focus catch-all — any interactive element not covered by a
 * component-specific :focus rule above gets the standard 3px outline.
 * Uses :focus-visible so mouse users don't see the ring on click.
 */
a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible,
[tabindex]:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 3px;
}

/* ==========================================================================
   Responsive Design — Media Queries
   ========================================================================== */

/* --- Laptop (max-width: 1024px) --- */

@media (max-width: 1024px) {
  .section {
    padding-top: var(--section-padding-laptop);
    padding-bottom: var(--section-padding-laptop);
  }

  .section__container {
    max-width: 960px;
  }

  /* Navigation — the bar shows from 769px up, so this tightened padding step
     covers its narrowest band (769–1024). Measured 2026-08-25 with the Unbounded
     wordmark (01-prd-marketing-restyle.md FR 4): the mark is 217px at its
     1.7rem bar size; at 769 the first link starts 18px after it and "Sign In"
     ends 10px from the edge, one row, no overlap; at 1024 the mark-to-links gap
     is 145px. Re-measure if a nav item is added, the wordmark face or size
     changes, or the bar's hide breakpoint moves. */
  .nav-bar {
    padding: 0 var(--space-sm);
  }
}

/* --- Tablet (max-width: 768px) --- */

@media (max-width: 768px) {
  .section {
    padding-top: var(--section-padding-tablet);
    padding-bottom: var(--section-padding-tablet);
  }

  /* CTA group — stack buttons vertically, constrain width and center the pair */
  .cta-group {
    flex-direction: column;
    align-items: center;
  }

  .cta-group .btn {
    width: 100%;
    max-width: 405px;
    text-align: center;
  }

  /* Footer — Reach spans full width on its own row; Explore/Support/Connect share row 2 */
  .footer__nav-grid {
    grid-template-columns: repeat(3, 1fr);
  }

  .footer__nav-grid > *:first-child {
    grid-column: 1 / -1;
  }

  /* Navigation — hide the entire bar; hamburger takes over as the sole nav entry point */
  .nav-bar {
    display: none;
  }

  .nav__toggle {
    display: flex;
  }

  /* Announcement bar — with the nav-bar hidden, the fixed hamburger disc
     (top/right 1rem, 4.4rem round) overlaps the bar's strip; symmetric 6rem
     inline padding keeps the centered text clear of it on both sides
     (re-measured at restyle 17.2 with the disc: the text ends 6 px short of
     the disc at 375, where the bar runs three lines; the MENU caption hangs
     below the disc, under the bar's band at 480 and up) */
  .announcement-bar {
    padding-left: 6rem;
    padding-right: 6rem;
  }

  /* Footer — the ember bank swaps to its phone scene (a composition designed
     for the narrow width; a centre-crop of the desktop scene lost the side
     lobes — restyle 17.2), and the tagline's sky shrinks with it */
  .footer__bank--desktop {
    display: none;
  }

  .footer__bank--phone {
    display: block;
    height: 40rem;
  }

  .footer--ember .footer__tagline {
    margin-bottom: calc(var(--space-4xl) + var(--space-xl));
  }

}

/* --- Mobile (max-width: 480px) --- */

@media (max-width: 480px) {
  /* Section */
  .section {
    padding-top: var(--section-padding-mobile);
    padding-bottom: var(--section-padding-mobile);
  }

  .section__heading {
    font-size: var(--font-size-2xl);
  }

  /* Footer */
  .footer__nav-grid {
    grid-template-columns: 1fr;
  }

  /* Navigation — no extra overrides needed at 480px.
   * overflow-y and padding-top are set on .nav-overlay__nav in base styles,
   * ensuring links clear the top bar and scroll on very short viewports. */

}

/* --- Scroll animations --- */

/* Hide elements before their scroll animation runs. Gated behind .js so
 * no-JS users always see content, and on the root motion-reduce class —
 * NOT the bare media query — because the class is the site's one motion
 * switch (design.md § Cross-Cutting Motion Rules): the OS preference stamps
 * it via the head include's preflight, and so does the footer toggle. Until
 * 2026-08-28 this rule sat inside @media (prefers-reduced-motion:
 * no-preference), which the OS preference lifts but the toggle does not,
 * while scroll-animations.js skips all setup under the class — so a toggle
 * user got every [data-anim] block hidden with nothing to reveal it (found at
 * marketing-restyle task 11.3; shipped since v0.11.0 on every page with
 * reveals). data-anim-done is set by the JS module after animation
 * completes, restoring visibility and allowing CSS hover transitions to
 * work. */
html.js:not(.motion-reduce) [data-anim]:not([data-anim-done]) {
  opacity: 0;
}

/* --- Reduced motion --- */

@media (prefers-reduced-motion: reduce) {
  /* Buttons — shorten the color/opacity transitions. There is no lift left to
     cancel: hover movement retired sitewide 2026-08-26
     (01-prd-marketing-restyle.md FR 11). */
  .btn {
    transition: background-color 150ms ease,
                color 150ms ease,
                border-color 150ms ease,
                opacity 150ms ease;
  }

  /* Service cards — shorten the color/shadow transitions (same: no lift to cancel) */
  .service-card,
  .service-card--dark {
    transition: box-shadow 150ms ease,
                border-color 150ms ease,
                background-color 150ms ease;
  }

  /* Loading spinner — animation is essential UI feedback, keep it running */

  /* Navigation — moved to the root-class block below (restyle 17.2). */
}

/* Navigation under reduced motion — gated on the root motion-reduce class, NOT
   the media query above, because the class is the site's one motion switch
   (design.md § Cross-Cutting Motion Rules): the OS preference stamps it via the
   head include's preflight, and so does the footer toggle. Until 2026-09-03
   these rules sat inside @media (prefers-reduced-motion: reduce), which the OS
   preference matches but the site toggle does not — so a toggle user still got
   the overlay's 250 ms link stagger, its 300 ms fade and the hamburger's 300 ms
   scroll-hide slide (found by every Phase D reduced-motion pass from 12.5 on,
   measured at 17.1: 12 animations running after the overlay opened at 480;
   fixed at restyle 17.2 — the sixth instance of the class after 11.3 / 12.3 /
   13.6 / 14.3 / 15.4 and the only one in shared chrome). Functionality unaffected:
   the overlay still opens and the hamburger still hides on scroll, instantly. */
html.motion-reduce .nav__toggle,
html.motion-reduce .nav-overlay,
html.motion-reduce .nav-overlay__link,
html.motion-reduce .nav-overlay__sublink,
html.motion-reduce .nav-overlay__utility,
html.motion-reduce .nav__toggle-icon--hamburger,
html.motion-reduce .nav__toggle-icon--close {
  transition-duration: 0s !important;
  animation-duration: 0s !important;
  /* The stagger's per-link delays (50–470 ms) must go too: with the duration at
     0 s and `backwards` fill, a delayed link sits at its from-state (opacity 0)
     until its delay elapses, so the list popped in one link at a time — a
     stagger with the motion removed but the wait kept. The media-query block
     had the same gap (found at restyle 17.2 by the toggle-path census). */
  animation-delay: 0s !important;
}
