/* ═══ tokens.css — accent-re-hued for Bob Gertsen (#9C2088) ═══ */
/* ═══════════════════════════════════════════════════════════════
   D4D Site Platform — design tokens
   Source of truth: DESIGN.md (Session 0, 2026-08-14). Every value
   here is either measured from the reference set or a documented
   Session-0 decision. Components consume tokens only — no raw hex
   values in any component CSS.
   ═══════════════════════════════════════════════════════════════ */

/* ── Build-time accent re-hue hook — INKS, NOT PAPERS ─────────────
   The nine ink/paper neutrals below are the NEUTRAL-WARM DEFAULTS.
   At build time the generation pipeline re-hues the INK tokens toward
   the candidate's accent; the PAPER tokens ship VERBATIM. That split
   is what the reference sites actually did by hand — measured from
   Gertsen: its inks carry the accent hue (#170414 H 333.7 · #2A1F26
   H 340.3 vs accent #9C2088 H 335.8) while its papers stay warm bone
   (#FBF7F1 H 78.3 · #F3EBE1 H 73.7). The Session-3 ship-quality gate
   measured the cost of re-hueing both sides: --ink-950 under-tinted
   at ~3% chroma (below the 4–8% dark band) and papers drifted
   pink-lavender (≈#F6EEF4) — inks-not-papers is that fix.

   Pipeline contract (DESIGN.md "Neutral scale", amended Session 3;
   implemented in lib/generate/color.js):
     1. Convert the candidate accent to OKLCH; take its hue H.
     2. INK tokens (--ink-*): keep the token's LIGHTNESS exactly
        (lightness is locked so the contrast math in this file never
        changes per candidate), set hue = H, set chroma inside the
        4–8% dark band — OKLCH C 0.016–0.032, target C 0.024
        (chroma % per CSS Color 4 oklch(): 100% = C 0.4). The
        documented contrast floors outrank the band: chroma steps
        down if a measured pair would fail.
     3. PAPER tokens (--paper-*): emitted verbatim — warm bone is the
        cross-candidate constant. A whisper shift of at most C 0.006
        (≤1.5%) toward the accent is sanctioned; the v1 pipeline
        ships zero shift.
     4. Emit the re-hued hexes over the defaults below (a simple
        string substitution in this file — the token names are the API).
   --accent-h is registered here so the hue survives as an inspectable
   value in the built page and so any future runtime color-mix()
   upgrade has a typed hook to key off. The simple v1 pipeline only
   needs to rewrite the hex literals. */
@property --accent-h {
  syntax: '<number>';
  inherits: true;
  initial-value: 335.8;
}

:root {
  /* ── Accent system ─────────────────────────────────────────────
     --accent       candidate's color (default = Gertsen magenta, the
                    preview accent; pipeline replaces it per candidate)
     --accent-text  accessible variant for TEXT and small elements on
                    light grounds. When the chosen accent already
                    clears 4.5:1 on --paper-100 AND --paper-200 the
                    pipeline sets it equal to --accent; when it fails,
                    the pipeline preserves the hue and darkens
                    luminance until it clears (DESIGN.md: never reject
                    a candidate's color). Default #9C2088 clears at
                    6.61:1 on --paper-100 and 6.06:1 on --paper-200.
     Contrast note: white on default accent = 7.06:1 (buttons OK).
     Accent on --ink-950 is 2.61:1 — large decorative fields only,
     never text on dark. On dark grounds the text accent is --gold. */
  --accent-h: 335.8;               /* OKLCH-ish hue of --accent, set by pipeline */
  --accent: #9C2088;
  --accent-text: #9C2088;

  /* CTA gold — the one cross-candidate constant (G's --gold).
     Ratios: --gold-ink on --gold 7.28:1 · on --gold-hi 9.98:1 ·
     --gold on --ink-950 8.01:1. */
  --gold: #D9A21B;
  --gold-hi: #F0C24C;            /* hover/em-pop companion on dark grounds */
  --gold-ink: #2A1B00;           /* text on gold surfaces */

  /* ── Neutrals — nine steps, dark → light (re-hued at build) ──── */
  --ink-950: #1B1018;            /* darkest ground: hero, stats band, footer */
  --ink-900: #2A1E26;            /* second dark ground, gradient ends, modals */
  --ink-800: #2E222B;            /* body text on light — 14.20:1 on --paper-100 */
  --ink-700: #443741;            /* strong prose, <b> upgrades — 10.40:1 on --paper-100 */
  --ink-500: #6D5F69;            /* muted text — 5.60:1 on --paper-100, 5.13:1 on
                                    --paper-200; margin above the 4.5:1 AA floor is
                                    held in reserve for the accent-hue re-tinting */
  --paper-400: #DCD4C9;          /* borders and hairlines on light */
  --paper-300: #E6DFD3;          /* image placeholders, frame fills */
  --paper-200: #F3EDE2;          /* alternate light section background */
  --paper-100: #FBF7F1;          /* page background (G's --bone, verbatim) */

  /* ── On-dark text — tokenized alpha ramp, never literals ─────── */
  --on-dark-heading:   rgba(255,255,255,.87);
  --on-dark-body:      rgba(255,255,255,.84);
  --on-dark-secondary: rgba(255,255,255,.70);
  --on-dark-caption:   rgba(255,255,255,.55);
  --on-dark-legal:     rgba(255,255,255,.50);
  --on-dark-hairline:  rgba(255,255,255,.14);  /* borders/dividers on dark (G's stat rules) */
  --on-dark-ring:      rgba(255,255,255,.50);  /* ghost-button inset ring */
  --on-dark-ring-fill: rgba(255,255,255,.12);  /* ghost-button hover fill */

  /* Absolutes (so component CSS never carries a raw hex) */
  --white: #FFFFFF;
  --black: #000000;

  /* ── Type ──────────────────────────────────────────────────────
     Fraunces variable (SOFT/WONK axes) for display; Inter variable
     for body, kickers, buttons, forms. Self-hosted — see base.css
     @font-face; no font CDN at runtime. */
  --ff-d: 'Fraunces', Georgia, 'Times New Roman', serif;
  --ff-b: 'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;

  /* 1.35 ratio @ 17px base — governs any NEW size a component needs.
     17 → 23 → 31 → 42 → 56.5 → 76.2 → 103 → 139 (px at 1rem=16px) */
  --fs-0: 1.0625rem;             /*  17px — body */
  --fs-1: 1.4375rem;             /*  23px */
  --fs-2: 1.9375rem;             /*  31px */
  --fs-3: 2.625rem;              /*  42px */
  --fs-4: 3.5312rem;             /*  56.5px */
  --fs-5: 4.7625rem;             /*  76.2px */
  --fs-6: 6.4375rem;             /* 103px */
  --fs-7: 8.6875rem;             /* 139px */

  /* Adopted display clamps — Gertsen verbatim, documented off-scale
     ceilings (the 131px hero cap is a column-fit constraint, not a
     scale step). Ultrawide tiers step these up — see the min-width
     blocks at the bottom of this file. */
  --fs-hero: clamp(3.5rem, 11.5vw, 131px);
  --fs-h2:   clamp(2.05rem, 4.6vw, 3.55rem);
  --fs-h3:   clamp(1.42rem, 2.9vw, 2rem);
  --fs-stat: clamp(2.5rem, 6.2vw, 4.1rem);

  /* Line-heights (DESIGN.md system values; hero 0.9 is the deliberate
     override of G's .86 so two-line surnames with descenders never collide) */
  --lh-hero: 0.9;
  --lh-h2:   1.05;
  --lh-h3:   1.06;
  --lh-body: 1.65;

  /* Kicker — Gertsen verbatim (0.7812rem / .19em / Inter 600 upper-
     case, 34×2px currentColor leading dash, 11px gap). The dash and
     face are the section-header component's spec; the sizes live here. */
  --fs-kicker: 0.7812rem;
  --ls-kicker: .19em;
  --kicker-dash-w: 34px;
  --kicker-dash-h: 2px;
  --kicker-gap: 11px;

  /* ── Spacing — 4px base, tokenized scale ─────────────────────── */
  --sp-1:  4px;
  --sp-2:  8px;
  --sp-3:  12px;
  --sp-4:  16px;
  --sp-5:  24px;
  --sp-6:  32px;
  --sp-7:  44px;
  --sp-8:  56px;
  --sp-9:  66px;
  --sp-10: 76px;
  --sp-11: 132px;

  /* Section rhythm — G verbatim (the only fluid rhythm of the four).
     Sections vs interstitials is a real editorial distinction; keep it. */
  --rhythm-section:      clamp(72px, 10vw, 132px);
  --rhythm-interstitial: clamp(48px, 6vw, 78px);
  --rhythm-headgap:      clamp(38px, 5vw, 66px);

  /* ── Container ────────────────────────────────────────────────── */
  --wrap: 1220px;                /* 1400 ≥1700px, 1580 ≥2300px — see below */
  --pad-inline: 24px;            /* 18px ≤620px — see below */

  /* ── Header ───────────────────────────────────────────────────── */
  --head-h: 74px;                /* 66px ≤860px — shared by hero offset,
                                    scroll-margin and mobile-nav inset */

  /* ── Motion ───────────────────────────────────────────────────── */
  --ease: cubic-bezier(.22, 1, .36, 1);
  --dur: .85s;

  /* ── Z-scale ──────────────────────────────────────────────────────
     Documented ladder — components never invent z-index values.
     Fixes G's grain-at-z-90-over-header bug: page-level texture (70)
     now sits BELOW the header (80).
       texture (in-section) 1 · content-over-texture 2 · grain 70 ·
       mobile-nav 79 · header 80 · lightbox 120 · modal 130 · skip 200 */
  --z-texture:  1;
  --z-raise:    2;
  --z-grain:    70;
  --z-nav:      79;
  --z-header:   80;
  --z-lightbox: 120;
  --z-modal:    130;
  --z-skip:     200;

  /* ── Corner radius — theme: soft (default) ────────────────────────
     Two sanctioned voices, decided 2026-08-14. Chosen per site via
     data-radius on <html>; never mixed within one site. Components
     use ONLY these two tokens for any rounding. */
  --r-surface: 14px;             /* cards, frames, panels */
  --r-control: 999px;            /* buttons, pills, inputs */
}

/* Radius theme: sharp (S's Swiss hard-corner language).
   :root[…] outranks the bare :root block at any rule order. */
:root[data-radius="sharp"] {
  --r-surface: 0px;
  --r-control: 0px;
}

/* ═══════════════════════════════════════════════════════════════
   Breakpoints — the system set (G's, the only one where JS and CSS
   agree on the same numbers). Use these and no component-local
   variants:
     max-width: 1080px · 860px · 620px · 420px
     min-width: 1700px · 2300px (ultrawide tiers)
   JS that needs a breakpoint (nav collapse) keys off 860 to match.
   ═══════════════════════════════════════════════════════════════ */

@media (max-width: 860px) {
  :root { --head-h: 66px; }
}

@media (max-width: 620px) {
  :root { --pad-inline: 18px; }
}

/* Ultrawide: widen the CONTAINERS and scale the art, keep the text
   measure capped — long lines are the one thing that gets worse, not
   better, with more room (G's comment, adopted as policy). */
@media (min-width: 1700px) {
  :root {
    --wrap: 1400px;
    --fs-hero: clamp(3.5rem, 11.5vw, 150px);
    --fs-h2:   clamp(2.05rem, 4.6vw, 4.05rem);
  }
}

@media (min-width: 2300px) {
  :root { --wrap: 1580px; }
}


/* ═══ base.css ═══ */
/* ═══════════════════════════════════════════════════════════════
   D4D Site Platform — base layer
   Reset, fonts, body type, prose, rhythm classes, container,
   accessibility primitives, print. Requires tokens.css first.
   All colors are tokens — see tokens.css.
   ═══════════════════════════════════════════════════════════════ */

/* ── Self-hosted variable fonts ──────────────────────────────────
   Fraunces carries the SOFT/WONK axes DESIGN.md requires (verified
   in the shipped files: opsz 9–144, wght 100–900, SOFT 0–100,
   WONK 0–1). Inter: opsz 14–32, wght 100–900 (covers G's weight-550
   nav voice with no extra file). woff2 preferred, ttf fallback.
   No font CDN at runtime — all four reference sites violate this;
   the platform self-hosts from day one. */
@font-face {
  font-family: 'Fraunces';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('assets/fonts/Fraunces-VF.woff2') format('woff2-variations');
}
@font-face {
  font-family: 'Fraunces';
  font-style: italic;
  font-weight: 100 900;
  font-display: swap;
  src: url('assets/fonts/Fraunces-Italic-VF.woff2') format('woff2-variations');
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('assets/fonts/Inter-VF.woff2') format('woff2-variations');
}
@font-face {
  font-family: 'Inter';
  font-style: italic;
  font-weight: 100 900;
  font-display: swap;
  src: url('assets/fonts/Inter-Italic-VF.woff2') format('woff2-variations');
}

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

/* must beat any display:grid/flex a component sets on its block */
[hidden] { display: none !important; }

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

/* clip (not hidden) preserves position:sticky while killing the
   sideways drag that reveal transforms can otherwise cause */
html, body { overflow-x: clip; }

html { scroll-behavior: smooth; }

/* Anchored sections land clear of the sticky header everywhere:
   header height + 8px cushion, on every element an anchor can hit. */
:where([id]) { scroll-margin-top: calc(var(--head-h) + 8px); }

body {
  margin: 0;
  background: var(--paper-100);
  color: var(--ink-800);
  /* rem, not px — px ignores the reader's browser font-size setting
     (G's commented rationale, adopted as law) */
  font: 400 1.0625rem/1.65 var(--ff-b);
  -webkit-font-smoothing: antialiased;
  overflow-wrap: break-word;
}

/* break-word on body protects against long URLs, but display type
   must never split mid-word — a candidate's surname breaking in half
   is not acceptable */
h1, h2, h3, h4, .display {
  overflow-wrap: normal;
  word-break: normal;
  hyphens: none;
}

img, svg, picture, video { max-width: 100%; }
img { height: auto; display: block; }  /* height:auto — never distort a real photo */
a { color: inherit; }

button, input, select, textarea { font: inherit; }

/* ── Container ─────────────────────────────────────────────────── */
.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--pad-inline);
}

/* ── Section rhythm ────────────────────────────────────────────────
   .sec  — standard full-width section band
   .band — interstitial (donate band, quote band, marquee dressing):
           tighter vertical rhythm, a real editorial distinction.
   Surface modifiers keep the print flattener honest; components may
   add their own backgrounds but dark grounds must carry --dark so
   print and on-dark text tokens apply. */
.sec  { padding: var(--rhythm-section) 0; position: relative; }
.band { padding: var(--rhythm-interstitial) 0; position: relative; }

.sec--alt  { background: var(--paper-200); }
.sec--dark, .band--dark {
  background: var(--ink-950);
  color: var(--on-dark-body);
}

/* Section header block (kicker + h2 + optional sub live inside;
   their own specs belong to the section-header component) */
.sec__head { margin-bottom: var(--rhythm-headgap); }

/* ── Prose ─────────────────────────────────────────────────────────
   54ch lands at ~68 real characters (Inter's "0" is wider than its
   average glyph) and, being font-relative, holds the measure at
   every breakpoint (G's commented math, kept). */
.prose { max-width: 54ch; }
.prose p { margin: 0 0 1.05em; }
.prose b, .prose strong { font-weight: 650; color: var(--ink-700); }

/* Measure utilities — DESIGN.md: 52ch for section subs and hero
   ledes, 44ch for donate copy. */
.measure-lede   { max-width: 52ch; }
.measure-narrow { max-width: 44ch; }

/* ── Accessibility primitives ──────────────────────────────────── */
.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: var(--z-skip);
  background: var(--accent);
  color: var(--white);
  padding: var(--sp-3) var(--sp-4);
  border-radius: 0 0 var(--r-surface) 0;
  text-decoration: none;
  font: 650 0.9375rem/1 var(--ff-b);
}
.skip:focus { left: 0; }

/* Two-layer focus indicator — survives EVERY ground (a gold-only
   outline is 2.15:1 on --paper-100 / 1.97:1 on --paper-200 — a
   WCAG 1.4.11 failure on light sections). The 6px --ink-950 shadow
   fills the 3px outline offset, so the composite reads inner 3px
   ink ring → outer 3px gold ring. The two layers are 8.01:1 against
   EACH OTHER, so whichever layer matches the ground, the other
   still carries the indicator at ≥3:1.
   Computed ratios (WCAG relative luminance, this file's tokens):
     ink-950 layer: 17.26:1 vs --paper-100 · 15.81:1 vs --paper-200
                    · 8.01:1 vs --gold bands (1:1 on --ink-950
                    grounds, where the gold layer carries at 8.01:1)
     gold layer:     8.01:1 vs --ink-950 grounds · 8.01:1 vs the ink
                    ring (2.15:1 / 1.97:1 vs paper-100/200 and 1:1
                    on gold bands — the ink layer carries those)
   No explicit radius: outline and shadow both follow each element's
   own corner shape. The skip link inherits this same two-layer ring
   when focused — its accent chip + white text sit inside it
   unchanged, so the variant stays coherent with the house ring. */
:focus-visible {
  outline: 3px solid var(--gold);
  outline-offset: 3px;
  box-shadow: 0 0 0 6px var(--ink-950);
}

/* ── Reduced motion (global floor) ─────────────────────────────────
   The reveal system's animation styles live in motion.css inside
   @media (prefers-reduced-motion: no-preference) — hidden states are
   applied by JS only, so no extra un-hiding is needed here. This
   block is the belt-and-suspenders floor for everything else. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
}

/* ── Body type responsive tiers (G verbatim) ───────────────────── */
@media (max-width: 620px) {
  body { font-size: 1.0312rem; }
}
@media (min-width: 1700px) {
  body { font-size: 1.125rem; }
}
@media (min-width: 2300px) {
  body { font-size: 1.1875rem; }
}

/* ── Print ─────────────────────────────────────────────────────────
   A volunteer printing the priorities page gets readable paper:
   chrome and motion hidden, dark bands flattened to white.
   Foundation-level flatten: the --on-dark-* ramp itself goes black,
   so EVERY on-dark consumer (formcard heads, stat labels, footer
   legal, hairlines, ghost rings…) flattens at once — no component
   needs its own print rules. */
@media print {
  :root {
    --on-dark-heading:   var(--black);
    --on-dark-body:      var(--black);
    --on-dark-secondary: var(--black);
    --on-dark-caption:   var(--black);
    --on-dark-legal:     var(--black);
    --on-dark-hairline:  var(--black);
    --on-dark-ring:      var(--black);
  }
  /* .dmodal removed — Decision 27 killed the donate popup; --z-modal stays reserved in tokens.css */
  .site-head, .mobile-nav, .skip, .marquee,
  .tex-grain, .hero__scroll, .lb {
    display: none !important;
  }
  body { background: var(--white); color: var(--black); }
  .sec--dark, .band--dark, .stats, .foot {
    background: var(--white) !important;
    color: var(--black) !important;
  }
}


/* ═══ motion.css ═══ */
/* ═══════════════════════════════════════════════════════════════
   D4D Site Platform — motion (reveal system CSS)
   Pairs with reveal.js. Requires tokens.css.

   THE DOUBLE GATE (R's pattern — the only fully safe one of the
   four reference implementations):
     Gate 1 — the hidden state (.is-prep) is applied by JS ONLY.
              Base CSS never hides content: no JS, no hiding, and
              the page reads exactly as authored ("readable before
              JS executes" is true by construction, not by promise).
     Gate 2 — every animation style lives inside
              @media (prefers-reduced-motion: no-preference).
              reveal.js additionally early-returns under reduced
              motion, so the classes are never even applied.

   Opt-in from components:
     class="reveal"                     one element
     class="reveal-group"               parent; reveal.js opts in each
                                        child and staggers them 0…N−1
     data-d="3"                         manual stagger index override

   Presentation is G's blur+stagger: 26px rise, 5px blur, 85ms per
   stagger step, house ease.

   QA kill switch: ?noanim=1 — reveal.js sets data-noanim on <html>
   and never arms; the rule at the bottom also freezes every other
   animation (marquee, orbs, sheen) for clean screenshots. */

@media (prefers-reduced-motion: no-preference) {
  .reveal.is-prep {
    opacity: 0;
    transform: translateY(26px);
    filter: blur(5px);
  }
  .reveal.is-in {
    opacity: 1;
    transform: none;
    filter: blur(0);
    transition:
      opacity .8s var(--ease),
      transform .9s var(--ease),
      filter .8s var(--ease);
    transition-delay: calc(var(--d, 0) * 85ms);
  }
}

/* ?noanim=1 — deterministic frames for QA and screenshot diffing */
:root[data-noanim] *,
:root[data-noanim] *::before,
:root[data-noanim] *::after {
  animation: none !important;
  transition: none !important;
}


/* ═══ buttons.css ═══ */
/* ═══════════════════════════════════════════════════════════════
   D4D Site Platform — buttons
   Gertsen's button, tokenized. Pill (or hard corner) comes from
   --r-control — the soft/sharp radius theme, never a local value.
   Sheen wipe and lift are motion-safe enhancements only.
   Requires tokens.css.

   Variants: .btn--accent (default look) · .btn--cta (gold) ·
             .btn--ghost (on dark) · .btn--sm · .btn--full

   AA check (computed against tokens.css defaults, WCAG 2.x):
     --white on --accent #9C2088 ..................... 7.06:1  PASS
     --gold-ink #2A1B00 on --gold #D9A21B ............ 7.28:1  PASS
     --gold-ink #2A1B00 on --gold-hi #F0C24C (hover) . 9.98:1  PASS
     --white on ghost hover fill over --ink-950 ...... ≥18:1   PASS
   Hover states change ELEVATION, not surface color, for accent and
   ghost (C's amber-hover 2.4:1 failure is the bug this prevents);
   the one hover recolor (--cta → --gold-hi) is checked above.
   NOTE: --accent is candidate-chosen. The curated bank guarantees
   white-on-accent AA; for a candidate-supplied accent the pipeline
   must re-verify white-on-accent ≥ 4.5:1 and, if it fails, set the
   button face to the derived --accent-text value instead. */

.btn {
  --bg: var(--accent);
  --fg: var(--white);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* 10/15/26px are G's hand-set optical values, adopted verbatim —
     each within the sanctioned ±1–2px of the 4px scale (--sp-2+2,
     --sp-4−1, --sp-5+2). Keeping them is what keeps the button
     feeling hand-set rather than gridded. */
  gap: 10px;
  padding: 15px 26px;
  border: 0;
  border-radius: var(--r-control);
  background: var(--bg);
  color: var(--fg);
  font: 650 0.9375rem/1 var(--ff-b);
  letter-spacing: .01em;
  text-decoration: none;
  cursor: pointer;
  position: relative;
  overflow: hidden;
  isolation: isolate;
  box-shadow: 0 2px 0 rgba(0, 0, 0, .14);
  /* SYSTEM RULE — a pill label never wraps. A label broken mid-phrase
     reads as broken chrome, not typography (measured 2026-08-18: three
     buttons squeezed onto one 375px row turned "Build my site" into
     three stacked words in a ~110px pill). The ROW is responsible for
     space: give button rows flex-wrap or stack them to a column at
     narrow widths, never let them squeeze a label.
     The one sanctioned exception is .btn--wrap below. */
  white-space: nowrap;
  /* …and the floor that makes the rule above safe. nowrap alone only
     changes HOW a squeeze fails: instead of a three-line pill you get a
     silently CUT-OFF label, which is worse. `overflow: hidden` two lines
     up disables the flex item's automatic minimum size (per CSS Sizing,
     min-width:auto applies only to items whose overflow is visible), so
     a `flex: 1 1 0%` row will happily shrink a button past its own text.
     Measured 2026-08-18 before this line: "Done — back to topics" in the
     intake nav rendered scrollWidth 191 into clientWidth 166 at 375px.
     min-content restores the floor explicitly, so the row must wrap or
     stack rather than crush — the label is never the thing that gives.
     (.btn--wrap opts out of nowrap, so its min-content is one word.) */
  min-width: min-content;
}

/* Arrow slide target — inline SVG arrows inside the label */
.btn svg { flex: none; }

/* ── Variants ──────────────────────────────────────────────────── */
.btn--accent { --bg: var(--accent); --fg: var(--white); }

.btn--cta    { --bg: var(--gold);   --fg: var(--gold-ink); }

.btn--ghost {
  --bg: transparent;
  --fg: var(--white);
  box-shadow: inset 0 0 0 2px var(--on-dark-ring);
}
.btn--ghost:hover { --bg: var(--on-dark-ring-fill); }

/* 11/19px: G's optical values, within ±1–2px of --sp-3/--sp-4+3…
   19 is 3px past --sp-4 — kept verbatim from the adopted spec with
   this note, per the optical-deviation rule.
   min-height 2.75rem = the 44px touch floor (rem, so it grows with the
   reader's font setting). Without it the small pill measured 35.5px
   tall — under target everywhere it ships: the header Donate chip on
   candidate sites, Retake/Remove/Check-again in the intake. The label
   stays centered; only the hit area grows. */
.btn--sm   { padding: 11px 19px; font-size: 0.8438rem; min-height: 2.75rem; }
.btn--full { width: 100%; }

/* The one sanctioned wrap: a FULL-WIDTH button whose label is a whole
   sentence ("Approve this site — then pick my address" cannot fit one
   line at 375px at any honest size). Two balanced, centered lines set
   on purpose are not the squeezed-pill defect the nowrap rule kills.
   Never use this inside a multi-button row — rows stack instead.
   padding-block is re-cut to 12.75px so the ONE-line case still measures
   exactly 45px (12.75×2 + 15px×1.3 = 45) and sits flush with the plain
   pill beside it on a wide screen, where the label no longer needs two
   lines. Measured 2026-08-18 at 1440 before this: 49.5 next to 45.
   Two lines then land at 64.5px — still clear of the 44px floor. */
.btn--wrap {
  white-space: normal;
  line-height: 1.3;
  padding-block: 12.75px;
  text-wrap: balance;
  text-align: center;
}

/* ── Motion-safe enhancements ──────────────────────────────────────
   Sheen wipe, −3px lift, arrow slide. Everything in this block is
   decoration: with reduced motion the button is a plain solid pill
   with all states fully readable. The gold hover brightens to
   --gold-hi (9.98:1, checked above) so the recolor is safe too. */
@media (prefers-reduced-motion: no-preference) {
  .btn {
    transition: transform .35s var(--ease), box-shadow .35s var(--ease);
  }
  .btn::after { /* sheen wipe */
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    background: linear-gradient(100deg,
      transparent 20%, rgba(255, 255, 255, .32) 50%, transparent 80%);
    transform: translateX(-120%);
    transition: transform .7s var(--ease);
  }
  .btn:hover { transform: translateY(-3px); box-shadow: 0 12px 26px -10px rgba(0, 0, 0, .5); }
  .btn:hover::after { transform: translateX(120%); }
  .btn:active { transform: translateY(-1px); }
  .btn svg { transition: transform .35s var(--ease); }
  .btn:hover svg { transform: translateX(4px); }
  .btn--cta:hover { --bg: var(--gold-hi); }
}

/* ── Focus ring repair (must stay LAST in this file) ─────────────────
   .btn's own box-shadow declarations (rest 0 2px 0, ghost inset ring,
   motion-block hover lift) are equal specificity and would REPLACE
   base.css's two-layer :focus-visible ring, leaving only the gold
   outline (2.15:1 on paper-100 — a WCAG 1.4.11 failure). Re-append the
   ink layer on top of each variant's own stack; placed after the
   motion block so a hovered+focused button keeps its ring too. */
.btn:focus-visible,
.btn:hover:focus-visible {
  box-shadow: 0 0 0 6px var(--ink-950), 0 2px 0 rgba(0, 0, 0, .14);
}
.btn--ghost:focus-visible {
  box-shadow: 0 0 0 6px var(--ink-950), inset 0 0 0 2px var(--on-dark-ring);
}


/* ═══ textures.css ═══ */
/* ═══════════════════════════════════════════════════════════════
   D4D Site Platform — texture layers
   G's campaign-print physicality family, adopted 2026-08-14 with
   constraints. Pure CSS / inline-SVG data URIs — zero requests.
   Requires tokens.css.

   THE RULES (Session-0 decision, non-negotiable):
     · MAX ONE texture per section. Two textures reads as noise.
     · Every texture element carries aria-hidden="true" in markup —
       they are pure decoration and must not exist for readers.
     · NEVER on form surfaces. Inputs sit on clean ground.
     · In-section textures require position:relative on the section
       (base.css .sec/.band already set it). Put the texture div
       FIRST inside the section; interactive content that must sit
       above it raises itself with z-index: var(--z-raise).

   Z-FIX (the bug this file exists to not reproduce): G shipped the
   page grain at z-index 90, ABOVE its sticky header at 80. Here the
   grain rides at var(--z-grain) = 70 — texture never outranks
   chrome. All values from the tokens.css z-scale; no local z-index
   numbers, ever.

   Colors inside the SVG data URIs are encoded grayscale/black
   fills — they are image assets tinted by opacity and blend mode,
   not palette values, so the no-raw-hex rule does not apply inside
   the url() strings. Every CSS-side color is a token.

   Usage:
     <div class="tex-grain" aria-hidden="true"></div>       (once per page, on <body>)
     <div class="tex-topo" aria-hidden="true"></div>        (dark sections)
     <div class="tex-topo tex-topo--light" aria-hidden="true"></div>  (light sections)
     <div class="tex-halftone" aria-hidden="true"></div>    (dark panels)
     <div class="tex-ribs" aria-hidden="true"></div>        (light sections)
   ═══════════════════════════════════════════════════════════════ */

/* Paper grain — one fixed page-level wash. Multiply blend keeps it
   honest on both light and dark grounds. */
.tex-grain {
  position: fixed;
  inset: 0;
  z-index: var(--z-grain); /* BELOW the header (80) — the G fix */
  pointer-events: none;
  opacity: .30;
  mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='220' height='220' filter='url(%23n)' opacity='.55'/%3E%3C/svg%3E");
}

/* Topographic contours — county-land feel on dark grounds */
.tex-topo {
  position: absolute;
  inset: 0;
  z-index: var(--z-texture);
  pointer-events: none;
  opacity: .055;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='620' height='620' viewBox='0 0 620 620'%3E%3Cg fill='none' stroke='%23000' stroke-width='1.6'%3E%3Cpath d='M-40 300c120-90 230-40 330-96s180-120 370-70'/%3E%3Cpath d='M-40 340c120-90 230-40 330-96s180-120 370-70'/%3E%3Cpath d='M-40 384c130-96 240-44 340-104s186-126 380-74'/%3E%3Cpath d='M-40 434c140-104 250-48 352-112s194-134 396-80'/%3E%3Cpath d='M-40 492c150-112 262-52 366-120s202-142 412-86'/%3E%3Cpath d='M-40 558c160-120 274-56 380-128s210-150 428-92'/%3E%3Cpath d='M-40 232c110-82 216-36 312-88s170-112 348-66'/%3E%3Cpath d='M-40 170c100-74 202-32 292-80s160-104 328-62'/%3E%3Cpath d='M-40 112c92-68 190-30 274-74s150-96 310-58'/%3E%3Cpath d='M-40 60c84-62 178-28 256-68s140-88 292-54'/%3E%3C/g%3E%3C/svg%3E");
  background-size: 760px 760px;
}
/* Light-section variant: inverted strokes, a touch more presence */
.tex-topo--light {
  opacity: .10;
  filter: invert(1);
}

/* Halftone — screenprint feel on dark panels, fading out along a
   160° sweep so it reads as ink, not wallpaper */
.tex-halftone {
  position: absolute;
  inset: 0;
  z-index: var(--z-texture);
  pointer-events: none;
  opacity: .16;
  background-image: radial-gradient(circle at center,
    var(--on-dark-heading) 1.15px, transparent 1.2px);
  background-size: 13px 13px;
  -webkit-mask-image: linear-gradient(160deg, var(--black) 0%, transparent 62%);
  mask-image: linear-gradient(160deg, var(--black) 0%, transparent 62%);
}

/* Coroplast ribs — the vertical fluting of a real yard sign, for
   light sections. The rib stroke derives from --ink-800 via
   color-mix at 4.5% alpha, so the build-time re-hue reaches it
   through the token (the pipeline rewrites token hexes in
   tokens.css — a raw rgba here would never be re-hued). The first
   background-image is the no-color-mix fallback: the default
   --ink-800 RGB (43,36,40) at .045, frozen at the neutral-warm
   default. */
.tex-ribs {
  position: absolute;
  inset: 0;
  z-index: var(--z-texture);
  pointer-events: none;
  opacity: .5;
  background-image: repeating-linear-gradient(90deg,
    rgba(43, 36, 40, .045) 0 1px, transparent 1px 9px);
  background-image: repeating-linear-gradient(90deg,
    color-mix(in srgb, var(--ink-800) 4.5%, transparent) 0 1px,
    transparent 1px 9px);
}


/* ═══ header/header.css ═══ */
/* ═══════════════════════════════════════════════════════════════
   D4D component: header — sticky top bar + 2px scroll progress
   Ported from reference/gertsen main.css §header (measured, not
   re-imagined), values swapped to tokens. Requires tokens.css,
   base.css, buttons.css (the nav Donate CTA is a .btn).

   ARCHITECTURE (COMPONENTS.md "Platform" line, G's):
   · Solid dark is the DEFAULT, not a JS-applied state. The nav text
     is on-dark white and the header floats over light sections too —
     if JS never runs, a transparent header would leave white text on
     bone at ~1.06:1. Transparency is the *enhancement*: header.js
     adds .is-top only at the top of the page, and only when the
     composition opted in with data-transparent-top (dark hero).
   · min-height, not height — large user font sizes grow the bar
     instead of clipping (G's fix, adopted).
   · --head-h is the single shared height token: base.css global
     scroll-margin-top, the hero offset, and the mobile-nav inset all
     consume it (kills C's 78px scroll-margin vs 70px header
     hard-coding drift).
   · z-index from the token scale only: header at --z-header (80),
     above the page grain at --z-grain (70) — the G z-bug fix lives
     in the tokens, this file just consumes them.
   ═══════════════════════════════════════════════════════════════ */

.site-head {
  position: sticky;
  top: 0;
  z-index: var(--z-header);
  /* Solid fallback: full-opacity ink. The translucent+blur treatment
     below only applies where color-mix() exists, so no browser ever
     sees an un-blurred see-through bar. */
  background: var(--ink-950);
  /* Black-alpha shadows are effect constants, not palette values
     (buttons.css precedent) */
  box-shadow: 0 10px 34px -18px rgba(0, 0, 0, .9);
}

/* G's measured treatment: ink at 93% over saturate+blur */
@supports (background: color-mix(in srgb, red 50%, transparent)) {
  .site-head {
    background: color-mix(in srgb, var(--ink-950) 93%, transparent);
    -webkit-backdrop-filter: saturate(1.4) blur(14px);
    backdrop-filter: saturate(1.4) blur(14px);
  }
}

/* .is-top — applied by header.js only, and only while at the very
   top of a page that declared data-transparent-top */
.site-head.is-top {
  background: transparent;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  box-shadow: none;
}

.head__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-5);
  min-height: var(--head-h);  /* min-height, never height — see header comment */
  flex-wrap: wrap;
}

/* ── Brand: generated wordmark lockup ──────────────────────────── */
.brand {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  color: var(--white);
  text-decoration: none;
  flex: none;
  min-height: 44px;           /* touch target */
}
.brand__name {
  font: 600 var(--fs-1)/1 var(--ff-d);
  letter-spacing: -.01em;
}
.brand__office {
  font: 600 var(--fs-kicker)/1.25 var(--ff-b);
  letter-spacing: var(--ls-kicker);
  text-transform: uppercase;
  color: var(--on-dark-secondary);
  border-left: 1px solid var(--on-dark-hairline);
  padding-left: var(--sp-3);
}
/* Transparent-top contrast step-up — the .hero--accent context fix.
   While .is-top the bar floats on the hero's own ground. Over the
   photo variant that ground is the ink-950 stack (--on-dark-secondary
   there = 9.34:1, fine) — but over .hero--accent it is the candidate
   ACCENT, and .70 white composited on the default accent #9C2088 is
   only 4.18:1 (< 4.5). Stepped up to the body ramp while transparent:
   .84 white on #9C2088 = 5.39:1 ✓ (computed 2026-08-15; candidate-
   supplied accents get the same pipeline re-verification as .btn).
   Solid states keep the quieter secondary step — this is why the
   accent-hero composition does NOT have to drop data-transparent-top. */
.site-head.is-top .brand__office { color: var(--on-dark-body); }

/* Uploaded-logo variant (constrained per DESIGN.md: header only,
   capped height; "never scaled beyond native resolution" is enforced
   by the pipeline at build time, not by CSS) */
.brand--logo img {
  /* Explicit height, not max-height: an SVG logo with only a viewBox has
     no intrinsic pixel size, and max-height+width:auto collapses it to
     0x0 (measured in the 2026-08-15 re-gate). height forces the box;
     width:auto keeps the viewBox aspect. */
  height: 44px;
  width: auto;
}

/* ── Desktop nav ───────────────────────────────────────────────────
   Everything link-ish is scoped :not(.btn) so the Donate CTA stays a
   pure buttons.css .btn (G let its .nav a font bleed into the button
   via cascade order — not carried forward). */
.nav {
  display: flex;
  align-items: center;
  gap: 6px;                   /* 6 = --sp-2 −2, G optical */
}
.nav a:not(.btn) {
  color: var(--on-dark-heading);
  text-decoration: none;
  /* 0.9062rem: G measured, adopted verbatim. Weight 550 is G's nav
     voice — Divergence 10 kept as-is; the Inter VF covers 550 with
     no extra file (foundation note). */
  font: 550 0.9062rem/1 var(--ff-b);
  padding: 11px 14px;         /* 11 = --sp-3 −1 · 14 = --sp-3 +2, G optical */
  border-radius: var(--r-control);
  display: inline-flex;
  align-items: center;
  min-height: 44px;           /* touch target */
  position: relative;
}
/* Gold underline wipe — state here, transition in the motion gate */
.nav a:not(.btn)::after {
  content: "";
  position: absolute;
  left: 14px;                 /* matches the link's inline padding */
  right: 14px;
  bottom: 6px;                /* 6 = --sp-2 −2, G optical */
  height: 2px;
  background: var(--gold);
  transform: scaleX(0);
  transform-origin: left;
}
.nav a:not(.btn):hover,
.nav a:not(.btn):focus-visible { color: var(--white); }
.nav a:not(.btn):hover::after,
.nav a:not(.btn):focus-visible::after { transform: scaleX(1); }

/* ── Burger (behavior in ../mobile-nav/mobile-nav.js) ──────────── */
.burger {
  display: none;              /* shown ≤860 below */
  width: 44px;                /* touch target */
  height: 44px;
  border: 0;
  background: transparent;
  cursor: pointer;
  padding: 10px;              /* 10 = --sp-2 +2, G optical */
  flex: none;
}
.burger span {
  display: block;
  height: 2px;
  background: var(--white);
  margin: 5px 0;              /* 5 = --sp-1 +1, G optical */
}
/* Three fixed spans — a fixed-child icon, not a stagger ladder */
.burger[aria-expanded="true"] span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.burger[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.burger[aria-expanded="true"] span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

/* ── 2px scroll-progress rule ──────────────────────────────────────
   Hairline + gradient fill riding the header's bottom edge. Hidden
   while transparent over the hero; visible in the solid state — so
   the no-JS page (never .is-top) shows a quiet hairline with a
   0-width bar, which is correct. */
.head__rule {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2px;
  /* G's white@.10 consolidated to the ramp token (the named steps
     are the system's cleanup of G's ad-hoc alpha sprawl) */
  background: var(--on-dark-hairline);
  opacity: 0;
}
.site-head:not(.is-top) .head__rule { opacity: 1; }
.head__bar {
  display: block;
  height: 100%;
  width: 0%;                  /* set by header.js */
  background: linear-gradient(90deg, var(--gold), var(--accent));
}

/* ── Motion-safe enhancements only ─────────────────────────────── */
@media (prefers-reduced-motion: no-preference) {
  /* backdrop-filter deliberately NOT transitioned (G did; it janks
     cross-browser — the background fade carries the effect) */
  .site-head { transition: background .45s var(--ease), box-shadow .45s var(--ease); }
  .brand { transition: transform .4s var(--ease); }
  .brand:hover { transform: scale(1.035); }
  .nav a:not(.btn) { transition: color .3s var(--ease); }
  .nav a:not(.btn)::after { transition: transform .4s var(--ease); }
  .burger span { transition: transform .38s var(--ease), opacity .25s; }
  .head__rule { transition: opacity .4s; }
  .head__bar { transition: width .12s linear; }
}

/* ── Breakpoints — system set only ─────────────────────────────── */
@media (max-width: 860px) {
  /* nav collapses to the burger; mobile-nav.js keys off the same 860 */
  .nav { display: none; }
  .burger { display: block; }
}

@media (max-width: 420px) {
  /* narrowest phones: name only — the brand aria-label still carries
     the full lockup */
  .brand__office { display: none; }
}


/* ═══ mobile-nav/mobile-nav.css ═══ */
/* ═══════════════════════════════════════════════════════════════
   D4D component: mobile-nav — full-screen overlay (G's pattern)
   Ported from reference/gertsen main.css §mobile-nav, tokenized.
   Requires tokens.css, base.css. Behavior: mobile-nav.js.

   THE DOUBLE GATE, honored here the same way as the reveal system:
   · Base CSS never hides the links. G shipped opacity:0 +
     translateY on them unconditionally — not carried forward. The
     entrance animation's from-state does the hiding, it exists only
     inside the no-preference media block, and it only runs when
     mobile-nav.js adds .is-open (JS-applied, in the rAF before
     first paint, so links never flash).
   · Under reduced motion the links simply appear. Under ?noanim=1
     motion.css freezes the animation globally.

   The overlay slots under the header: inset starts at --head-h
   (74/66 — the same token the header and base.css scroll-margin
   consume) and rides at --z-nav (79), below the header bar (80), so
   the burger stays clickable above it.
   ═══════════════════════════════════════════════════════════════ */

.mobile-nav {
  position: fixed;
  inset: var(--head-h) 0 0;
  z-index: var(--z-nav);
  /* ink-900 → ink-950: the neutrals are re-hued toward the accent at
     build time, which reproduces G's accent-dark gradient for every
     candidate without touching this file */
  background: linear-gradient(170deg, var(--ink-900), var(--ink-950));
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--sp-1);
  padding: var(--sp-5) 30px;    /* 30 = --sp-6 −2, G optical */
  overflow-y: auto;
  overscroll-behavior: contain; /* keep rubber-band scroll inside the panel */
}

/* Display-face links — visible by default (no static hidden state) */
.mobile-nav a {
  color: var(--on-dark-heading);
  text-decoration: none;
  font-family: var(--ff-d);
  font-weight: 600;
  /* fluid display size built from ladder tokens; lands within ~1–2px
     of G's measured clamp(1.5rem, 6vw, 2.1rem) */
  font-size: clamp(var(--fs-1), 6vw, var(--fs-2));
  padding: 13px 0;              /* 13 = --sp-3 +1, G optical; rows land ≥44px tall */
  border-bottom: 1px solid var(--on-dark-hairline);
  flex: none;
}
.mobile-nav a:hover,
.mobile-nav a:focus-visible { color: var(--white); }

/* Social echo — body face, gold, visually separated from the list */
.mobile-nav a.mobile-nav__social {
  color: var(--gold-hi);
  border-bottom: 0;
  font: 600 var(--fs-0)/1.2 var(--ff-b);  /* ≈ G's 1.15rem, ladder-normalized */
  margin-top: var(--sp-4);
}
.mobile-nav a.mobile-nav__social:hover,
.mobile-nav a.mobile-nav__social:focus-visible { color: var(--gold); }

/* ── Motion-safe entrance ──────────────────────────────────────────
   Stagger is GENERATED for N children: mobile-nav.js sets --i on
   every link (0…N−1). G's hand-written :nth-child(1)–(7) ladder left
   links 8–9 animating at zero delay — that bug is unauthorable here. */
@media (prefers-reduced-motion: no-preference) {
  .mobile-nav.is-open a {
    animation: mobileNavIn .5s var(--ease) both;
    animation-delay: calc(50ms + var(--i, 0) * 50ms);
  }
  @keyframes mobileNavIn {
    from { opacity: 0; transform: translateY(18px); }
    to   { opacity: 1; transform: none; }
  }
}


/* ═══ hero/hero.css ═══ */
/* ═══════════════════════════════════════════════════════════════
   D4D Site Platform — hero
   Two variants, keyed to whether `background_image` exists
   (COMPONENTS.md Divergence 6, both adopted):

     .hero--photo   G's layer stack, ported measured: photo →
                    accent wash → topo texture → copy/portrait.
     .hero--accent  S/R's full-bleed accent ground: accent radial
                    (R measured) + R's dot grid, same copy/portrait
                    recipe on top.

   ONE hero recipe, ONE clamp (--fs-hero) — the fix for S's
   per-page h1 drift. The hero is its own layout primitive, never
   a .sec — the fix for R's inline style="padding:0" escape hatch.

   THE FILTER-BEFORE-MASK GOTCHA (G's comment, kept as law):
   filter and mask on the SAME element apply filter first, so a
   drop-shadow on the masked <img> is itself masked — the shadow
   pops back in as a rectangular ghost along the dissolved edge.
   Shadow lives on the <picture> WRAPPER; mask lives on the <img>.

   Layering is DOM-order + position, no invented z-index values
   (bg → wash → topo paint in source order; .hero__inner rides at
   --z-raise above the foundation texture's --z-texture).

   Requires tokens.css, base.css, buttons.css; textures.css for the
   photo variant's topo layer; motion.css/reveal.js for the copy
   stagger; hero.js ONLY when the opt-in check badge is present.

   Colors: tokens only. The white/black-alpha literals below are
   effect constants (washes, shadows, glass), not palette values —
   same doctrine as buttons.css's sheen and textures.css's inks.
   color-mix() derives accent-tinted washes from tokens; every
   color-mix background is preceded by a plain-token fallback, so
   engines without color-mix still render a readable hero.
   ═══════════════════════════════════════════════════════════════ */

.hero {
  position: relative;
  isolation: isolate;
  min-height: min(104vh, 1000px);  /* fallback for engines without svh */
  min-height: min(104svh, 1000px);
  display: flex;
  align-items: center;
  /* Slides under the transparent-at-top sticky header: the negative
     pull is exactly --head-h, the top padding hands the height back
     plus 30px (--sp-6 − 2, G optical) of breathing room. */
  padding: calc(var(--head-h) + 30px) 0 90px; /* 90px bottom: G measured,
     adopted verbatim — clears the absolute-positioned scroll cue
     (22px inset + 42px tall) with air. Documented off-scale value. */
  margin-top: calc(var(--head-h) * -1);
  overflow: hidden;
  background: var(--ink-950);
  color: var(--on-dark-body);
}

/* ── Variant grounds ───────────────────────────────────────────── */

/* Photo-backed (G): the img is layer 1 of the stack */
.hero__bg {
  position: absolute;
  inset: 0;
}
.hero__bg img {
  width: 100%;
  height: 100%;   /* beats base.css img{height:auto} on specificity */
  object-fit: cover;
}

/* Accent wash over the photo — G's wash, measured stop-for-stop and
   re-derived from tokens. G's fourth stop was a second violet hue
   (a Gertsen brand move); the platform is a one-accent system, so
   the far stop stays on --accent. Fallback first: engines without
   color-mix() get a plain ink scrim with the same left-heavy
   readability guarantee. */
.hero__wash {
  position: absolute;
  inset: 0;
  background: linear-gradient(104deg,
    rgba(0, 0, 0, .88) 6%, rgba(0, 0, 0, .60) 45%, rgba(0, 0, 0, .38) 100%);
  background:
    radial-gradient(120% 90% at 78% 18%,
      color-mix(in srgb, var(--accent) 30%, transparent), transparent 62%),
    linear-gradient(104deg,
      color-mix(in srgb, var(--ink-950) 96%, transparent) 6%,
      color-mix(in srgb,
        color-mix(in srgb, var(--accent) 40%, var(--ink-950)) 90%,
        transparent) 40%,
      color-mix(in srgb, var(--accent) 62%, transparent) 72%,
      color-mix(in srgb, var(--accent) 55%, transparent) 100%);
}

/* Hero-scoped refinement of the foundation topo texture (the photo
   variant's third layer): G ran it brighter than section topo and
   faded it along a 200° sweep so it reads as land, not wallpaper. */
.hero--photo .tex-topo {
  opacity: .13;
  -webkit-mask-image: linear-gradient(200deg, var(--black), transparent 70%);
  mask-image: linear-gradient(200deg, var(--black), transparent 70%);
}

/* Full-bleed accent (S/R): R's radial ground, measured
   (1200px 700px at 78% -10%: lit corner → accent → deepened foot),
   re-derived from tokens. Fallback: flat accent. */
.hero--accent {
  background: var(--accent);
  background: radial-gradient(1200px 700px at 78% -10%,
    color-mix(in srgb, var(--accent) 82%, var(--white)) 0%,
    var(--accent) 42%,
    color-mix(in srgb, var(--accent) 55%, var(--ink-950)) 100%);
}
/* R's dot grid, measured (26px cell, 1.4px dot, .05 white @ .5).
   Pure decoration on a pseudo-element — never in the a11y tree. */
.hero--accent::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: radial-gradient(rgba(255, 255, 255, .05) 1.4px,
    transparent 1.4px);
  background-size: 26px 26px;
  opacity: .5;
}

/* ── Grid ──────────────────────────────────────────────────────── */
.hero__inner {
  position: relative;
  z-index: var(--z-raise);  /* above the foundation topo (--z-texture) */
  display: grid;
  grid-template-columns: minmax(0, 1.06fr) minmax(0, .94fr);
  gap: 40px;                /* G measured, adopted verbatim (off-scale) */
  align-items: center;
}
/* No portrait supplied → single column, copy holds its measure */
.hero--no-portrait .hero__inner { grid-template-columns: minmax(0, 1fr); }

.hero__copy { max-width: 660px; }

/* ── Eyebrow pill (hero-only object — NOT the section kicker) ──── */
.hero__eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 10px;                          /* --sp-2 + 2, G optical */
  font: 600 var(--fs-kicker)/1 var(--ff-b);
  letter-spacing: .2em;               /* G measured: eyebrow runs .01 wider
                                         than the .19em section kicker */
  text-transform: uppercase;
  color: var(--on-dark-body);         /* G's .82 → ramp step .84 */
  background: rgba(255, 255, 255, .09);   /* glass fill — effect constant */
  border: 1px solid rgba(255, 255, 255, .18);
  padding: 9px 16px;                  /* --sp-2 +1 / --sp-4, G optical */
  border-radius: var(--r-control);
  margin: 0 0 22px;                   /* --sp-5 − 2, G optical */
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
.hero__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;   /* geometric circle, not a themed corner */
  background: var(--gold);
  flex: none;
}

/* ── Name ──────────────────────────────────────────────────────── */
.hero__title {
  font-family: var(--ff-d);
  font-weight: 600;
  font-size: var(--fs-hero);          /* the one clamp — S's drift fix */
  line-height: var(--lh-hero);        /* 0.9: the descender-safe override */
  letter-spacing: -.035em;
  color: var(--on-dark-heading);
  margin: 0 0 20px;                   /* G measured, adopted verbatim */
  font-variation-settings: 'SOFT' 25, 'WONK' 1;
}
.hero__title .line { display: block; overflow: hidden; }
.hero__title em {
  font-style: italic;
  color: var(--gold-hi);
  font-variation-settings: 'SOFT' 55, 'WONK' 1;
  padding-right: .06em;   /* italic overshoot clearance (G comment) */
}

/* ── Office line + district chip ───────────────────────────────── */
.hero__for {
  font-family: var(--ff-d);
  font-size: clamp(1.15rem, 2.5vw, 1.62rem);  /* G measured, adopted */
  font-weight: 400;
  color: var(--on-dark-heading);      /* G's .94 → ramp top step .87 */
  margin: 0 0 20px;                   /* G measured, adopted verbatim */
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-3);
}
.hero__for b { font-weight: 650; }
.hero__chip {
  font: 650 0.75rem/1 var(--ff-b);
  letter-spacing: .14em;
  text-transform: uppercase;
  background: var(--gold);
  color: var(--gold-ink);
  padding: 7px 13px;                  /* --sp-2 −1 / --sp-3 +1, G optical */
  border-radius: var(--r-control);
}

/* ── Lede, CTAs, date ──────────────────────────────────────────── */
.hero__lede {
  max-width: 52ch;                    /* DESIGN.md hero-lede measure */
  color: var(--on-dark-body);
  font-size: 1.06em;
  margin: 0 0 30px;                   /* --sp-6 − 2, G optical */
}
.hero__cta {
  display: flex;
  gap: 13px;                          /* --sp-3 + 1, G optical */
  flex-wrap: wrap;
  margin-bottom: 28px;                /* G measured, adopted verbatim */
}
.hero__date {
  display: flex;
  align-items: center;
  gap: 10px;                          /* --sp-2 + 2, G optical */
  flex-wrap: wrap;
  font: 500 0.875rem/1.4 var(--ff-b);
  color: var(--on-dark-secondary);    /* G's .68 → ramp step .70 */
  margin: 0;
  padding-top: 20px;                  /* G measured, adopted verbatim */
  border-top: 1px solid var(--on-dark-hairline);
}
.hero__date b { color: var(--gold-hi); font-weight: 650; }
.hero__date svg { flex: none; }

/* .hero--accent runs the date row on the ACCENT ground, where the
   dark-ground ramp loses its guarantee (ratios vs the default accent
   #9C2088, computed 2026-08-15):
     --on-dark-secondary (.70 white composited) 4.18:1 ✗ (< 4.5)
     --gold-hi                                  4.21:1 ✗ (< 4.5)
     --on-dark-body (.84 white composited)      5.39:1 ✓
     --white                                    7.06:1 ✓
   So the accent variant steps the row up to the body ramp and the
   date itself to pure white. --gold-hi may return on the date only
   where the pipeline verifies it ≥4.5:1 over the actual candidate
   accent (see README). */
.hero--accent .hero__date { color: var(--on-dark-body); }
.hero--accent .hero__date b { color: var(--white); }

/* ── Portrait — cutout on a stage, never boxed ─────────────────────
   Stacking is pure DOM order (all positioned, no z-index): ring
   under blob under picture under badge. */
.hero__portrait {
  display: flex;
  justify-content: center;
  align-items: flex-end;
  position: relative;
}
.hero__stage {
  position: relative;
  width: min(100%, 470px);
}
/* THE GOTCHA, applied: shadow on the wrapper… */
.hero__stage picture {
  display: block;
  position: relative;   /* positions it above the blob (DOM order) */
  filter: drop-shadow(0 26px 40px rgba(0, 0, 0, .5));
}
/* …mask on the img. The source cutout ends in a straight crop at
   the chest — dissolve it so the portrait sits in the scene instead
   of floating on a hard edge (G's comment, kept). */
.hero__stage img {
  width: 100%;
  -webkit-mask-image: linear-gradient(var(--black) 80%,
    rgba(0, 0, 0, .6) 92%, transparent 100%);
  mask-image: linear-gradient(var(--black) 80%,
    rgba(0, 0, 0, .6) 92%, transparent 100%);
}
/* Glow blob: gold + accent radial, blurred. Decorative — if
   color-mix is unsupported the declaration drops and the portrait
   simply sits without the glow. */
.hero__blob {
  position: absolute;
  inset: 8% -6% -4% -6%;
  background: radial-gradient(58% 58% at 50% 46%,
    color-mix(in srgb, var(--gold) 32%, transparent),
    color-mix(in srgb, var(--accent) 30%, transparent) 55%,
    transparent 74%);
  filter: blur(26px);
}
/* Slow dashed orbit ring (spin is motion-gated below) */
.hero__ring {
  position: absolute;
  left: 50%;
  top: 47%;
  width: 118%;
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  border: 1.5px dashed rgba(255, 255, 255, .26); /* effect constant */
  border-radius: 50%;   /* geometric circle, not a themed corner */
}

/* ── Check badge — OPT-IN (omit the svg to omit the device) ───────
   Base state is DRAWN: no JS, no observer, reduced motion, or
   ?noanim=1 all show the finished mark. hero.js applies
   .hero--armed (undrawn) only when it is actually going to animate
   — the reveal system's double gate, applied to strokes. */
.hero__badge {
  position: absolute;
  right: -8px;
  bottom: 38px;
  width: 96px;
  height: 96px;
  color: var(--gold);
  filter: drop-shadow(0 6px 16px rgba(0, 0, 0, .5));
}

/* ── Scroll cue ─────────────────────────────────────────────────────
   Optional. MUST target the first full content section after the
   hero — never a marquee or interstitial band (G shipped its cue
   anchored to a ~56px marquee sliver; that is the bug this rule
   exists for). */
.hero__scroll {
  position: absolute;
  left: 50%;
  bottom: 22px;                       /* --sp-5 − 2, G optical */
  transform: translateX(-50%);
  width: 26px;
  height: 42px;
  border: 2px solid var(--on-dark-ring);
  border-radius: var(--r-control);
  display: grid;
  place-items: start center;
  padding-top: 7px;
  z-index: var(--z-raise);
}
/* 44×44 hit-area extension: the drawn capsule is 26×42 — under the
   44px touch floor. The pseudo pads the target without changing the
   art (fixed square centered on the capsule, so it holds regardless
   of border/box-sizing math). */
.hero__scroll::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
}
.hero__scroll span {
  width: 3px;
  height: 8px;
  border-radius: var(--r-control);
  background: var(--on-dark-heading);
}

/* ── Motion (all of it) — safe-gated ───────────────────────────── */
@media (prefers-reduced-motion: no-preference) {
  /* pulsing eyebrow dot — enhancement only */
  .hero__dot {
    box-shadow: 0 0 0 0 color-mix(in srgb, var(--gold) 70%, transparent);
    animation: hero-pulse 2.6s infinite;
  }
  @keyframes hero-pulse {
    70%  { box-shadow: 0 0 0 11px transparent; }
    100% { box-shadow: 0 0 0 0 transparent; }
  }

  /* slow orbit */
  .hero__ring { animation: hero-spin 46s linear infinite; }
  @keyframes hero-spin {
    to { transform: translate(-50%, -50%) rotate(360deg); }
  }

  /* check-badge draw-on: armed by hero.js only */
  .hero--armed .hero__badge rect {
    stroke-dasharray: 416;
    stroke-dashoffset: 416;
    transition: stroke-dashoffset 1s var(--ease) .25s;
  }
  .hero--armed .hero__badge .hero__tick {
    stroke-dasharray: 150;
    stroke-dashoffset: 150;
    transition: stroke-dashoffset .7s var(--ease) 1.05s;
  }
  .hero--armed.is-in .hero__badge rect,
  .hero--armed.is-in .hero__badge .hero__tick { stroke-dashoffset: 0; }

  /* scroll-cue wheel */
  .hero__scroll span { animation: hero-wheel 1.9s var(--ease) infinite; }
  @keyframes hero-wheel {
    0%   { opacity: 0; transform: translateY(0); }
    35%  { opacity: 1; }
    100% { opacity: 0; transform: translateY(15px); }
  }
}

/* ── Responsive (system breakpoints only) ──────────────────────── */
@media (max-width: 1080px) {
  /* portrait reorders ABOVE copy — G and S both converged on this */
  .hero__inner { grid-template-columns: 1fr; gap: var(--sp-4); }
  .hero__copy { max-width: none; order: 1; }
  .hero__portrait { order: 0; justify-content: flex-start; }
  .hero__stage { width: min(66%, 330px); }
  .hero__badge { width: 70px; height: 70px; bottom: 20px; }
  .hero { min-height: 0; padding-bottom: 70px; } /* G measured, adopted */
}

@media (max-width: 620px) {
  /* fully length-bound on phones: a rem floor would push the surname
     wider than a 390px screen once the reader's font setting is
     cranked up (G's comment, kept — the one sanctioned px floor) */
  .hero__title { font-size: clamp(46px, 15.5vw, 74px); }
  .hero__cta { gap: 10px; }           /* --sp-2 + 2, G optical */
  .hero__cta .btn { flex: 1 1 100%; }
  .hero__stage { width: min(68%, 262px); }
  /* keep the pill on one line at 390px (G's comment, kept) */
  .hero__eyebrow {
    font-size: 0.6875rem;
    letter-spacing: .13em;
    padding: 8px 13px;                /* --sp-2 / --sp-3 +1, G optical */
    gap: var(--sp-2);
  }
  .hero__dot { width: 7px; height: 7px; }
}

/* Ultrawide: art scales with the container (--fs-hero steps up via
   tokens.css at these same tiers) */
@media (min-width: 1700px) {
  .hero__stage { width: min(100%, 545px); }
  .hero__badge { width: 112px; height: 112px; }
}
@media (min-width: 2300px) {
  .hero__stage { width: min(100%, 610px); }
}

/* ── Print — flatten the dark ground (base.css flattens .sec--dark
   but the hero is its own primitive, so it flattens itself) ────── */
@media print {
  .hero {
    background: var(--white) !important;
    color: var(--black) !important;
    min-height: 0;
    margin-top: 0;
    padding: var(--sp-6) 0;
  }
  .hero__bg, .hero__wash, .hero--photo .tex-topo,
  .hero__blob, .hero__ring, .hero__badge, .hero__scroll,
  .hero--accent::before {
    display: none !important;
  }
  .hero__title, .hero__for, .hero__lede, .hero__date,
  .hero__eyebrow, .hero__title em, .hero__date b {
    color: var(--black) !important;
  }
  .hero__eyebrow { background: none; border-color: var(--black); }
}


/* ═══ section-header/section-header.css ═══ */
/* ═══════════════════════════════════════════════════════════════
   D4D Site Platform — section header (kicker + h2 + optional sub)
   The system's backbone: all four reference sites converged on this
   exact trio. G's spec adopted verbatim (DESIGN.md "Kicker"), with
   the two generation-safety changes decided in Session 0:

     · text-wrap: balance on the h2 — replaces G's editorial <br>
       breaks, which cannot survive generated headlines
       (COMPONENTS.md Divergence 4).
     · ONE kicker system site-wide (R ran two on one page — the
       anti-pattern). The hero's eyebrow pill is a different object
       and lives in the hero component.

   These atoms (.kicker / .h2 / .sec__sub) are G's page-wide idiom,
   owned here and consumed by every content module. base.css owns
   the .sec__head rhythm margin; this file owns everything inside.

   Requires tokens.css + base.css. No JS. All colors are tokens.
   ═══════════════════════════════════════════════════════════════ */

/* G measured: header column caps at 850px so a long sub never runs
   the full 1220px wrap. Rhythm margin comes from base.css. */
.sec__head { max-width: 850px; }
.sec__head--center { margin-inline: auto; text-align: center; }

/* ── Kicker — DESIGN.md verbatim ───────────────────────────────────
   0.7812rem / .19em / Inter 600 uppercase, 34×2px currentColor
   leading dash, 11px gap (all from tokens). Accent-derived text on
   light grounds; gold on dark. */
.kicker {
  font: 600 var(--fs-kicker)/1 var(--ff-b);
  letter-spacing: var(--ls-kicker);
  text-transform: uppercase;
  color: var(--accent-text);
  margin: 0;
  display: flex;
  align-items: center;
  gap: var(--kicker-gap);
}
.kicker::before {
  content: "";
  width: var(--kicker-dash-w);
  height: var(--kicker-dash-h);
  background: currentColor;
  flex: none;
}
.kicker--light { color: var(--gold); }
.sec__head--center .kicker { justify-content: center; }

/* ── h2 — G's face, tokenized sizes ────────────────────────────────
   One italic <em> accent-pop maximum, chosen at generation. No hard
   <br>: balance does the breaking, and headline length is capped at
   intake instead. */
.h2 {
  font-family: var(--ff-d);
  font-weight: 600;
  font-size: var(--fs-h2);
  line-height: var(--lh-h2);
  letter-spacing: -.022em;
  margin: .18em 0 .55em;
  font-variation-settings: 'SOFT' 18, 'WONK' 1;
  text-wrap: balance;   /* the <br> replacement — harmless where unsupported */
}
.h2 em {
  font-style: italic;
  color: var(--accent-text);
  font-variation-settings: 'SOFT' 40, 'WONK' 1;
}
.h2--light { color: var(--on-dark-heading); }
.h2--light em { color: var(--gold-hi); }

/* ── Optional sub ──────────────────────────────────────────────────
   52ch measure per DESIGN.md (section subs and hero ledes). */
.sec__sub {
  max-width: 52ch;
  font-size: 1.06em;
  color: var(--ink-500);
  margin: .2em 0 0;
}
.sec__sub--light { color: var(--on-dark-secondary); }
.sec__head--center .sec__sub { margin-inline: auto; }

/* ── Print — base.css flattens dark SECTIONS with !important on the
   section, but element-level colors here would still win the
   cascade for their own boxes; flatten them explicitly. ─────────── */
@media print {
  .kicker, .kicker--light,
  .h2, .h2--light, .h2 em, .h2--light em,
  .sec__sub, .sec__sub--light {
    color: var(--black) !important;
  }
}


/* ═══ marquee/marquee.css ═══ */
/* ═══════════════════════════════════════════════════════════════
   D4D Site Platform — marquee band
   G's dual-set architecture, adopted verbatim (COMPONENTS.md: "the
   correct architecture"): two identical .marquee__set divs, second
   one aria-hidden, width:max-content track, translateX(-50%) loop —
   the loop point lands exactly where set 2 stands where set 1
   started, so the scroll is seamless at any content length. Screen
   readers hear the list ONCE.

   Both sets are GENERATED from the data array — the fix for S's
   hand-duplicated copy (~24 manual edits per slogan change).

   Default dressing: G's gold band with 3px ink borders.
   Variant: .marquee--hazard — S's hazard-stripe sandwich (7px
   ink/gold diagonals, S measured), for law-enforcement-adjacent
   archetypes (Divergence 8: G default, S variant).

   The scroll animation is motion: it lives inside
   @media (prefers-reduced-motion: no-preference). Reduced motion =
   a static band showing the first phrases. ?noanim=1 freezes it via
   motion.css's global data-noanim rule. Print hides it (base.css).

   NEVER an anchor target: this band is a ~56px sliver — G pointed
   its hero scroll cue at it, which is the bug. No id attribute.

   Requires tokens.css + base.css. No JS. All colors are tokens.
   ═══════════════════════════════════════════════════════════════ */

.marquee {
  background: var(--gold);
  color: var(--gold-ink);
  padding: 15px 0;                 /* --sp-4 − 1, G optical */
  overflow: hidden;
  position: relative;
  border-block: 3px solid var(--ink-950);
}

.marquee__track {
  display: flex;
  width: max-content;              /* track = exactly two sets wide */
}

.marquee__set {
  display: flex;
  align-items: center;
  gap: 26px;                       /* --sp-5 + 2, G optical */
  padding-right: 26px;             /* keeps the seam gap = the set gap */
}

.marquee__set span {
  font-family: var(--ff-d);
  font-weight: 600;
  font-style: italic;
  font-size: clamp(1.05rem, 2.3vw, 1.5rem);  /* G measured, adopted */
  white-space: nowrap;
  font-variation-settings: 'SOFT' 40, 'WONK' 1;
  overflow-wrap: normal;           /* town names never split mid-word */
}

/* separator glyph (✦ default, ★ variant) — always aria-hidden in
   markup; visually quieted like G */
.marquee__set i {
  font-style: normal;
  opacity: .5;
  font-size: .8em;
}

/* ── Hazard-stripe variant (S measured: 7px strips, ink/gold
   diagonals at -45°, 12px/26px rhythm) ─────────────────────────── */
.marquee--hazard {
  border-block: 0;
  padding: 22px 0;                 /* 15px band rhythm + 7px stripe
                                      clearance each side */
}
.marquee--hazard::before,
.marquee--hazard::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  height: 7px;
  background: repeating-linear-gradient(-45deg,
    var(--ink-950) 0 12px, var(--gold) 12px 26px);
}
.marquee--hazard::before { top: 0; }
.marquee--hazard::after { bottom: 0; }

/* ── Motion — the scroll IS motion, so all of it is gated ──────── */
@media (prefers-reduced-motion: no-preference) {
  .marquee__track {
    animation: marquee-march 46s linear infinite;  /* G measured */
  }
  .marquee:hover .marquee__track {
    animation-play-state: paused;                  /* G's hover pause */
  }
  @keyframes marquee-march {
    to { transform: translateX(-50%); }
  }
}

/* Ultrawide: the band scales with the page art (G measured) */
@media (min-width: 2300px) {
  .marquee__set span { font-size: 1.7rem; }
}


/* ═══ stats/stats.css ═══ */
/* ═══════════════════════════════════════════════════════════════
   D4D Site Platform — stats band + counters
   Spec: COMPONENTS.md "Stats band + counters" — S's markup contract
   (final values pre-rendered in the HTML) + G's presentation (dark
   band, Fraunces numerals, hairline dividers). Presentation values
   measured from reference/gertsen/assets/css/main.css (.stats/.stat)
   and swapped to tokens. Requires tokens.css + base.css.

   Markup contract (see stats.html / README.md):
     section.stats > .wrap.stats__grid > .stats__item ×4
     .stats__n carries the FORMATTED FINAL VALUE as text content,
     plus data-count / data-suffix / data-plain for stats.js.
   No JS, or reduced motion, or ?noanim=1 → this CSS alone renders
   the finished band. stats.js is pure enhancement.
   ═══════════════════════════════════════════════════════════════ */

.stats {
  background: var(--ink-950);
  color: var(--on-dark-body);
  /* 58px = --sp-8 + 2px — G's hand-set optical value, kept verbatim
     per the sanctioned ±1–2px deviation rule */
  padding: 58px 0;
  position: relative; /* texture anchor (markup may opt into ONE
                         foundation texture, e.g. .tex-halftone) */
  overflow: hidden;
}

.stats__grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  /* 26px = --sp-5 + 2px optical (G verbatim) */
  gap: 26px;
  position: relative;
  z-index: var(--z-raise); /* content above an opted-in texture layer */
}

.stats__item {
  text-align: center;
  /* 6px/10px = --sp-1 + 2 / --sp-2 + 2 optical (G verbatim) */
  padding: 6px 10px;
  position: relative;
}

/* Hairline divider between figures — lives at half the grid gap */
.stats__item + .stats__item::before {
  content: "";
  position: absolute;
  left: -13px; /* half of the 26px gap */
  top: 12%;
  bottom: 12%;
  width: 1px;
  background: var(--on-dark-hairline);
}

.stats__n {
  display: block;
  font-family: var(--ff-d);
  font-weight: 700;
  font-size: var(--fs-stat);
  line-height: 1;
  color: var(--gold-hi);
  letter-spacing: -.03em;
  /* tabular figures so the width is stable while stats.js counts */
  font-variant-numeric: tabular-nums;
  font-variation-settings: 'SOFT' 12, 'WONK' 0;
}

.stats__l {
  display: block;
  margin-top: 11px; /* --sp-3 − 1px optical (G verbatim) */
  /* 0.8438rem — G's measured label size, ported verbatim (same
     precedent as buttons.css .btn--sm) */
  font-size: 0.8438rem;
  line-height: 1.42;
  color: var(--on-dark-secondary);
  letter-spacing: .01em;
}

/* ── Responsive (system breakpoints only) ──────────────────────────
   ONE rule per breakpoint — C only stayed 2×2 on mobile because its
   rule was duplicated across two media queries; here each layout
   state has a single source. */
@media (max-width: 860px) {
  .stats__grid {
    grid-template-columns: repeat(2, 1fr);
    /* 32px = --sp-6 · 18px = --sp-4 + 2px optical (G verbatim) */
    gap: 32px 18px;
  }
  .stats__item + .stats__item::before { left: -9px; } /* half of 18px gap */
  /* row starts (items 1 and 3) carry no divider in a 2×2 grid */
  .stats__item:nth-child(odd)::before { display: none; }
}

@media (max-width: 420px) {
  .stats__grid {
    grid-template-columns: 1fr;
    gap: 26px; /* --sp-5 + 2px optical (G verbatim) */
  }
  .stats__item + .stats__item::before { display: none; }
  .stats__item {
    padding-top: 22px; /* --sp-5 − 2px optical (G verbatim) */
    border-top: 1px solid var(--on-dark-hairline);
  }
}

@media (min-width: 2300px) {
  .stats__grid {
    max-width: 1400px;
    margin-inline: auto;
  }
}

/* ── Print ─────────────────────────────────────────────────────────
   base.css flattens the .stats ground to white; this component's
   explicit on-dark/gold text must flatten with it (G printed
   gold-on-white numerals and white-alpha labels — near-invisible). */
@media print {
  .stats__n, .stats__l { color: var(--black); }
}


/* ═══ bio/bio.css ═══ */
/* ═══════════════════════════════════════════════════════════════
   D4D Site Platform — bio ("Meet <candidate>")
   Source: Gertsen .about (quality bar) for grid, frames, and the
   mobile inset geometry; Carr/Roebuck for the sticky photo aside
   (the platform choice above 1080px — COMPONENTS.md "Bio / prose").
   Requires tokens.css + base.css (.wrap/.sec/.prose).

   Fixes shipped (COMPONENTS.md "Bio / prose"):
   · R's uncapped ~78ch bio column — copy always sits in base.css
     .prose (54ch, the worst-measure fix).
   · G's caption-under-inset truncation — the main frame's caption
     is visually dropped ≤620px but KEPT in the a11y tree.
   · Frame hover zoom + its transition live entirely inside
     @media (prefers-reduced-motion: no-preference).

   NOTE for integrator: .bio__kicker / .bio__headline implement the
   DESIGN.md kicker + h2 specs from tokens, self-contained. If the
   section-header component exposes shared .kicker/.h2 classes, the
   pipeline may swap these for those — same tokens, same output.

   Shadow/scrim blacks below are rgba(0,0,0,…) per the foundation
   precedent in buttons.css (shadows are not palette colors). */

.bio__grid {
  display: grid;
  /* G's asymmetric pair, verbatim; align start (not G's center)
     because the platform aside is sticky — a centered grid item
     shrink-wraps and sticky has nothing to travel in (C and R, the
     two sticky references, both use start) */
  grid-template-columns: minmax(0, .92fr) minmax(0, 1.08fr);
  gap: clamp(34px, 6vw, 80px);
  align-items: start;
}

/* ── Photo aside ─────────────────────────────────────────────────
   Sticky above 1080px (C: top 96px, R: 88px — ours derives from the
   header token instead of a magic number). */
.bio__media {
  position: sticky;
  top: calc(var(--head-h) + var(--sp-6));
}

/* Two-photo variant: main frame + small inset overlapping its
   bottom-right corner. The padding reserves the overlap so the
   inset never covers a face or a sign in the photo underneath
   (G's comment, kept). 104px is G's hand-set clearance for the
   desktop layout; the ≤620px block below replaces both numbers
   with the locked percentage pair. */
.bio__media--inset { padding-bottom: 104px; }

/* ── Photo frame ─────────────────────────────────────────────────
   G's .frame, tokenized. Radius from the site theme — soft 14px /
   sharp 0 — never a local value. */
.bio__frame {
  margin: 0;
  position: relative;
  border-radius: var(--r-surface);
  overflow: hidden;
  background: var(--paper-300);
}
.bio__frame img,
.bio__frame svg {
  width: 100%;
  height: auto;
  display: block;
}
.bio__frame::after {
  /* hairline ring keeps light photo edges off light grounds */
  content: "";
  position: absolute;
  inset: 0;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .08);
  border-radius: var(--r-surface);
  pointer-events: none;
}
.bio__frame figcaption {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  /* 26/18/13px: G's hand-set optical caption inset, verbatim
     (--sp-5+2 / --sp-4+2 / --sp-3+1 — within the sanctioned ±2) */
  padding: 26px 18px 13px;
  font: 600 0.7188rem/1.3 var(--ff-b);
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--white);
  background: linear-gradient(transparent, rgba(0, 0, 0, .72));
}

.bio__frame--tilt   { transform: rotate(-1.7deg); }
.bio__frame--tilt-r { transform: rotate(2.1deg); }
.bio__frame--small  { width: 46%; }

.bio__media--inset .bio__frame--tilt {
  box-shadow: 0 26px 54px -26px rgba(0, 0, 0, .55);
}
.bio__media--inset .bio__frame--small {
  position: absolute;
  right: -22px;
  bottom: 0;
  box-shadow: 0 20px 44px -22px rgba(0, 0, 0, .6);
}

/* Hover zoom is decoration — gated, with its transition (G shipped
   the transition unguarded) */
@media (prefers-reduced-motion: no-preference) {
  .bio__frame img,
  .bio__frame svg { transition: transform .9s var(--ease); }
  .bio__frame:hover img,
  .bio__frame:hover svg { transform: scale(1.04); }
}

/* ── Copy column ───────────────────────────────────────────────── */

/* Kicker — DESIGN.md spec verbatim, from the kicker tokens.
   Accent-text (not raw accent) so a failing candidate color still
   clears AA on light grounds. */
.bio__kicker {
  font: 600 var(--fs-kicker)/1 var(--ff-b);
  letter-spacing: var(--ls-kicker);
  text-transform: uppercase;
  color: var(--accent-text);
  margin: 0;
  display: flex;
  align-items: center;
  gap: var(--kicker-gap);
}
.bio__kicker::before {
  content: "";
  width: var(--kicker-dash-w);
  height: var(--kicker-dash-h);
  background: currentColor;
  flex: none;
}

/* Headline — G's .h2 spec on the adopted clamp; line-height from
   the system token (1.05, the two-line-surname fix). Generated
   headlines can't be hand-broken with <br>, so text-wrap:balance
   carries the editorial break (COMPONENTS.md "Section header"). */
.bio__headline {
  font-family: var(--ff-d);
  font-weight: 600;
  font-size: var(--fs-h2);
  line-height: var(--lh-h2);
  letter-spacing: -.022em;
  margin: .18em 0 .55em;
  color: var(--ink-800);
  font-variation-settings: 'SOFT' 18, 'WONK' 1;
  text-wrap: balance;
}
.bio__headline em {
  font-style: italic;
  color: var(--accent-text);
  font-variation-settings: 'SOFT' 40, 'WONK' 1;
}

/* Body copy lives in base.css .prose — 54ch, <b> → 650/--ink-700.
   Nothing to add here; the measure cap IS the component. */

/* ── Responsive ───────────────────────────────────────────────── */
@media (max-width: 1080px) {
  .bio__grid { grid-template-columns: 1fr; }
  /* aside stops sticking; stays RELATIVE (not static) so the
     absolutely-positioned inset frame keeps its anchor */
  .bio__media {
    position: relative;
    top: auto;
    max-width: 560px;
  }
}

@media (max-width: 620px) {
  /* G's documented jaw-clearance geometry, verbatim. On phones the
     inset was landing 54% down the main photo — straight across the
     subject's jaw. Percentage padding resolves against the container
     WIDTH, so these two numbers stay locked together at any screen
     size:
       inset height = 56% × 0.75 (4:3) = 42% of width
       padding      = 27%  →  overlap of 15%, i.e. the inset starts
                              ~80% down, well clear of a face framed
                              in the upper two-thirds. */
  .bio__frame--small { width: 56%; }
  .bio__media--inset { padding-bottom: 27%; }

  /* The inset now sits over the tail of the main frame's caption,
     truncating it mid-word. The caption stays in the a11y tree —
     visually hidden, not display:none (G's fix, kept). */
  .bio__media--inset .bio__frame--tilt figcaption {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    background: none;
  }
}


/* ═══ pull-quote/pull-quote.css ═══ */
/* ═══════════════════════════════════════════════════════════════
   D4D Site Platform — pull-quote
   The candidate's own words, set large, breaking the body column
   (DESIGN.md signature element). Gertsen's .pull blockquote spec,
   adopted verbatim and tokenized (COMPONENTS.md "Pull quote":
   "G's blockquote spec. One class, no inline re-creations").
   Requires tokens.css. In-column, light grounds — the full-bleed
   sibling is the quote-band component.

   Optical values kept verbatim from G with the sanctioned ±1–2px
   note: 34 (--sp-6+2) · 26 (--sp-5+2) · 8 (--sp-2) · 30 (--sp-6−2)
   · 12 (--sp-3). */

.pull {
  margin: 34px 0 0;
  padding: 26px 0 8px 30px;
  border-left: 4px solid var(--gold);
  position: relative;
}

.pull p {
  font-family: var(--ff-d);
  font-style: italic;
  font-weight: 400;
  /* G's hand-tuned clamp, adopted verbatim (off-ladder, documented) */
  font-size: clamp(1.28rem, 2.6vw, 1.72rem);
  line-height: 1.3;
  /* --accent-text, not raw --accent: at this size large-text AA is
     3:1 and the default clears at 6.61:1 on --paper-100; a failing
     candidate color is replaced by its derived variant upstream */
  color: var(--accent-text);
  margin: 0 0 12px;
  /* soft on pull quotes — DESIGN.md's per-context Fraunces voice */
  font-variation-settings: 'SOFT' 45, 'WONK' 1;
}

.pull cite {
  /* G verbatim: kicker size, .16em tracking (G's cite tracks 3/100em
     tighter than the .19em kicker — measured, kept) */
  font: 600 var(--fs-kicker)/1 var(--ff-b);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ink-500);
  font-style: normal;
}


/* ═══ priorities/priorities.css ═══ */
/* ═══════════════════════════════════════════════════════════════
   D4D Site Platform — priorities (numbered rows)
   Spec: COMPONENTS.md "Priorities (numbered)" — G's numbered-row
   layout, the only reference that is type-and-numeral with no icons
   (the doctrine). Values measured from reference/gertsen main.css
   (.iss) and swapped to tokens. Requires tokens.css + base.css.

   The 01–06 numerals are GENERATED (CSS counter) — never hard-coded
   spans. All four reference sites hard-coded them; G even shipped a
   vestigial `counter-reset:none`. Generated numerals cannot drift
   from list order.

   Composes on light grounds (.sec / .sec--alt). 0–6 items — build
   exactly as many as the data supplies (CLAUDE.md hard rule 1).
   ═══════════════════════════════════════════════════════════════ */

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

.prio__item {
  counter-increment: prio;
  display: grid;
  /* G's measured label/body split */
  grid-template-columns: minmax(0, 345px) minmax(0, 1fr);
  gap: clamp(20px, 4vw, 52px);            /* G's fluid gap, verbatim */
  padding: clamp(26px, 3.4vw, 38px) 0;    /* G's fluid row rhythm, verbatim */
  border-top: 1px solid var(--paper-400);
  position: relative;
}
.prio__item:last-child { border-bottom: 1px solid var(--paper-400); }

.prio__label {
  display: flex;
  align-items: baseline;
  gap: var(--sp-4);
}

/* Generated display numeral — 01, 02, … 06, in the accessible accent
   (numeral is body-size text, so it must clear AA on light grounds:
   --accent-text, never raw --accent). The second `content` line uses
   the alt-text syntax to keep the decorative numeral out of the
   accessibility tree where supported; older engines drop that
   declaration and fall back to the first. */
.prio__label::before {
  content: counter(prio, decimal-leading-zero);
  content: counter(prio, decimal-leading-zero) / "";
  font-family: var(--ff-d);
  font-weight: 700;
  font-size: var(--fs-0); /* G's 1.05rem snapped to the ladder base */
  color: var(--accent-text);
  letter-spacing: .04em;
  flex: none;
  font-variant-numeric: tabular-nums;
}

.prio__title {
  font-family: var(--ff-d);
  font-weight: 600;
  font-size: var(--fs-h3);
  line-height: var(--lh-h3);
  letter-spacing: -.018em;
  margin: 0;
  color: var(--ink-800);
  font-variation-settings: 'SOFT' 20, 'WONK' 1;
}

.prio__body { max-width: 54ch; }

/* Italic stance line — maps to SCHEMA `summary`. Fraunces italic,
   soft axis up, in the accessible accent (G used its dark accent
   step for exactly this AA reason). Clamp is G's measured value,
   ported verbatim (a component-local fluid size, same precedent as
   the adopted display clamps). */
.prio__stance {
  font-family: var(--ff-d);
  font-weight: 500;
  font-style: italic;
  font-size: clamp(1.14rem, 2.2vw, 1.4rem);
  line-height: 1.32;
  color: var(--accent-text);
  margin: 0 0 14px; /* --sp-4 − 2px optical (G verbatim) */
  font-variation-settings: 'SOFT' 42, 'WONK' 1;
}

/* Detail paragraph — maps to SCHEMA `detail` */
.prio__detail { margin: 0; color: var(--ink-700); }
.prio__detail b,
.prio__detail strong { color: var(--ink-800); font-weight: 650; }

/* ── Hover wipe — motion-safe enhancement only ─────────────────────
   G's device: a 2px accent→gold line chases the eye across the row
   on hover. Pure decoration, so the whole device lives inside the
   no-preference gate — under reduced motion there is no wipe at all,
   not an instant one. */
@media (prefers-reduced-motion: no-preference) {
  .prio__item::before {
    content: "";
    position: absolute;
    left: 0;
    top: -1px; /* sits on the row's own border */
    height: 2px;
    width: 0;
    background: linear-gradient(90deg, var(--accent), var(--gold));
    transition: width .6s var(--ease);
  }
  .prio__item:hover::before { width: 100%; }
}

/* ── Responsive (system breakpoints only) ──────────────────────── */
@media (max-width: 860px) {
  .prio__item {
    grid-template-columns: 1fr;
    gap: 14px; /* --sp-4 − 2px optical (G verbatim) */
  }
  .prio__label { gap: var(--sp-3); }
}


/* ═══ timeline/timeline.css ═══ */
/* ═══════════════════════════════════════════════════════════════
   D4D Site Platform — timeline
   Spec: COMPONENTS.md "Timeline" — G's implementation ported as-is
   (sole reference): scroll-driven fill line via --tl, per-item dots
   lit by IO, sticky photo aside. Values measured from
   reference/gertsen main.css (.work/.timeline/.tl), tokenized.
   Requires tokens.css + base.css. Composes on DARK grounds
   (.sec--dark) — every text color is from the on-dark ramp.

   Enhancement layering (the platform correction):
     · No JS: the track (::before) is fully visible, every item is
       readable, dots render in their neutral state. Nothing hides.
     · timeline.js lights dots (.is-lit) and drives the fill (--tl).
     · Reduced motion / ?noanim=1: JS sets everything lit + fill at
       100% in one shot — no scroll choreography.
     · Dot lighting uses .is-lit, NOT the reveal system's .is-in —
       in G both systems shared .is-in and two observers with
       different rootMargins could trip each other.
   ═══════════════════════════════════════════════════════════════ */

.timeline {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, .72fr);
  gap: clamp(34px, 5vw, 72px); /* G's fluid gap, verbatim */
  align-items: start;
  position: relative;
  z-index: var(--z-raise); /* content above an opted-in texture layer */
}

/* Composition sets this when the data supplies no aside photos —
   the markup then omits .timeline__aside entirely (zero-output for
   the sub-element) and the list takes the full measure. */
.timeline--noaside { grid-template-columns: minmax(0, 1fr); }

.timeline__aside {
  position: sticky;
  top: calc(var(--head-h) + 34px); /* --sp-6 + 2px optical (G verbatim) */
  display: grid;
  gap: var(--sp-5); /* G's 20px re-gridded to the 4px scale (24px) */
}

/* ── The list, track and fill ──────────────────────────────────── */
.timeline__list {
  list-style: none;
  margin: 0;
  padding: 0;
  position: relative;
}

/* Track — plain CSS, visible with or without JS */
.timeline__list::before {
  content: "";
  position: absolute;
  left: 11px;
  top: 14px;
  bottom: 14px;
  width: 2px;
  background: var(--on-dark-hairline);
}

/* Fill — enhancement only: height is driven by --tl, which ONLY
   timeline.js sets (scroll-linked, or 100% under reduced motion).
   Default 0% = no JS = just the track above. */
.timeline__list::after {
  content: "";
  position: absolute;
  left: 11px;
  top: 14px;
  width: 2px;
  height: var(--tl, 0%);
  background: linear-gradient(var(--gold), var(--accent));
}

.timeline__item {
  position: relative;
  /* left inset 46px = --sp-7 + 2px optical (G verbatim): clears the
     20px dot + gutter. Bottom: G's 40px re-gridded to --sp-7. */
  padding: 0 0 var(--sp-7) 46px;
}
.timeline__item:last-child { padding-bottom: 0; }

/* ── Dot ───────────────────────────────────────────────────────────
   Neutral by default (readable without JS); .is-lit turns it gold.
   Radius rides --r-control: circles in the soft theme, hard squares
   in sharp — the dot follows the site's radius voice. */
.timeline__dot {
  position: absolute;
  left: 2px;
  top: 5px;
  width: 20px;
  height: 20px;
  border-radius: var(--r-control);
  background: var(--ink-950);
  border: 2px solid var(--on-dark-ring);
  display: grid;
  place-items: center;
  z-index: var(--z-raise); /* above the ::after fill line */
}
.timeline__dot::after {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: var(--r-control);
  background: var(--on-dark-ring);
}
.timeline__item.is-lit .timeline__dot { border-color: var(--gold); }
.timeline__item.is-lit .timeline__dot::after { background: var(--gold); }

/* ── Tag pill (optional per item) ──────────────────────────────────
   Translucent gold chip. Solid-gold-border fallback first; engines
   with color-mix() soften it to G's measured 16%/36% gold washes —
   both derive from the --gold token, no raw color values. */
.timeline__tag {
  display: inline-block;
  /* 0.6562rem — G's measured tag size, ported verbatim (same
     precedent as buttons.css .btn--sm) */
  font: 650 0.6562rem/1 var(--ff-b);
  letter-spacing: .15em;
  text-transform: uppercase;
  color: var(--gold-hi);
  background: transparent;
  background: color-mix(in srgb, var(--gold) 16%, transparent);
  border: 1px solid var(--gold);
  border-color: color-mix(in srgb, var(--gold) 36%, transparent);
  padding: 6px 11px; /* --sp-1 + 2 / --sp-3 − 1 optical (G verbatim) */
  border-radius: var(--r-control);
  margin-bottom: 11px; /* --sp-3 − 1px optical (G verbatim) */
}

.timeline__h {
  font-family: var(--ff-d);
  font-weight: 600;
  /* G's measured heading clamp, ported verbatim per the timeline
     port-as-is decision (component-local fluid size) */
  font-size: clamp(1.22rem, 2.5vw, 1.62rem);
  line-height: 1.15;
  margin: 0 0 var(--sp-2);
  color: var(--on-dark-heading);
  font-variation-settings: 'SOFT' 20;
}

.timeline__p {
  margin: 0;
  color: var(--on-dark-secondary);
  max-width: 52ch;
}

/* ── Aside photos ──────────────────────────────────────────────── */
.timeline__shot { margin: 0; position: relative; }
.timeline__shot > picture,
.timeline__shot > svg {
  display: block;
  border-radius: var(--r-surface);
  overflow: hidden;
  /* neutral shadow constant, same precedent as buttons.css */
  box-shadow: 0 26px 54px -28px rgba(0, 0, 0, .75);
}
.timeline__shot img { width: 100%; }
.timeline__shot figcaption {
  padding: var(--sp-3) 2px 0; /* 2px optical side-align (G verbatim) */
  font: 500 0.7812rem/1.5 var(--ff-b);
  color: var(--on-dark-caption);
}
.timeline__shot--sm {
  width: 78%;
  margin-left: auto;
  transform: rotate(1.6deg); /* hand-placed print feel (static, not motion) */
}

/* ── Motion-safe enhancements only ─────────────────────────────── */
@media (prefers-reduced-motion: no-preference) {
  .timeline__list::after { transition: height .25s linear; }
  .timeline__dot {
    transition: border-color .5s, background .5s, transform .5s var(--ease);
  }
  .timeline__dot::after { transition: background .5s; }
  .timeline__item.is-lit .timeline__dot { transform: scale(1.12); }
  .timeline__shot img { transition: transform .9s var(--ease); }
  .timeline__shot:hover img { transform: scale(1.035); }
}

/* ── Responsive (system breakpoints only) ──────────────────────── */
@media (max-width: 1080px) {
  .timeline { grid-template-columns: 1fr; gap: var(--sp-7); }
  .timeline__aside {
    position: static;
    grid-template-columns: 1fr 1fr;
    align-items: start;
  }
  .timeline__shot--sm { width: 100%; margin-left: 0; }
}

@media (max-width: 860px) {
  .timeline__aside { grid-template-columns: 1fr; }
  .timeline__shot--sm { transform: none; }
}

/* ── Print ─────────────────────────────────────────────────────────
   base.css flattens the dark section ground to white; this
   component's explicit on-dark text must flatten with it (G's
   timeline paragraphs printed white-alpha on white paper). Dots and
   fill are decoration — hidden; the hairline track prints as-is. */
@media print {
  .timeline__h, .timeline__p, .timeline__tag,
  .timeline__shot figcaption { color: var(--black); }
  .timeline__tag { border-color: var(--black); background: none; }
  .timeline__dot, .timeline__list::after { display: none; }
}


/* ═══ service/service.css ═══ */
/* ═══════════════════════════════════════════════════════════════
   D4D Site Platform — service (boards & commissions)
   Spec: COMPONENTS.md "Service cards / at-a-glance":
     · Card treatment — G's hover-flip cards REBUILT in a single
       stacking context. G stacked per-element z-index shims on
       every child (h3, p, .card__ix all carried z-index) — fragile
       when children are added. Here the card isolates itself
       (isolation:isolate) and the flip layer rides z-index:-1, so
       children need no z-index at all, ever.
     · Glance treatment — S's "at a glance" label/value spec table,
       re-skinned into house tokens, for credentials-forward
       archetypes (Coroner, Judicial — SCHEMA archetype matrix).
   Values measured from reference/gertsen main.css (.cards/.card)
   and reference/sands meet-hugh.php (aside), tokenized.
   Requires tokens.css + base.css. Both treatments compose on light
   grounds.
   ═══════════════════════════════════════════════════════════════ */

/* ── Treatment A: cards ────────────────────────────────────────── */
.svc {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(255px, 1fr));
  gap: 22px; /* --sp-5 − 2px optical (G verbatim) */
  position: relative;
  z-index: var(--z-raise); /* above an opted-in section texture */
}

.svc__card {
  position: relative;
  /* THE fix: one stacking context per card. The flip layer below
     rides z-index:-1 inside it, so every child sits above the flip
     with no per-element shims. */
  isolation: isolate;
  background: var(--white);
  border-radius: var(--r-surface);
  /* 34/26/26 = --sp-6+2 / --sp-5+2 / --sp-5+2 optical (G's 34/26/28,
     bottom re-gridded from 28) */
  padding: 34px 26px 26px;
  /* neutral shadow constants, same precedent as buttons.css */
  box-shadow: 0 1px 0 rgba(0, 0, 0, .05), 0 18px 40px -30px rgba(0, 0, 0, .5);
  border-top: 4px solid var(--accent);
  overflow: hidden;
}

/* Flip layer — grows from the bottom on hover. Solid accent
   fallback; color-mix deepens the gradient tail where supported
   (both ends derive from --accent, no raw values). */
.svc__card::before {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: 0;
  z-index: -1; /* below all card content, above the card background */
  background: var(--accent);
  background: linear-gradient(var(--accent),
    color-mix(in srgb, var(--accent) 72%, var(--black)));
}
.svc__card:hover::before { height: 100%; }

/* White-out on hover. The flip is decoration: the resting card is
   the complete design, and under reduced motion the swap is instant
   (base.css floors transition durations). Contrast on the flipped
   ground = white on --accent, the same pairing buttons.css records
   at 7.06:1 for the default accent; the pipeline re-verifies
   candidate-supplied accents exactly as it does for .btn. */
.svc__card:hover .svc__ix,
.svc__card:hover .svc__org,
.svc__card:hover .svc__note { color: var(--white); }
/* role flips to --white, NOT --gold-hi: gold-hi on the default accent
   #9C2088 is 4.21:1 (< 4.5, computed 2026-08-15) and carries no
   guarantee on an arbitrary candidate accent; --white on #9C2088 =
   7.06:1 ✓ — the same rule event-card applies to its chip text. */
.svc__card:hover .svc__role { color: var(--white); }

.svc__ix {
  /* 0.6875rem — G's measured index size, ported verbatim (same
     precedent as buttons.css .btn--sm) */
  font: 700 0.6875rem/1 var(--ff-b);
  letter-spacing: .2em;
  color: var(--accent-text);
}

.svc__org {
  font-family: var(--ff-d);
  font-weight: 600;
  font-size: 1.16rem; /* G's measured card heading, ported verbatim */
  line-height: 1.22;
  margin: 13px 0 8px; /* --sp-3 + 1px / --sp-2 optical (G verbatim) */
  color: var(--ink-800);
  font-variation-settings: 'SOFT' 18;
}

.svc__role {
  font: 650 0.75rem/1.4 var(--ff-b);
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--accent-text);
  margin: 0 0 10px; /* --sp-2 + 2px optical (G verbatim) */
}

.svc__note {
  margin: 0;
  font-size: .95em;
  color: var(--ink-500);
}

/* Motion-safe: lift, flip glide, color glide */
@media (prefers-reduced-motion: no-preference) {
  .svc__card {
    transition: transform .5s var(--ease), box-shadow .5s var(--ease);
  }
  .svc__card::before { transition: height .55s var(--ease); }
  .svc__card:hover {
    transform: translateY(-8px);
    box-shadow: 0 1px 0 rgba(0, 0, 0, .05), 0 34px 54px -32px rgba(0, 0, 0, .62);
  }
  .svc__ix, .svc__org, .svc__role, .svc__note { transition: color .4s; }
}

/* ── Treatment B: at-a-glance spec list ────────────────────────────
   S's aside re-skinned: heavy accent rules top and bottom, hairline
   rows, uppercase keys, right-aligned values. Type-only — it reads
   like the spec panel on a well-printed flyer. */
.glance {
  border-top: 5px solid var(--accent);
  display: grid;
  align-content: start;
}

.glance__title {
  /* kicker voice, without the section-kicker's leading dash */
  font: 650 var(--fs-kicker)/1 var(--ff-b);
  letter-spacing: var(--ls-kicker);
  text-transform: uppercase;
  color: var(--accent-text);
  margin: 0;
  padding: 18px 0 10px; /* --sp-4+2 / --sp-2+2 optical (S verbatim) */
}

.glance__list { margin: 0; }

.glance__row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--sp-4);
  padding: 13px 0; /* --sp-3 + 1px optical (S verbatim) */
  border-top: 1px solid var(--paper-400);
}
.glance__row:last-child { border-bottom: 5px solid var(--accent); }

.glance__k {
  font: 600 0.875rem/1.35 var(--ff-b);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-500);
}

.glance__v {
  margin: 0;
  font-weight: 600;
  font-size: 1rem;
  text-align: right;
  color: var(--ink-800);
}


/* ═══ personal/personal.css ═══ */
/* ═══════════════════════════════════════════════════════════════
   D4D Site Platform — personal ("off the clock")
   G's .offclock/.facts key-value module, sole reference, ported
   as-is per COMPONENTS.md ("180px/1fr baseline grid, uppercase keys
   in light accent, single column ≤860. No observed quirks. Port
   as-is."). This is where the white-oak rocking chair lives —
   SCHEMA.md calls it high value for specificity.

   Dark-ground component: mount on .sec--dark (or a themed dark
   band). All text via the on-dark ramp; keys in --gold-hi (G
   verbatim — on dark grounds the text accent is the gold family,
   tokens.css note).

   Port-as-is values kept verbatim where G hand-set them off-scale,
   each flagged; hairlines consolidated to --on-dark-hairline the
   same way the alpha ramp consolidated G's ad-hoc whites.

   Requires tokens.css + base.css. */

.personal__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, .85fr);
  gap: clamp(34px, 6vw, 72px);
  align-items: center;
}

/* Kicker — DESIGN.md spec from tokens, light (on-dark) voice */
.personal__kicker {
  font: 600 var(--fs-kicker)/1 var(--ff-b);
  letter-spacing: var(--ls-kicker);
  text-transform: uppercase;
  color: var(--gold);
  margin: 0;
  display: flex;
  align-items: center;
  gap: var(--kicker-gap);
}
.personal__kicker::before {
  content: "";
  width: var(--kicker-dash-w);
  height: var(--kicker-dash-h);
  background: currentColor;
  flex: none;
}

/* Headline — G's .h2--light on the adopted clamp */
.personal__headline {
  font-family: var(--ff-d);
  font-weight: 600;
  font-size: var(--fs-h2);
  line-height: var(--lh-h2);
  letter-spacing: -.022em;
  margin: .18em 0 .55em;
  color: var(--on-dark-heading);
  font-variation-settings: 'SOFT' 18, 'WONK' 1;
  text-wrap: balance;
}
.personal__headline em {
  font-style: italic;
  color: var(--gold-hi);
  font-variation-settings: 'SOFT' 40, 'WONK' 1;
}

/* ── The key/value rows ──────────────────────────────────────────
   G verbatim: 28px top margin, 2px row gap, 180px key column,
   22px column gap, 19px row padding (28/19 are G's hand-set
   opticals, kept under the port-as-is instruction and flagged —
   they sit 3–4px off the scale). */
.facts {
  list-style: none;
  margin: 28px 0 0; /* port-as-is: G optical, --sp-6−4 */
  padding: 0;
  display: grid;
  gap: 2px;
}
.facts li {
  display: grid;
  grid-template-columns: 180px 1fr;
  gap: 22px; /* --sp-5−2, within ±2 */
  align-items: baseline;
  padding: 19px 0; /* port-as-is: G optical, --sp-4+3 */
  /* G measured rgba(255,255,255,.19); consolidated to the named
     hairline token per the DESIGN.md on-dark cleanup */
  border-top: 1px solid var(--on-dark-hairline);
}
.facts li:last-child {
  border-bottom: 1px solid var(--on-dark-hairline);
}

.facts__k {
  /* 0.75rem: G verbatim (12px — G set this 0.5px under his own
     kicker size; port-as-is) */
  font: 650 0.75rem/1.3 var(--ff-b);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--gold-hi);
}

.facts__v {
  color: var(--on-dark-body);
  font-size: 1.02em; /* port-as-is: G's hair-larger value voice */
}
.facts__v b {
  color: var(--white);
  font-weight: 650;
}

/* ── Optional photo aside ──────────────────────────────────────── */
.personal__frame {
  margin: 0;
  position: relative;
  border-radius: var(--r-surface);
  overflow: hidden;
  background: var(--paper-300);
  /* shadow black is rgba per foundation buttons.css precedent */
  box-shadow: 0 26px 54px -24px rgba(0, 0, 0, .6);
}
.personal__frame img,
.personal__frame svg {
  width: 100%;
  height: auto;
  display: block;
}
.personal__frame::after {
  content: "";
  position: absolute;
  inset: 0;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .08);
  border-radius: var(--r-surface);
  pointer-events: none;
}
.personal__frame--tilt-r { transform: rotate(2.1deg); }

/* ── Responsive (G verbatim breaks) ────────────────────────────── */
@media (max-width: 1080px) {
  .personal__grid { grid-template-columns: 1fr; }
  .personal__media { max-width: 440px; margin-inline: auto; }
}

@media (max-width: 860px) {
  /* single column: key stacks over its value */
  .facts li {
    grid-template-columns: 1fr;
    gap: 6px; /* G verbatim; --sp-1+2, within ±2 */
  }
}


/* ═══ gallery/gallery.css ═══ */
/* ═══════════════════════════════════════════════════════════════
   D4D Site Platform — gallery + lightbox
   Source: Gertsen's CSS-columns masonry and lightbox, measured and
   ported (main.css "gallery"/"lightbox" blocks), tokenized. R's
   mobile natural-aspect rule ported for the crop variant.

   Fixes shipped (COMPONENTS.md "Gallery + lightbox"):
   · G's dead .gal--tall class — not carried forward (masonry needs
     no per-figure height class; natural aspect does the work).
   · G's gallery skipping <picture>/webp — canonical markup is
     <picture> with a webp source (see gallery.html + README).
   · G's lightbox missing the focus trap its donate modal had —
     gallery.js traps Tab inside the dialog.
   · G's vestigial `filter .5s` img transition (declared, never
     triggered) — dropped.
   · Hover wash literals rgba(123,21,104,…)/rgba(75,46,158,…) —
     rebuilt from --accent/--ink-900 via color-mix.

   A11y / AA notes:
   · Lightbox buttons: --white on --on-dark-ring-fill over the
     ink-950 scrim ≥ 18:1 (same computation foundation records for
     ghost buttons).
   · Figures become buttons via JS only; keyboard focus gets the
     global gold :focus-visible ring (base.css).
   · [hidden] on .lb wins over display:grid via base.css's
     `[hidden]{display:none!important}` — base's comment anticipates
     exactly this block.

   Requires tokens.css + base.css. Pairs with gallery.js.
   ═══════════════════════════════════════════════════════════════ */

/* ── Masonry (G verbatim: columns 4/3/2, breakout past the wrap) ── */
.gallery {
  columns: 4;
  /* 14px gap/pad/rhythm: G's measured optical value (--sp-4 − 2px),
     kept per the ±1–2px optical-deviation rule */
  column-gap: 14px;
  max-width: 1500px;
  margin: 0 auto;
  padding: 0 14px;
}

.gal {
  margin: 0 0 14px;               /* same 14px optical value as the gap */
  break-inside: avoid;
  position: relative;
  overflow: hidden;
  border-radius: var(--r-surface);
  background: var(--paper-300);
  cursor: zoom-in;
}

.gal picture { display: block; }  /* kill inline-gap under the img */
.gal img { width: 100%; }         /* height:auto comes from base.css */

/* Grid captions are for the lightbox and screen readers — visually
   hidden in the masonry (G's clip pattern), surfaced by gallery.js
   in the lightbox caption line. */
.gal__cap {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ── Crop variant ──────────────────────────────────────────────────
   Default figures are NEVER cropped — masonry renders natural aspect,
   so the faces-never-cropped house rule holds by construction.
   .gal--crop ratio-locks a photo that carries dead space; the focal
   point decides what stays in frame. The generator emits BOTH
   data-focal="x y" (machine-readable contract) and style="--focal:x y"
   (what this rule consumes — works before/without JS).
   Default focal 50% 30% biases toward where faces sit in candid shots.
   Released entirely on phones (620 block below) — R's rule. */
.gal--crop img {
  aspect-ratio: 4 / 3;
  object-fit: cover;
  object-position: var(--focal, 50% 30%);
}

/* ── Lightbox ─────────────────────────────────────────────────────
   G's structure; opened/closed by gallery.js via [hidden]. */
.lb {
  position: fixed;
  inset: 0;
  z-index: var(--z-lightbox);
  /* G measured rgba(11,2,10,.96) — rebuilt as ink-950 at 96%.
     Solid-ink first line is the fallback for browsers without
     color-mix (older rural-phone browsers are the target audience,
     not the edge case). */
  background: var(--ink-950);
  background: color-mix(in srgb, var(--ink-950) 96%, transparent);
  display: grid;
  place-items: center;
  padding: 34px;                  /* G measured (--sp-6 + 2px) */
}

.lb__fig {
  margin: 0;
  display: grid;
  gap: var(--sp-3);
  justify-items: center;
}

.lb__img {
  max-width: 92vw;                /* G verbatim */
  max-height: 82vh;               /* G's 88vh minus the caption line */
  width: auto;
  border-radius: var(--r-surface);
  box-shadow: 0 30px 90px rgba(0, 0, 0, .7);  /* shadow only — rgba per foundation precedent */
}

.lb__cap {
  margin: 0;
  max-width: 60ch;
  text-align: center;
  color: var(--on-dark-secondary);
  font-size: 0.9375rem;
}

.lb__count {
  margin: 0;
  color: var(--on-dark-caption);
  font: 600 0.8438rem/1 var(--ff-b);
  letter-spacing: .08em;
  text-transform: uppercase;
}

/* Round controls: --r-control = circle in the soft theme, square in
   sharp — the themed radius doing its job, never a local 50%. */
.lb__x {
  position: absolute;
  top: 16px;
  right: 20px;                    /* G measured (--sp-5 − 4 → kept verbatim, see note) */
  width: 48px;
  height: 48px;
  border: 0;
  border-radius: var(--r-control);
  background: var(--on-dark-ring-fill);
  color: var(--white);
  font-size: 1.875rem;
  line-height: 1;
  cursor: pointer;
}

.lb__nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 54px;
  height: 54px;
  border: 0;
  border-radius: var(--r-control);
  background: var(--on-dark-ring-fill);
  color: var(--white);
  font-size: 2rem;
  line-height: 1;
  cursor: pointer;
}

.lb__x:hover, .lb__x:focus-visible,
.lb__nav:hover, .lb__nav:focus-visible {
  /* no-color-mix fallback: hover simply stays at the .12 fill */
  background: var(--on-dark-ring-fill);
  background: color-mix(in srgb, var(--white) 24%, transparent); /* G's .24 hover step */
}

.lb__prev { left: 18px; }         /* G measured (--sp-4 + 2px) */
.lb__next { right: 18px; }

/* ── Motion-safe enhancements only ────────────────────────────────
   Hover zoom + accent wash + fades are decoration; under reduced
   motion the gallery is a plain, fully readable grid. */
@media (prefers-reduced-motion: no-preference) {
  .gal img { transition: transform .95s var(--ease); }
  .gal:hover img,
  .gal:focus-visible img { transform: scale(1.065); }

  .gal::after {                   /* accent hover wash (G's device, tokenized) */
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(200deg,
      color-mix(in srgb, var(--accent) 44%, transparent),
      color-mix(in srgb, var(--ink-900) 34%, transparent));
    opacity: 0;
    transition: opacity .45s var(--ease);
    pointer-events: none;
  }
  .gal:hover::after,
  .gal:focus-visible::after { opacity: 1; }

  .lb { animation: lbIn .3s var(--ease); }
  @keyframes lbIn { from { opacity: 0; } to { opacity: 1; } }

  .lb__x, .lb__nav { transition: background .3s; }
}

/* ── Breakpoints (system set only) ──────────────────────────────── */
@media (max-width: 1080px) {
  .gallery { columns: 3; }
}

@media (max-width: 860px) {
  .gallery { columns: 2; }
}

@media (max-width: 620px) {
  /* 9px: G's measured phone gap (--sp-2 + 1px) */
  .gallery { column-gap: 9px; padding-inline: 9px; }
  .gal { margin-bottom: 9px; }
  /* R's fix, ported with its comment:
     natural aspect on mobile — no face crops */
  .gal--crop img {
    aspect-ratio: auto;
    object-fit: contain;
    object-position: center;
  }
  .lb { padding: 16px; }
  .lb__nav { width: 44px; height: 44px; font-size: 1.625rem; }
  .lb__prev { left: 6px; }
  .lb__next { right: 6px; }
}

/* Ultrawide: widen the art, per the DESIGN.md ultrawide policy
   (G verbatim tiers) */
@media (min-width: 1700px) {
  .gallery { max-width: 1780px; columns: 5; }
}

@media (min-width: 2300px) {
  .gallery { max-width: 2060px; columns: 6; }
}


/* ═══ donate/donate.css ═══ */
/* ═══════════════════════════════════════════════════════════════
   D4D Site Platform — donate module
   Two variants, one data contract, NO POPUP (Decision 27, 2026-08-14
   — the exit-intent modal is killed; these two carry conversion):

     A · .donate — G's panel + amount grid, composed inside a dark
         interstitial (.band .band--dark.band-donate). The adopted
         platform spec.
     B · .donate-band — R's full-bleed band, the interstitial
         variant: gold ground (the one cross-candidate constant with
         recorded ratios), preset pills that invert to accent on
         hover (R's move), single big CTA.

   No JS file: donate clicks are attributed by analytics.js, which
   parses the refcode straight off each ActBlue URL — the URL is the
   single source of truth (refcode taxonomy web-{placement}-{preset};
   placements here: `panel`, `band`).

   Requires tokens.css + base.css + buttons.css. All colors are
   tokens or color-mix() over tokens — no raw hex in this file.

   Contrast (against tokens.css defaults, WCAG 2.x):
     panel: white/on-dark ramp over --ink-950-family grounds ─ AA ✓
     .amt hover: --gold-ink on --gold .................. 7.28:1 ✓
     band: --gold-ink on --gold ........................ 7.28:1 ✓
     band fine print: mix(gold-ink 82%, gold) on --gold  5.17:1 ✓
     band pills: --accent-text on --paper-100 — the exact pair the
       pipeline guarantees ≥4.5:1 for every accent (tokens.css);
       hover: --white on --accent — bank-guaranteed; for a
       candidate-supplied accent the pipeline re-verifies (same
       rule as buttons.css).
   ═══════════════════════════════════════════════════════════════ */

/* ── Variant A · the panel (G verbatim, tokenized) ─────────────── */
.donate {
  max-width: 1080px;
  margin: 0 auto;
  background: linear-gradient(150deg,
    color-mix(in srgb, var(--gold) 16%, transparent),
    color-mix(in srgb, var(--white) 5%, transparent));
  border: 1px solid color-mix(in srgb, var(--gold) 42%, transparent);
  border-radius: var(--r-surface); /* G measured 20px → theme token */
  padding: clamp(26px, 3.4vw, 40px); /* G verbatim */
  backdrop-filter: blur(9px);
  -webkit-backdrop-filter: blur(9px);
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, .92fr);
  gap: clamp(22px, 3vw, 44px); /* G verbatim */
  align-items: center;
  box-shadow:
    0 0 0 1px color-mix(in srgb, var(--gold) 10%, transparent),
    0 30px 60px -40px rgba(0, 0, 0, 1);
}

/* Panel kicker — the kicker spec via its tokens, gold on dark */
.donate__k {
  font: 650 var(--fs-kicker)/1 var(--ff-b);
  letter-spacing: var(--ls-kicker);
  text-transform: uppercase;
  color: var(--gold);
  margin: 0 0 var(--sp-3);
  display: flex;
  align-items: center;
  gap: var(--kicker-gap);
}
.donate__k::before {
  content: "";
  width: var(--kicker-dash-w);
  height: var(--kicker-dash-h);
  background: currentColor;
  flex: none;
}

.donate__title {
  font-family: var(--ff-d);
  font-weight: 600;
  /* G measured clamp(1.5rem,3vw,2.1rem) → ladder endpoints */
  font-size: clamp(var(--fs-1), 3vw, var(--fs-2));
  line-height: 1.08;
  margin: 0 0 var(--sp-3);
  color: var(--on-dark-heading);
  font-variation-settings: 'SOFT' 20, 'WONK' 1;
}
.donate__title em { font-style: italic; color: var(--gold-hi); }
.donate__sub {
  margin: 0;
  color: var(--on-dark-secondary);
  font-size: .97em;
  max-width: 44ch; /* DESIGN.md: donate copy measure */
}

/* ── The amount grid ───────────────────────────────────────────── */
.donate__amounts {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 11px; /* G optical verbatim — --sp-3 −1 */
}
.amt {
  display: grid;
  place-items: center;
  padding: 1em .5em;
  min-height: 3.4rem; /* comfortable touch target, scales with reader */
  border-radius: var(--r-surface); /* G measured 12px → theme token */
  background: color-mix(in srgb, var(--black) 24%, transparent);
  border: 1.5px solid color-mix(in srgb, var(--white) 22%, transparent);
  color: var(--white);
  text-decoration: none;
  font-family: var(--ff-d);
  font-weight: 650;
  font-size: 1.28rem; /* G verbatim (control size, like .btn's 0.9375rem) */
  letter-spacing: -.01em;
  font-variant-numeric: tabular-nums;
}
/* Hover recolor stays OUTSIDE the motion gate: it is state feedback
   (both pairs AA), and reduced-motion users still deserve it. The
   lift + shadow are motion and live below. */
.amt:hover,
.amt:focus-visible {
  background: var(--gold);
  border-color: var(--gold);
  color: var(--gold-ink);
}
.amt--other {
  font-family: var(--ff-b);
  font-size: .95rem; /* G verbatim */
  letter-spacing: .06em;
  text-transform: uppercase;
}

/* ── Check-donations row (check_donations + mailing_address) ───── */
.donate__check {
  grid-column: 1 / -1;
  margin: var(--sp-1) 0 0;
  padding-top: var(--sp-5); /* G measured 20px, snapped to scale */
  border-top: 1px solid var(--on-dark-hairline);
  display: flex;
  align-items: center;
  gap: 10px; /* G optical — --sp-2 +2 */
  flex-wrap: wrap;
  font: 500 0.8438rem/1.5 var(--ff-b);
  color: var(--on-dark-secondary);
}
.donate__check b {
  color: var(--on-dark-heading);
  font-weight: 600;
}
.donate__check svg { flex: none; }

/* The chip-in interstitial: .band supplies the rhythm (tighter than
   a section — a real editorial distinction, G's comment kept); the
   panel itself centers via its own margin above. */

/* ── Variant B · full-bleed band (R, tokenized) ────────────────── */
.donate-band {
  background: var(--gold);
  color: var(--gold-ink);
  text-align: center;
}
.donate-band__kicker {
  font: 650 var(--fs-kicker)/1 var(--ff-b);
  letter-spacing: var(--ls-kicker);
  text-transform: uppercase;
  color: var(--gold-ink);
  margin: 0 0 var(--sp-3);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--kicker-gap);
}
.donate-band__kicker::before {
  content: "";
  width: var(--kicker-dash-w);
  height: var(--kicker-dash-h);
  background: currentColor;
  flex: none;
}
.donate-band__title {
  font-family: var(--ff-d);
  font-weight: 650;
  /* R measured clamp(2rem,3.6vw,2.9rem) → ladder endpoints */
  font-size: clamp(var(--fs-2), 3.6vw, var(--fs-3));
  line-height: var(--lh-h3);
  margin: 0 0 var(--sp-3);
  color: var(--gold-ink);
  font-variation-settings: 'SOFT' 20, 'WONK' 1;
}
.donate-band__lede {
  max-width: 52ch;
  margin: 0 auto var(--sp-5);
  color: color-mix(in srgb, var(--gold-ink) 82%, var(--gold));
}

.amt-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3); /* R measured .7rem ≈ 11px → --sp-3 */
  justify-content: center;
  margin-bottom: var(--sp-5);
}
/* Band pills — R's move: quiet paper pill, inverts to accent.
   Paper-100 face because --accent-text on --paper-100 is the exact
   pair the pipeline guarantees AA for every candidate accent. */
.amt--pill {
  padding: .8rem 1.5rem; /* R verbatim */
  min-height: 0;
  border-radius: var(--r-control); /* pill (soft) / hard (sharp) */
  background: var(--paper-100);
  border: 1.5px solid color-mix(in srgb, var(--gold-ink) 25%, transparent);
  color: var(--accent-text);
  font-family: var(--ff-b);
  font-weight: 700;
  font-size: 1.15rem; /* R verbatim */
}
.amt--pill:hover,
.amt--pill:focus-visible {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--white);
}

/* Band CTA size-up (R oversized its Donate Now inline; a class here
   instead — co-applied with .btn .btn--accent from buttons.css) */
.donate-band__cta {
  font-size: 1.15rem;
  /* R measured ≈17×35px; snapped to --sp-4 +1 / --sp-6 +2 (sanctioned) */
  padding: 17px 34px;
}
.donate-band__fine {
  margin: var(--sp-5) 0 0;
  font-size: 0.8125rem;
  /* ≥82% gold-ink: mix(gold-ink 82%, gold) on --gold = 5.17:1 ✓
     (≥5:1 target; the previous 75% mix was 4.44:1 — under the 4.5:1
     AA floor for this 13px line. Computed 2026-08-15. Matches the
     lede/check-row mix.) No-color-mix engines drop the declaration
     and inherit .donate-band's --gold-ink: 7.28:1 ✓. */
  color: color-mix(in srgb, var(--gold-ink) 82%, var(--gold));
}

/* Check row re-tinted for the gold ground */
.donate-band .donate__check {
  justify-content: center;
  border-top-color: color-mix(in srgb, var(--gold-ink) 25%, transparent);
  color: color-mix(in srgb, var(--gold-ink) 82%, var(--gold));
  margin-top: var(--sp-5);
}
.donate-band .donate__check b { color: var(--gold-ink); }

/* ── Motion-safe enhancements ──────────────────────────────────── */
@media (prefers-reduced-motion: no-preference) {
  .amt {
    transition:
      background .3s var(--ease),
      border-color .3s var(--ease),
      color .3s,
      transform .3s var(--ease),
      box-shadow .3s var(--ease);
  }
  .amt:hover {
    transform: translateY(-3px);
    box-shadow: 0 12px 26px -12px rgba(0, 0, 0, .6);
  }
}

/* ── Responsive (system breakpoints only) ──────────────────────── */
@media (max-width: 1080px) {
  .donate {
    grid-template-columns: 1fr;
    max-width: 640px;
    text-align: left;
  }
}
@media (max-width: 860px) {
  .donate__amounts { grid-template-columns: repeat(2, 1fr); }
}


/* ═══ forms/form.css ═══ */
/* ═══════════════════════════════════════════════════════════════
   D4D Site Platform — forms chassis
   Shared by volunteer / contact / yard-sign (one chassis, mounted
   wherever a form module is on — the fix for S's two-page yard-sign
   drift). Pairs with form.js. Requires tokens.css + base.css +
   buttons.css. All colors are tokens or color-mix() over tokens —
   no raw hex anywhere in this file.

   GROUND: the chassis is G's glass card and is designed for DARK
   sections (.sec--dark / --ink-950 family). Compose forms on dark
   ground; the on-dark alpha ramp does the text work. (G, the
   adopted spec, only ever mounts forms on dark.)

   TEXTURES: never on form surfaces (Session-0 rule). The section
   may carry one texture; the formcard itself stays clean — and as
   of decision 25 the card is OPAQUE, so a section texture cannot
   physically cross an input even if one is present (enforce.js
   rejects such plans; this is the CSS-side guarantee).

   THE SPECIFICITY FIX (COMPONENTS.md, volunteer form):
   G needed two documented !important fights because bare
   `.field label` (0,1,1) outranked `.chk` (0,1,0) and `.field input`
   styled the checkbox boxes. This chassis removes both structurally:
     · field labels/legends carry an explicit .field__label class —
       a checkbox's .chk label never matches the field-label selector;
     · checkboxes live inside fieldset.checks, which is NOT a .field,
       so `.field input` can never reach them.
   Zero !important in this file, by construction.

   ERROR/SUCCESS COLOR NOTE (flag for design review): tokens.css
   ships no semantic error/success tokens, and the no-raw-hex rule
   is absolute, so validation states use the house attention family
   (--gold-hi text + brightened border) plus, always, a text message
   wired to the input (errors are never color alone). If the
   foundation later adds --err/--ok tokens, swap them in here.

   Defects this file fixes (COMPONENTS.md):
     · G's two !important specificity fights (structural fix above)
     · C/S's combined "Email or Phone" free-text field → typed,
       separated email + tel inputs (the Reply-To header defect)
     · C's inert honeypot (handler checked a field no form had) —
       the honeypot is present in every form's markup
     · S's yard-sign form duplication drift — one definition
     · touch targets: checks ≥2.875rem, follow link ≥44px (G's own
       footer fix, applied here too)
   ═══════════════════════════════════════════════════════════════ */

/* ── Layout: the two-card grid (G verbatim, system breakpoints) ──
   420px floor keeps paired name/town + email/phone rows from
   squeezing to ~347px side-by-side on tablet (G's comment, kept);
   min(420px,100%) so the floor can never overflow a narrow phone. */
.forms {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(420px, 100%), 1fr));
  gap: 26px; /* G optical verbatim — --sp-5 +2, sanctioned deviation */
  align-items: start;
  max-width: 1080px;
  margin-inline: auto;
}

/* ── Form card — G's glass card, tokenized, OPAQUE (decision 25) ──
   The card used to be a translucent tint (white 5.5%/10% over
   transparent + backdrop blur), so a section texture ran straight
   through the form surface — and textures must never cross an input
   (Session-0 rule; enforce.js also rejects texture-on-form-section
   plans — this is the rendering-side guarantee to that plan-side
   gate). Fix: bake what the tint effectively showed over the dark
   ground it is designed for. color-mix(in srgb, white N%, ink-950)
   IS the source-over composite of the old N% white layer on an
   opaque --ink-950 section (sRGB gamma-space interpolation = CSS
   alpha compositing), so the resting look is pixel-identical on the
   canonical ground, tracks the build-time ink re-hue via the token,
   and keeps the no-raw-hex rule. The backdrop blur went with the
   transparency: nothing can show through an opaque card, which is
   the point. */
.formcard {
  background: color-mix(in srgb, var(--white) 5.5%, var(--ink-950));
  border: 1px solid var(--on-dark-hairline);
  border-radius: var(--r-surface); /* G measured 20px → nearest theme token */
  padding: clamp(26px, 3.4vw, 38px); /* G verbatim */
}
.formcard--primary {
  background: color-mix(in srgb, var(--white) 10%, var(--ink-950));
  border-color: color-mix(in srgb, var(--gold) 42%, transparent);
  box-shadow:
    0 0 0 1px color-mix(in srgb, var(--gold) 12%, transparent),
    0 30px 60px -40px rgba(0, 0, 0, 1);
}

.formcard__head { margin-bottom: var(--sp-5); }
/* Type-only head — G's decorative icon chip is not carried forward
   (icon sets standing in for content, DESIGN.md doctrine). */
.formcard__head h3 {
  font-family: var(--ff-d);
  font-weight: 600;
  font-size: var(--fs-1); /* G measured 1.55rem → nearest ladder step */
  font-variation-settings: 'SOFT' 20;
  line-height: 1.12;
  margin: 0 0 7px; /* G optical verbatim — --sp-2 −1 */
  color: var(--on-dark-heading);
}
.formcard__head p {
  margin: 0;
  color: var(--on-dark-secondary);
  font-size: .96em;
}

/* ── Form grid ─────────────────────────────────────────────────── */
.form { display: grid; gap: 15px; } /* G optical verbatim — --sp-4 −1 */
.f-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 15px; /* matches .form gap */
}
.field { display: grid; gap: 7px; min-width: 0; } /* G optical — --sp-2 −1 */

/* Explicit class — the structural end of G's `.field label` fights */
.field__label {
  font: 650 0.7188rem/1 var(--ff-b); /* G verbatim */
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--on-dark-secondary);
  padding: 0;
}
.field__req { color: var(--gold); }

/* Resting border: white 36% clears the 3:1 non-text floor against
   every surface it touches (white 20% sat at 1.92:1 — invisible).
   Recomputed 2026-08-15 against the now-OPAQUE card bases (WCAG
   relative luminance, border composited over each base; default
   tokens, with the worst case across the four bank accents' re-hued
   --ink-950 in parentheses — the lightness lock keeps that spread
   ≤0.04):
     vs quiet card base (white 5.5% mix):  3.29:1 (≥3.26)
     vs primary card base (white 10% mix): 3.15:1 (≥3.15)
     vs input fill on quiet card:          3.33:1 (≥3.33)
     vs input fill on primary card:        3.32:1 (≥3.29)
   All ≥3:1 — no adjustment needed. Because the base is opaque these
   ratios now hold no matter what sits behind the card (a texture
   can no longer shift the effective ground). */
.field input,
.field select,
.field textarea {
  width: 100%;
  font: 400 1rem/1.45 var(--ff-b);
  color: var(--white);
  background: color-mix(in srgb, var(--black) 24%, transparent);
  border: 1px solid color-mix(in srgb, var(--white) 36%, transparent);
  border-radius: var(--r-surface); /* G measured 10px → theme token (sharp = 0) */
  padding: 13px 15px; /* G optical verbatim — --sp-3 +1 / --sp-4 −1 */
}
/* Placeholder: white 48% clears the 4.5:1 text floor vs the input
   fill (white 34% sat at 3.12:1). Recomputed 2026-08-15 against the
   opaque card bases: 4.94:1 on the quiet card's fill · 4.77:1 on
   the primary card's fill (worst case across the re-hued bank
   inks: 4.90 · 4.73 — both ≥4.5). */
.field input::placeholder,
.field textarea::placeholder {
  color: color-mix(in srgb, var(--white) 48%, transparent);
}
/* Focus: border + ground deepen. The global gold :focus-visible
   outline from base.css is NOT suppressed — it is the house focus
   indicator; this is the complementary surface response. */
.field input:focus,
.field select:focus,
.field textarea:focus {
  border-color: var(--gold);
  background: color-mix(in srgb, var(--black) 34%, transparent);
}
.field textarea { resize: vertical; min-height: 88px; }

/* ── Checkboxes — G's drawn checks, no specificity fights ──────── */
.checks { border: 0; margin: 0; padding: 6px 0 0; min-width: 0; } /* 6px: G optical — --sp-2 −2 */
.checks .field__label { display: block; } /* legend, same label voice */
.checks__grid {
  display: grid;
  /* em-based track minimum, NOT px: a 160px min doesn't grow with the
     reader's font setting, and at root fs=24 the multi-column layout
     collapsed labels onto neighboring pills (responsive gate,
     2026-08-15). 10em scales with the font so columns drop instead. */
  grid-template-columns: repeat(auto-fit, minmax(min(10em, 100%), 1fr));
  gap: var(--sp-2);
  margin-top: 10px; /* G optical — --sp-3 −2 */
}
.chk {
  /* anywhere (not break-word): break-word never lowers min-content, so a
     long label as a bare text node still forced pill overlap at large
     reader fonts; anywhere lets the intrinsic minimum shrink. */
  overflow-wrap: anywhere;
}
.chk {
  display: flex;
  align-items: center;
  gap: 10px; /* G optical — --sp-2 +2 */
  cursor: pointer;
  font: 500 0.875rem/1.3 var(--ff-b);
  color: var(--on-dark-body);
  background: color-mix(in srgb, var(--black) 20%, transparent);
  border: 1px solid var(--on-dark-hairline); /* G .16 → ramp token (.14) */
  border-radius: var(--r-surface); /* G measured 10px */
  padding: .7em .8em;
  min-height: 2.875rem; /* ≥44px touch target, scales with the reader */
}
.chk:hover {
  border-color: color-mix(in srgb, var(--gold) 55%, transparent);
}
.chk input {
  appearance: none;
  -webkit-appearance: none;
  width: 1.35em;
  height: 1.35em;
  min-width: 18px;
  min-height: 18px;
  flex: none;
  margin: 0;
  padding: 0;
  /* ≈5px at soft, 0 at sharp — an 18px box can't carry the full
     14px surface radius; derived from the theme token, not local */
  border-radius: calc(var(--r-surface) * 0.36);
  border: 2px solid color-mix(in srgb, var(--white) 42%, transparent);
  background: transparent;
  display: grid;
  place-items: center;
}
.chk input::after {
  content: "";
  width: .5em;
  height: .3em;
  margin-top: -.1em;
  border-left: .12em solid var(--gold-ink);
  border-bottom: .12em solid var(--gold-ink);
  transform: rotate(-45deg) scale(0);
}
.chk input:checked {
  background: var(--gold);
  border-color: var(--gold);
}
.chk input:checked::after { transform: rotate(-45deg) scale(1); }
.chk:has(input:checked) {
  border-color: var(--gold);
  background: color-mix(in srgb, var(--gold) 14%, transparent);
}

/* ── Honeypot — PRESENT in markup, invisible, never focusable ───
   (C's handlers checked a honeypot no form contained — inert spam
   protection. Here the field ships in every form.) */
.hp {
  position: absolute;
  left: -9999px;
  top: auto;
  width: 1px;
  height: 1px;
  overflow: hidden;
  margin: 0;
}

/* ── Validation + status ───────────────────────────────────────── */
/* Pre-rendered live region: every .err slot ships in markup with
   role="status" + id and stays RENDERED (never display:none — a
   display:none live region isn't registered by AT, so the first
   error would go unannounced). form.js only swaps textContent.
   Empty at rest = a zero-height block (costs one 7px field-grid
   gap, the price of a live region that actually announces). */
.err {
  font: 500 0.7812rem/1.3 var(--ff-b);
  color: var(--gold-hi); /* see header note — no semantic token exists */
  display: block;
}
.field.has-err input,
.field.has-err select,
.field.has-err textarea {
  border-color: var(--gold-hi);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--gold-hi) 20%, transparent);
}

.form__note {
  margin: 2px 0 0;
  font-size: 0.7812rem;
  color: var(--on-dark-caption);
  text-align: center;
}
/* The honest "not switched on yet" line. It sits at the TOP of the form,
   before anyone types, and reads as a notice rather than a footnote —
   a person should not fill in six fields to discover the submit is off.
   Rendered only while the shared endpoint is not declared live; when it
   is, this element is absent entirely (lib/generate/render/forms.js). */
.form__note--offline {
  margin: 0 0 4px;
  padding: 10px 12px;
  border: 1px solid var(--on-dark-hairline);
  border-radius: var(--r-surface);
  background: rgba(255, 255, 255, .06);
  color: var(--on-dark-secondary);
  font-size: 0.8125rem;
  line-height: 1.45;
  text-align: left;
}

/* …and the submit must LOOK as inert as it is. A button that reads live
   and does nothing is the same lie in a different place. Both button
   flavors collapse to the SAME muted pill, so neither the gold CTA nor the
   accent submit can be mistaken for a working control — and the label
   stays legible on the dark ground (grayscaling the accent purple just
   made it disappear). */
.form .btn[disabled] {
  --bg: rgba(255, 255, 255, .10);
  --fg: var(--on-dark-secondary);
  cursor: not-allowed;
  box-shadow: inset 0 0 0 1px var(--on-dark-hairline);
}
.form .btn[disabled]:hover { --bg: rgba(255, 255, 255, .10); }
.form__status {
  margin: 4px 0 0;
  font: 600 0.875rem/1.45 var(--ff-b);
  text-align: center;
}
.form__status.is-ok  { color: var(--on-dark-heading); }
.form__status.is-bad { color: var(--gold-hi); }
.form.is-sending button[type="submit"] {
  opacity: .6;
  pointer-events: none;
}

/* ── Social follow link (renders ONLY when `social` is non-empty) ── */
.formcard__follow {
  display: flex;
  align-items: center;
  gap: 11px; /* G optical — --sp-3 −1 */
  margin-top: 22px; /* G optical — --sp-5 −2 */
  padding-top: var(--sp-5); /* G measured 20px, snapped to scale */
  border-top: 1px solid var(--on-dark-hairline);
  color: var(--on-dark-body);
  text-decoration: none;
  font: 600 0.875rem/1 var(--ff-b);
  min-height: 44px; /* touch target — G's own footer fix */
}
.formcard__follow:hover { color: var(--gold-hi); }
.formcard__follow span { flex: 1; }
.formcard__follow svg { flex: none; }

/* ── Yard-sign section layout + pure-CSS sign preview (S's mock,
      re-cut in house tokens; fed by the wordmark slot) ──────────── */
.yardsign {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr));
  gap: clamp(28px, 4vw, 56px); /* S measured verbatim */
  align-items: start;
}
.signprev {
  margin-top: var(--sp-6);
  max-width: 320px;
  background: var(--accent);
  border-radius: var(--r-surface);
  /* S measured 24/22/20px — 22 is --sp-5 −2 (sanctioned), 20 snapped */
  padding: var(--sp-5) 22px var(--sp-4);
  display: grid;
  gap: var(--sp-3);
  justify-items: center;
  text-align: center;
}
.signprev__first {
  font: 600 1rem/1 var(--ff-b);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--on-dark-secondary);
}
.signprev__last {
  font-family: var(--ff-d);
  font-weight: 650;
  font-variation-settings: 'SOFT' 0, 'WONK' 0; /* crisp on the wordmark */
  font-size: var(--fs-3);
  line-height: .85;
  text-transform: uppercase;
  color: var(--white);
  overflow-wrap: normal; /* surnames never break (base.css rule, restated) */
}
.signprev__rule {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  color: var(--gold);
}
.signprev__rule::before,
.signprev__rule::after {
  content: "";
  flex: 1;
  height: 3px;
  background: currentColor;
}
.signprev__office {
  font: 600 1rem/1 var(--ff-b);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--white);
}
.signprev__bar {
  width: 100%;
  background: var(--gold);
  color: var(--gold-ink);
  font: 600 0.9375rem/1 var(--ff-b);
  letter-spacing: .08em;
  text-transform: uppercase;
  padding: 6px 0; /* S verbatim — --sp-2 −2 */
  border-radius: calc(var(--r-surface) * 0.36); /* echoes the check box */
}

/* ── Thank-you page (the form system's conversion beacon) ──────── */
.ty {
  min-height: 100svh;
  display: grid;
  place-items: center;
  text-align: center;
  background: var(--ink-950);
  color: var(--on-dark-body);
  padding: var(--sp-7) var(--pad-inline); /* G measured 40px, snapped */
}
.ty__in { max-width: 620px; }
.ty__check {
  width: 78px;
  height: 78px;
  color: var(--gold);
  margin: 0 auto var(--sp-5);
}
.ty h1 {
  font-family: var(--ff-d);
  font-weight: 600;
  font-variation-settings: 'SOFT' 20, 'WONK' 1;
  /* G measured clamp(2.1rem,7vw,3.3rem) → ladder endpoints */
  font-size: clamp(var(--fs-2), 7vw, var(--fs-4));
  line-height: 1.04;
  margin: 0 0 var(--sp-4);
  color: var(--on-dark-heading);
}
.ty h1 em { font-style: italic; color: var(--gold-hi); }
.ty__msg {
  color: var(--on-dark-secondary);
  font-size: var(--fs-0);
  margin: 0 0 var(--sp-6);
}
.ty__cta {
  display: flex;
  gap: var(--sp-3);
  justify-content: center;
  flex-wrap: wrap;
}
/* rem, not px — fixes G's one shipped 13px literal on this page */
.ty__vote {
  margin: var(--sp-7) 0 0;
  font-size: 0.8125rem;
  color: var(--on-dark-caption);
}
.ty__disclaimer {
  margin: 10px 0 0; /* optical — --sp-2 +2 */
  font-size: 0.7812rem;
  color: var(--on-dark-legal);
}

/* ═══ Motion-safe enhancements only below this line ═══════════════
   Everything decorative. Reduced motion (or no JS, or ?noanim=1)
   gets fully readable static states: the check mark still appears
   on :checked, the thank-you badge renders fully drawn. */
@media (prefers-reduced-motion: no-preference) {
  .field input,
  .field select,
  .field textarea {
    transition: border-color .3s, background .3s, box-shadow .3s;
  }
  .chk {
    transition: border-color .28s, background .28s, transform .28s var(--ease);
  }
  .chk:hover { transform: translateY(-2px); }
  .chk input { transition: background .22s, border-color .22s; }
  .chk input::after { transition: transform .22s var(--ease); }
  .formcard__follow { transition: color .3s; }
  .formcard__follow svg:last-child { transition: transform .35s var(--ease); }
  .formcard__follow:hover svg:last-child { transform: translateX(4px); }

  /* thank-you check draw-on — the UNDRAWN state lives INSIDE the
     keyframes (from → to), never in the resting CSS: kill the
     animation any way at all (?noanim=1's animation:none, reduced
     motion, a dropped keyframe) and the resting state IS the fully
     drawn badge. fill-mode: both covers the start delays with the
     from-state while the animation is alive. */
  .ty__check rect {
    stroke-dasharray: 416;
    animation: ty-draw .9s var(--ease) .15s both;
  }
  .ty__check .tick {
    stroke-dasharray: 150;
    animation: ty-draw-tick .7s var(--ease) .85s both;
  }
  @keyframes ty-draw {
    from { stroke-dashoffset: 416; }
    to   { stroke-dashoffset: 0; }
  }
  /* the tick's own keyframes: its undrawn offset is its own dash
     length (150) — starting it at 416 would wrap the 150-dash
     pattern and leave part of the tick visible before the draw */
  @keyframes ty-draw-tick {
    from { stroke-dashoffset: 150; }
    to   { stroke-dashoffset: 0; }
  }
}

/* ── Responsive (system breakpoints only) ──────────────────────── */
@media (max-width: 1080px) {
  .forms { grid-template-columns: 1fr; max-width: 640px; }
}
@media (max-width: 620px) {
  .f-row { grid-template-columns: 1fr; }
  .checks__grid { grid-template-columns: 1fr; }
}
@media (min-width: 1700px) {
  .forms { max-width: 1180px; }
}
@media (min-width: 2300px) {
  .forms { max-width: 1280px; }
}


/* ═══ footer/footer.css ═══ */
/* ═══════════════════════════════════════════════════════════════
   D4D component: footer — dark ground, nav echo, social, legal
   Ported from reference/gertsen main.css §footer (measured, not
   re-imagined), values swapped to tokens. Requires tokens.css,
   base.css. No JS.

   All on-dark text uses the tokenized alpha ramp — G's ad-hoc
   whites (.7/.5/.46/.55/.22/.20/.08…) consolidate onto the named
   steps (--on-dark-secondary/-legal/-caption/-heading/-hairline),
   which is exactly the cleanup DESIGN.md says the ramp exists for.
   Print: a container-level color flatten cannot reach the child
   colors set explicitly below, so base.css's @media print block
   overrides the --on-dark-* ramp tokens themselves — every
   ramp-tokened line here flattens to ink-on-white through the
   tokens, and this file ships no print rules of its own.
   ═══════════════════════════════════════════════════════════════ */

.foot {
  background: var(--ink-950);
  color: var(--on-dark-body);
  padding: 64px 0 30px;    /* 64 = --sp-9 −2 · 30 = --sp-6 −2, G optical */
  position: relative;
  overflow: hidden;
}

/* Coroplast-rib wash — this section's ONE texture (max-one rule).
   A pseudo-element: nothing exists for readers, no aria needed.
   White ribs on the dark ground (textures.css .tex-ribs is the
   ink-on-light sibling). */
.foot::before {
  content: "";
  position: absolute;
  inset: 0;
  opacity: .06;
  pointer-events: none;
  background-image: repeating-linear-gradient(90deg,
    var(--white) 0 1px, transparent 1px 11px);
}

/* content rows sit above the rib wash */
.foot__inner,
.foot__contact,
.foot__legal { position: relative; }

.foot__inner {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--sp-7);
  align-items: center;
}

/* ── Stacked generated wordmark ────────────────────────────────── */
.foot__wordmark {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}
.foot__name {
  font: 600 var(--fs-2)/1.05 var(--ff-d);
  letter-spacing: -.01em;
  color: var(--on-dark-heading);
}
.foot__office {
  font: 600 var(--fs-kicker)/1.4 var(--ff-b);
  letter-spacing: var(--ls-kicker);
  text-transform: uppercase;
  color: var(--on-dark-secondary);
}

/* ── Nav echo + social ─────────────────────────────────────────── */
.foot__cols {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 26px;               /* 26 = --sp-5 +2, G optical */
  flex-wrap: wrap;
}

.foot__nav {
  display: flex;
  flex-wrap: wrap;
  gap: 2px 22px;           /* 22 = --sp-5 −2; 2px row-gap keeps wrapped rows tight (G optical) */
}
/* inline-flex + min-height = 44px touch target — G's commented fix
   ("they were 24px tall on phones"), kept as the platform floor */
.foot__nav a {
  color: var(--on-dark-secondary);
  text-decoration: none;
  font-size: 0.9062rem;    /* G measured, adopted verbatim (matches header nav) */
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}
.foot__nav a:hover,
.foot__nav a:focus-visible { color: var(--gold-hi); }

.foot__social {
  display: flex;
  gap: var(--sp-3);
  list-style: none;
  margin: 0;
  padding: 0;
}
.foot__social a {
  display: grid;
  place-items: center;
  width: 44px;             /* touch target */
  height: 44px;
  /* themed control shape: circle in soft, square in sharp */
  border-radius: var(--r-control);
  border: 1px solid var(--on-dark-hairline);  /* G's .22 consolidated to the ramp token */
  color: var(--white);
}
/* recolor is contrast-checked: --gold-ink on --gold = 7.28:1
   (recorded in buttons.css AA table) */
.foot__social a:hover,
.foot__social a:focus-visible {
  background: var(--gold);
  color: var(--gold-ink);
  border-color: var(--gold);
}
.foot__social svg { display: block; }

/* ── Contact row ───────────────────────────────────────────────── */
.foot__contact {
  display: flex;
  flex-wrap: wrap;
  gap: 2px 22px;           /* matches the nav echo rhythm */
  margin-top: var(--sp-5);
}
.foot__contact a {
  color: var(--on-dark-secondary);
  text-decoration: none;
  font-size: 0.9062rem;
  display: inline-flex;
  align-items: center;
  min-height: 44px;        /* touch target */
}
.foot__contact a:hover,
.foot__contact a:focus-visible { color: var(--gold-hi); }

/* ── Legal strip ───────────────────────────────────────────────── */
.foot__legal {
  margin-top: var(--sp-7);
  padding-top: 22px;       /* 22 = --sp-5 −2, G optical */
  border-top: 1px solid var(--on-dark-hairline);
  display: flex;
  justify-content: space-between;
  gap: var(--sp-4);
  flex-wrap: wrap;
}
.foot__legal p {
  margin: 0;
  font-size: 0.8125rem;    /* G measured, adopted verbatim */
  color: var(--on-dark-legal);
}
/* the committee name reads a step brighter than the legal boilerplate */
.disclaimer b {
  color: var(--on-dark-heading);
  font-weight: 650;
}
.foot__vote { text-align: right; }

/* Vendor credit — its own quiet centered line (Decision 21).
   Scoped .foot__legal .foot__by outranks .foot__legal p cleanly —
   no !important fights (the G specificity anti-pattern). */
.foot__legal .foot__by {
  flex-basis: 100%;
  text-align: center;
  margin-top: 14px;        /* 14 = --sp-3 +2, G optical */
  padding-top: 14px;
  border-top: 1px solid var(--on-dark-hairline);  /* G's .08 consolidated to the ramp token */
  font-size: 0.75rem;      /* G measured, adopted verbatim */
  color: var(--on-dark-legal);
}
.foot__by a {
  color: var(--on-dark-caption);
  text-decoration: none;
  border-bottom: 1px solid var(--on-dark-hairline);
  padding-bottom: 1px;
}
.foot__by a:hover,
.foot__by a:focus-visible {
  color: var(--gold-hi);
  border-color: var(--gold-hi);
}

/* ── Motion-safe enhancements only ─────────────────────────────── */
@media (prefers-reduced-motion: no-preference) {
  .foot__nav a,
  .foot__contact a { transition: color .3s; }
  .foot__social a {
    transition: background .35s var(--ease),
                transform .35s var(--ease),
                border-color .35s,
                color .35s;
  }
  .foot__social a:hover { transform: translateY(-3px); }
  .foot__by a { transition: color .3s, border-color .3s; }
}

/* ── Breakpoints — system set only ─────────────────────────────── */
@media (max-width: 1080px) {
  .foot__inner {
    grid-template-columns: 1fr;
    gap: 30px;             /* 30 = --sp-6 −2, G optical */
  }
  .foot__cols {
    flex-direction: column;
    align-items: flex-start;
  }
}

/* ── Print ──────────────────────────────────────────────────────────
   base.css's print block flattens the --on-dark-* ramp tokens, but the
   social icons are literal --white — cover them here so glyphs don't
   print white-on-white inside their bordered chips. */
@media print {
  .foot__social a { color: var(--black); border-color: var(--black); }
}

@media (max-width: 860px) {
  .foot__legal { flex-direction: column; }
  .foot__vote { text-align: left; }
  .foot__name { font-size: var(--fs-1); }  /* G narrowed its brand art here too */
}
