/* =============================================================================
   OpenMFDA Foundry — main.css
   Global styles shared by every page. Landing-page-specific blocks
   (pillar grid, featured chip card, citations grid) live in home.css.

   Conventions:
     - No inline hex codes below :root. Reference var(--...) everywhere.
     - Mobile-first. Two em-based breakpoints: 48em (tablet), 80em (desktop).
     - Only seven utility patterns: .container, .stack-{4,6,8,12}, .cluster,
       .sr-only, .text-muted, .text-mono, .text-center.
     - Sparing-orange: --color-accent appears as a background only on .badge-phase2.
   ========================================================================== */


/* -----------------------------------------------------------------------------
   1. Design tokens
   -------------------------------------------------------------------------- */
:root {
  --color-deep-blue: #065A82;
  --color-teal: #1C7293;
  --color-navy: #21295C;
  --color-ice: #F0F7FA;
  --color-accent: #F09D51;
  /* Darkened accent, for accent-coloured TEXT only (the raw accent fails
     contrast as text on --bg-soft). 6.3:1 on --bg-soft — passes WCAG AA.
     Used by the "test print" status badge, the one orange badge in the
     catalog; never as a background fill. */
  --color-accent-ink: #8A4A12;
  /* Light blue, for links on the navy hero only. The base link colour,
     deep blue, measures 1.8:1 there, under the 4.5:1 minimum for text;
     this measures 6.8:1 through the intro's 0.92 opacity.
     verify_home_page.py measures every hero link on the rendered page. */
  --color-sky: #8CCBEB;
  --mesh-warmth: rgba(240, 157, 81, 0.06);
  --color-text: #1A2B3C;
  /* Design review 2026-10-03, item 10: muted text a shade darker (was
     #5C7080), 5.5:1 on --bg-soft; a decorative card border and a control
     border (3.85:1 on white: outlines only, never text). */
  --color-muted: #526675;
  --color-border: #D4E1E8;
  --color-control-border: #748492;
  --color-white: #FFFFFF;

  --bg-page: var(--color-white);
  --bg-dark: var(--color-navy);
  --bg-soft: var(--color-ice);
  --fg-body: var(--color-text);
  --fg-muted: var(--color-muted);
  --fg-on-dark: var(--color-ice);
  --link: var(--color-deep-blue);
  --link-hover: var(--color-teal);
  --link-on-dark: var(--color-sky);
  --link-on-dark-hover: var(--color-ice);
  --accent: var(--color-accent);

  --font-serif: Cambria, Georgia, "Times New Roman", serif;
  --font-sans: Calibri, "Inter", system-ui, -apple-system, sans-serif;
  --font-mono: "JetBrains Mono", "Fira Code", Consolas, monospace;

  --fs-300: 0.875rem; --fs-400: 1rem; --fs-500: 1.125rem;
  --fs-600: 1.375rem; --fs-700: 1.75rem; --fs-800: 2.25rem; --fs-900: 3rem;

  --sp-1: .25rem; --sp-2: .5rem; --sp-3: .75rem; --sp-4: 1rem;
  --sp-6: 1.5rem; --sp-8: 2rem; --sp-12: 3rem; --sp-16: 4rem; --sp-24: 6rem;

  --container-max: 72rem;
  --container-pad: var(--sp-6);
  --radius: 6px;
  --shadow-card: 0 1px 2px rgba(26, 43, 60, 0.06), 0 4px 12px rgba(26, 43, 60, 0.06);
  --shadow-card-hover: 0 4px 8px rgba(26, 43, 60, 0.10), 0 16px 32px rgba(26, 43, 60, 0.08);
  /* Deeper hover variant used only by .chip-card:hover (home.css). The base
     --shadow-card-hover stays calibrated for .pillar-card's restrained -2px
     lift; chip-cards graduate to -4px and need a roomier shadow to match. */
  --shadow-card-hover-strong: 0 6px 12px rgba(26, 43, 60, 0.12), 0 24px 48px rgba(26, 43, 60, 0.10);
  /* Inset shadow on the chip-card SVG art tray (.chip-card__media-link).
     Hairline inner border + soft top-edge inner shadow — suggests recess
     without darkening the ice fill. Token-only per the no-inline-hex rule. */
  --shadow-art-inset: 0 0 0 1px rgba(26, 43, 60, 0.04), 0 2px 4px rgba(26, 43, 60, 0.06);
  /* Teal-tinted ice for non-interactive pill hover (.cell-list code).
     12% --color-teal RGB on top of the implicit white page background
     resolves visually to "ice with a slight teal lean" — desktop polish. */
  --bg-soft-hover: rgba(28, 114, 147, 0.12);
}


/* -----------------------------------------------------------------------------
   2. Modern minimal reset
   -------------------------------------------------------------------------- */
*, *::before, *::after {
  box-sizing: border-box;
}

:where(html, body, h1, h2, h3, h4, h5, h6, p, figure, blockquote, dl, dd, ul, ol) {
  margin: 0;
  padding: 0;
}

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

body {
  min-height: 100vh;
  background: var(--bg-page);
  color: var(--fg-body);
  font-family: var(--font-sans);
  font-size: var(--fs-400);
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}

:where(img, picture, svg, video) {
  display: block;
  max-width: 100%;
}

:where(button, input, select, textarea) {
  font: inherit;
  color: inherit;
}

:where(ul, ol)[class] {
  list-style: none;
}


/* -----------------------------------------------------------------------------
   3. Reduced-motion opt-out (sits above components so it wins specificity-tied)
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition: none !important;
    animation: none !important;
    scroll-behavior: auto !important;
  }

  /* Force the .reveal end state immediately — no animation, no waiting.
     Without these, killing the transition leaves elements stuck at
     opacity: 0; transform: translateY(16px). !important is legitimate
     here because we're overriding rules declared later in the cascade
     from inside a media-query layer above them. */
  .reveal {
    opacity: 1 !important;
    transform: none !important;
  }

  /* Same for the hero — already in view, but its animation is suppressed. */
  .hero {
    opacity: 1 !important;
    transform: none !important;
  }
}


/* -----------------------------------------------------------------------------
   4. Base typography
   -------------------------------------------------------------------------- */
h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-serif);
  color: var(--fg-body);
  font-weight: 600;
}

h1 { font-size: var(--fs-900); line-height: 1.2; letter-spacing: -0.015em; }
h2 { font-size: var(--fs-800); line-height: 1.2; }
h3 { font-size: var(--fs-700); line-height: 1.2; }
h4 { font-size: var(--fs-600); line-height: 1.25; }
h5 { font-size: var(--fs-500); line-height: 1.25; }
h6 { font-size: var(--fs-400); line-height: 1.25; }

p {
  line-height: 1.65;
}

code, kbd, pre, samp {
  font-family: var(--font-mono);
  font-size: 0.95em;
}

pre {
  overflow-x: auto;
  padding: var(--sp-4);
  background: var(--bg-soft);
  border-radius: var(--radius);
  line-height: 1.6;
}

pre code {
  background: transparent;
  padding: 0;
  font-size: var(--fs-300);
}

a {
  color: var(--link);
  text-decoration: underline;
  text-underline-offset: 3px;
  transition: color 120ms ease;
}

a:hover {
  color: var(--link-hover);
}

/* The focus ring. Deep blue on the white and ice pages (7.5:1 on white,
   6.9:1 on ice); the accent orange measured 2.2:1 on white, under the 3:1 a
   focus indicator needs. On the navy sections, where deep blue would be
   1.8:1, it is the orange (6.3:1): those sections set --focus-ring. */
:root { --focus-ring: var(--color-deep-blue); }
.hero,
.site-footer,
.chip-verilog,
.cb-intro,
.cb-stage,
.cb-outro { --focus-ring: var(--accent); }

:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

/* Section-heading treatment: Cambria + a 60px teal accent rule beneath
   the text. Use the class on h2s that should read as "important section
   marker" — e.g. "Chip library", "Cite OpenMFDA", "How it works". */
.section-heading {
  font-family: var(--font-serif);
  position: relative;
  padding-bottom: var(--sp-4);
  margin-bottom: var(--sp-6);
}

.section-heading::after {
  content: "";
  display: block;
  position: absolute;
  left: 0;
  bottom: 0;
  width: 60px;
  height: 3px;
  background: var(--color-teal);
  border-radius: 1.5px;
}


/* -----------------------------------------------------------------------------
   5. Skip link
   -------------------------------------------------------------------------- */
.skip-link {
  position: absolute;
  left: -10000px;
  top: auto;
  width: 1px;
  height: 1px;
  overflow: hidden;
  background: var(--color-white);
  color: var(--fg-body);
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--radius);
  text-decoration: none;
}

.skip-link:focus,
.skip-link:focus-visible {
  position: static;
  left: auto;
  width: auto;
  height: auto;
  overflow: visible;
  display: inline-block;
  margin: var(--sp-3);
  z-index: 999;
}


/* -----------------------------------------------------------------------------
   6. Container
   -------------------------------------------------------------------------- */
.container {
  width: min(var(--container-max), 100% - 2 * var(--container-pad));
  margin-inline: auto;
}


/* -----------------------------------------------------------------------------
   7. Site header & primary nav
   -------------------------------------------------------------------------- */
.site-header {
  background: var(--bg-page);
  border-bottom: 1px solid var(--bg-soft);
  position: relative;
  z-index: 50;
}

.site-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  padding-block: var(--sp-4);
  flex-wrap: wrap;
}

.wordmark {
  font-family: var(--font-serif);
  font-size: var(--fs-600);
  font-weight: 600;
  color: var(--color-deep-blue);
  text-decoration: none;
  letter-spacing: 0.005em;
}

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

.nav-toggle {
  display: inline-flex;
  flex-direction: column;
  justify-content: space-between;
  width: 2.25rem;
  height: 1.5rem;
  padding: 0;
  background: transparent;
  border: 0;
  cursor: pointer;
}

.nav-toggle__bar {
  display: block;
  width: 100%;
  height: 2px;
  background: var(--color-deep-blue);
  border-radius: 1px;
  transition: transform 150ms ease, opacity 150ms ease;
  transform-origin: center;
}

/* Hamburger -> X when the toggle is expanded. Reduced-motion users
   get the same end state without the animation (handled globally). */
.nav-toggle[aria-expanded="true"] .nav-toggle__bar:nth-child(1) {
  transform: translateY(0.6875rem) rotate(45deg);
}

.nav-toggle[aria-expanded="true"] .nav-toggle__bar:nth-child(2) {
  opacity: 0;
}

.nav-toggle[aria-expanded="true"] .nav-toggle__bar:nth-child(3) {
  transform: translateY(-0.6875rem) rotate(-45deg);
}

/* No-JS-first: .primary-nav is visible by default on mobile so users without
   JavaScript still get a stacked menu. Once main.js runs it sets
   aria-expanded="false" on the toggle, which activates the hide rule below.
   The 48em breakpoint later in this file forces display: block regardless. */
.primary-nav {
  flex-basis: 100%;
}

.nav-toggle[aria-expanded="false"] + .primary-nav {
  display: none;
}

.nav-toggle[aria-expanded="true"] + .primary-nav,
.primary-nav.is-open {
  display: block;
}

.primary-nav ul {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding: var(--sp-4) 0 var(--sp-2);
}

.primary-nav a {
  display: inline-block;
  padding: var(--sp-2) var(--sp-1);
  color: var(--fg-body);
  text-decoration: none;
  font-size: var(--fs-400);
}

.primary-nav a:hover,
.primary-nav a:focus-visible {
  color: var(--link);
  text-decoration: underline;
  text-underline-offset: 4px;
}


/* -----------------------------------------------------------------------------
   8. Hero (above-the-fold; promoted from inline critical CSS).
      NOTE: hero rules live here in main.css because removing the inlined
      <style> block would otherwise FOUC the landing page before home.css
      arrives. They can be relocated to home.css in a later refactor pass.
   -------------------------------------------------------------------------- */
.hero {
  position: relative;
  background: var(--color-navy);
  /* Six layered backgrounds, top to bottom:
       1. Channel-pattern SVG (static; opacity 0.06 baked into the SVG).
       2. Deep-blue radial — top-left anchor.
       3. Teal radial — top-right balance.
       4. Deep-blue radial — bottom-right anchor.
       5. Warmth radial — bottom-left (the SOLE sparing-orange surface here;
          uses --mesh-warmth = rgba(240, 157, 81, 0.06)).
       6. Solid navy base (encoded as a flat linear-gradient so the
          background-position stack lines up across all six layers).

     The raw `%23ffffff` here is URL-encoded `#ffffff` inside a data URI
     inside a CSS url(). It is the SOLE inline-hex exception to the
     "no-hex-below-:root" rule because CSS custom properties (var(--*))
     do NOT expand inside url() — the data URI is opaque to the parser. */
  background-image:
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 600' preserveAspectRatio='xMidYMid slice'><g fill='none' stroke='%23ffffff' stroke-width='1.2' stroke-linecap='round' opacity='0.06'><path d='M 0 120 L 200 120 L 220 100 L 240 140 L 260 100 L 280 140 L 300 120 L 800 120 L 820 100 L 840 140 L 860 100 L 880 140 L 900 120 L 1200 120'/><path d='M 0 300 L 400 300 L 420 280 L 440 320 L 460 280 L 480 320 L 500 300 L 1200 300'/><path d='M 0 480 L 150 480 L 170 460 L 190 500 L 210 460 L 230 500 L 250 480 L 700 480'/><circle cx='220' cy='120' r='3' fill='%23ffffff'/><circle cx='880' cy='120' r='3' fill='%23ffffff'/><circle cx='440' cy='300' r='3' fill='%23ffffff'/><circle cx='190' cy='480' r='3' fill='%23ffffff'/></g></svg>"),
    radial-gradient(ellipse 60% 50% at 18% 22%, var(--color-deep-blue) 0%, transparent 55%),
    radial-gradient(ellipse 55% 45% at 82% 30%, var(--color-teal) 0%, transparent 50%),
    radial-gradient(ellipse 70% 60% at 70% 85%, var(--color-deep-blue) 0%, transparent 60%),
    radial-gradient(circle 35% 35% at 30% 78%, var(--mesh-warmth) 0%, transparent 55%),
    linear-gradient(var(--color-navy), var(--color-navy));
  background-color: var(--color-navy);  /* fallback if all gradient layers fail to paint */
  background-repeat: no-repeat, no-repeat, no-repeat, no-repeat, no-repeat, no-repeat;
  background-size: cover, cover, cover, cover, cover, cover;
  background-attachment: scroll, scroll, scroll, scroll, scroll, scroll;
  color: var(--color-white);
  padding-block: var(--sp-16) var(--sp-12);
  animation: hero-fade-up 600ms cubic-bezier(.2, .7, .2, 1) both,
             hero-mesh-drift 20000ms ease-in-out infinite alternate;
}

/* Drift the four radial layers in opposing pairs. The channel-pattern
   (layer 1) and the solid-navy base (layer 6) stay at 0% 0%; only the
   four radials shift ±2% so the mesh breathes without inducing moiré
   with the static SVG channel texture above it. Reduced-motion users
   get the global `animation: none !important` kill — no new rule. */
@keyframes hero-mesh-drift {
  0%   { background-position: 0% 0%, 0% 0%, 0% 0%, 0% 0%, 0% 0%, 0% 0%; }
  50%  { background-position: 0% 0%, -2% 1%, 2% -1%, -1% 2%, 1% -2%, 0% 0%; }
  100% { background-position: 0% 0%, 0% 0%, 0% 0%, 0% 0%, 0% 0%, 0% 0%; }
}

/* Initial-load fade-up. Distinct from .reveal (which is scroll-triggered).
   The hero is above the fold so it animates on first paint, not on
   viewport entry. */
@keyframes hero-fade-up {
  from {
    opacity: 0;
    transform: translateY(12px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* Eyebrow line above the hero h1. --fg-muted would not clear
   WCAG AA on the navy mesh, so we override with ice at 60% opacity for a
   muted "card key" treatment. One-off — no new token. */
.hero__eyebrow {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.85rem;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--color-ice);
  opacity: 0.6;
  margin-bottom: var(--sp-3);
}

.hero h1 {
  font-family: var(--font-serif);
  font-size: clamp(2.25rem, 6vw, 6rem);
  line-height: 1.1;
  letter-spacing: -0.02em;
  color: var(--color-white);
  max-width: 22ch;
  margin-bottom: var(--sp-4);
}

.hero .lede {
  font-size: var(--fs-500);
  max-width: 52ch;
  margin-bottom: var(--sp-8);
  opacity: 0.92;
  line-height: 1.55;
}

/* The intro's links. The base link colour, deep blue, all but vanishes on
   the navy; these stay underlined like every link, in a light blue that
   clears 4.5:1 there, and go lighter still on hover. */
.hero .lede a {
  color: var(--link-on-dark);
}

.hero .lede a:hover {
  color: var(--link-on-dark-hover);
}

.cta-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-4) var(--sp-6);
  align-items: center;
}

/* Larger primary CTA inside the hero only — scope keeps other contexts
   (request-band, eventual page-headers) on the base button sizing. */
.hero .btn-primary {
  padding: 1rem 1.75rem;
  font-size: var(--fs-500);
}

/* Secondary CTA: underline-from-left on hover. The ::after pseudo-element
   inherits currentColor, so it tracks both the hero (white) and the
   request-band (deep-blue) without further scoping. */
.cta-secondary {
  color: var(--color-white);
  font-size: var(--fs-400);
  text-decoration: none;
  position: relative;
  padding-bottom: 4px;
}

.cta-secondary::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 150ms ease;
}

.cta-secondary:hover::after,
.cta-secondary:focus-visible::after {
  transform: scaleX(1);
}

.cta-secondary:hover {
  color: var(--bg-soft);
}


/* --- Hero scroll indicator -------------------------------------------- */
/* Decorative chevron bobbing at the bottom of the hero. aria-hidden in
   markup; href="#main" lets sighted users click through if they want.
   main.js adds .is-hidden once scrollY exceeds one viewport height. */
.hero__scroll {
  position: absolute;
  left: 50%;
  bottom: var(--sp-6);
  transform: translateX(-50%);
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-ice);
  opacity: 0.5;
  pointer-events: none;
  transition: opacity 200ms ease;
  animation: hero-scroll-bob 2000ms ease-in-out infinite;
}

.hero__scroll svg {
  width: 100%;
  height: 100%;
}

.hero__scroll.is-hidden {
  opacity: 0;
}

@keyframes hero-scroll-bob {
  0%   { transform: translate(-50%, 0);   opacity: 0.5; }
  50%  { transform: translate(-50%, 4px); opacity: 0.85; }
  100% { transform: translate(-50%, 0);   opacity: 0.5; }
}


/* -----------------------------------------------------------------------------
   8b. Global keyframes shared across pages
       @keyframes are looked up by name regardless of which stylesheet they
       live in (as long as that stylesheet is loaded on the page). This block
       collects animations applied by more than one page so neither home.css
       nor detail.css owns the canonical definition.

       validated-corner-pulse — applied by:
         - .chip-card.reveal-in .validated-corner    (home.css; home page)
         - .chip-hero.reveal-in .validated-corner    (detail.css; per-chip page)
       The pulse runs once, 500ms after .reveal-in lands. Reduced-motion users
       get `animation: none !important` from the global block at section 3 and
       see the corner mark in its rest state (scale 1) — the same end state.
   -------------------------------------------------------------------------- */
@keyframes validated-corner-pulse {
  0%   { transform: scale(1); transform-origin: center; }
  50%  { transform: scale(1.15); transform-origin: center; }
  100% { transform: scale(1); transform-origin: center; }
}


/* -----------------------------------------------------------------------------
   9. Buttons
   -------------------------------------------------------------------------- */
.btn {
  display: inline-block;
  font-family: var(--font-sans);
  font-size: var(--fs-400);
  line-height: 1.2;
  padding: 0.75rem 1.4rem;
  border-radius: var(--radius);
  border: 1px solid transparent;
  text-decoration: none;
  cursor: pointer;
  transition: background-color 150ms ease, color 150ms ease, border-color 150ms ease;
}

.btn-primary {
  background: var(--color-white);
  color: var(--color-deep-blue);
  border-color: var(--color-white);
}

.btn-primary:hover,
.btn-primary:focus-visible {
  background: var(--bg-soft);
  color: var(--color-deep-blue);
}

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

.btn-secondary:hover,
.btn-secondary:focus-visible {
  background: var(--bg-soft);
  color: var(--link-hover);
  border-color: var(--link-hover);
}

.btn-disabled {
  background: var(--bg-soft);
  color: var(--fg-muted);
  border-color: var(--bg-soft);
  cursor: not-allowed;
  opacity: 0.5;
  pointer-events: none;
}


/* -----------------------------------------------------------------------------
   10. Badges
   -------------------------------------------------------------------------- */
.badge {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: var(--fs-300);
  line-height: 1;
  padding: 0.25rem 0.6rem;
  border-radius: 999px;
  letter-spacing: 0.02em;
}

.badge-available {
  background: var(--color-teal);
  color: var(--color-white);
}

.badge-beta {
  background: var(--color-deep-blue);
  color: var(--color-white);
}

/* Sparing-orange: the ONLY rule in main.css that uses --color-accent as
   a background surface. Confirmed via the v0.1 visual-identity policy. */
.badge-phase2 {
  background: var(--color-accent);
  color: var(--color-navy);
}

/* Chip-status badges. .badge-validated is functionally identical to
   .badge-available (teal/white); kept separate so the chip-status taxonomy
   (validated | concept) greps cleanly. .badge-concept uses an outline-style
   treatment to read as "not-yet-shipped, but not failure" — distinct from
   the filled muted-gray .badge-coming-soon used on stub pages. */
.badge-validated {
  background: var(--color-teal);
  color: var(--color-white);
}

.badge-concept {
  background: var(--bg-soft);
  color: var(--fg-muted);
  border: 1px solid var(--color-muted);
}

/* Pipeline-demo chips — neutral framing, no validation claim. Uses the
   muted token to read as "demonstrated but not assay-validated"; the
   pillar-3 / phase2 orange budget stays reserved for genuinely unshipped
   surfaces. */
.badge-demo {
  background: var(--color-muted);
  color: var(--color-white);
}

/* Wet-lab-validated chips — highest chip-status tier. Visually parallel
   to .badge-beta (deep-blue/white), kept a distinct class so the chip-
   status taxonomy (wlv | validated | demo | concept) greps cleanly, same
   reason .badge-validated and .badge-available are kept separate above.
   Deeper than .badge-validated's teal to read as "real-deal, wet-lab
   confirmed" without spending the orange budget. */
.badge-wlv {
  background: var(--color-deep-blue);
  color: var(--color-white);
}


/* -----------------------------------------------------------------------------
   11. Site footer
   -------------------------------------------------------------------------- */
.site-footer {
  background: var(--color-navy);
  color: var(--fg-on-dark);
  margin-top: var(--sp-24);
}

/* Block 9f: footer link underline replaced with a ::after pseudo that
   scales from left on hover. Applies to every .site-footer a — column
   links, the License link, the mailto: contact, and the copyright-strip
   links. The interaction language is now consistent across the footer.
   prefers-reduced-motion is handled by the global rule at section 3
   (transition: none !important) — reduced-motion users get an instant
   underline-appear; end state matches. */
.site-footer a {
  color: var(--fg-on-dark);
  text-decoration: none;
  position: relative;
  display: inline-block;
  padding-bottom: 2px;
}

.site-footer a::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 120ms ease;
}

.site-footer a:hover::after,
.site-footer a:focus-visible::after {
  transform: scaleX(1);
}

/* Block 9f: hairline rule above the column grid. Ice at 18% on the navy
   field reads as a faint divider — softer than --color-muted at 30% would,
   which on the dark fill goes nearly invisible. The literal rgba is the
   visual outcome the user described; rgba below :root is allowed (other
   shadow tokens already do the same). */
.site-footer__grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-8);
  padding-block: var(--sp-12);
  border-top: 1px solid rgba(240, 247, 250, 0.18);
}

/* Block 9f: column headings up from --fs-500 to --fs-600, with an
   explicit weight (instead of inherited 600) and tightened letter-spacing.
   margin-bottom bumped from --sp-3 to --sp-4 to rebalance against the
   bigger heading. */
.site-footer h4 {
  font-family: var(--font-serif);
  font-size: var(--fs-600);
  font-weight: 600;
  letter-spacing: -0.005em;
  color: var(--fg-on-dark);
  margin-bottom: var(--sp-4);
}

.site-footer p {
  font-size: var(--fs-400);
  opacity: 0.92;
}

.footer-col {
  min-width: 0;
}

.footer-links {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding: 0;
}

/* Block 9f: copyright strip tightened — smaller and more muted so the
   legal line sits visibly below the column headings without competing
   for attention. 0.8rem is a one-off literal between --fs-300 (0.875rem)
   and the smallest tokens we'd want to introduce; adding a single-use
   --fs-200 token would be pure overhead. */
.site-footer__bottom {
  border-top: 1px solid rgba(240, 247, 250, 0.12);
  padding-block: var(--sp-3);
  font-size: 0.8rem;  /* one-off below --fs-300 */
  opacity: 0.6;
}


/* -----------------------------------------------------------------------------
   12. Utilities — exactly seven patterns. Do not add more here; if you
       need something new, write a component class instead.
   -------------------------------------------------------------------------- */
.stack-4  > * + * { margin-top: var(--sp-4); }
.stack-6  > * + * { margin-top: var(--sp-6); }
.stack-8  > * + * { margin-top: var(--sp-8); }
.stack-12 > * + * { margin-top: var(--sp-12); }

.cluster {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-4);
  align-items: center;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.text-muted   { color: var(--fg-muted); }
.text-mono    { font-family: var(--font-mono); }
.text-center  { text-align: center; }


/* -----------------------------------------------------------------------------
   13. Breakpoints — mobile-first, em-based.
   -------------------------------------------------------------------------- */
@media (min-width: 48em) {
  :root {
    --container-pad: var(--sp-8);
  }

  .site-header__inner {
    flex-wrap: nowrap;
  }

  .nav-toggle {
    display: none;
  }

  /* Beat the mobile hide rule by matching its specificity: same compound
     selector inside the breakpoint forces the nav visible regardless of
     aria-expanded state. */
  .primary-nav,
  .nav-toggle[aria-expanded="false"] + .primary-nav,
  .nav-toggle[aria-expanded="true"] + .primary-nav {
    display: block;
    flex-basis: auto;
  }

  .primary-nav ul {
    flex-direction: row;
    gap: var(--sp-6);
    padding: 0;
  }

  .hero {
    padding-block: var(--sp-24) var(--sp-16);
  }

  /* Parallax-ish: the channel-pattern layer (layer 1) stays put while
     every other layer (the four radials + the solid-navy base) scrolls
     with the page. Desktop only — iOS Safari handles
     `background-attachment: fixed` poorly, so mobile keeps all-scroll.
     The radial layers stay scroll for cost reasons (a fixed full-bleed
     gradient triggers repaint cost on scroll); only the lightweight
     pattern layer is fixed. */
  .hero {
    background-attachment: fixed, scroll, scroll, scroll, scroll, scroll;
  }

  .site-footer__grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (min-width: 80em) {
  :root {
    --container-pad: var(--sp-12);
  }

  h1 { font-size: calc(var(--fs-900) * 1.1); line-height: 1.1; }
  h2 { font-size: calc(var(--fs-800) * 1.05); line-height: 1.1; }
  h3 { line-height: 1.15; }

  .site-footer__grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}


/* -----------------------------------------------------------------------------
   14. Page patterns — shared by /library/, /design/, /docs/, /about/
       and any future content page. Lighter analog of the landing hero.
   -------------------------------------------------------------------------- */
.page-header {
  background: var(--bg-soft);
  padding-block: var(--sp-12);
}

.page-header h1 {
  font-family: var(--font-serif);
  font-size: clamp(1.75rem, 4vw, 2.5rem);
  line-height: 1.2;
  color: var(--fg-body);
  max-width: 28ch;
  margin-bottom: var(--sp-3);
}

.page-header__lede {
  font-size: var(--fs-500);
  max-width: 62ch;
  color: var(--fg-body);
}

/* Sits ABOVE the h1 to flag stub pages as not-yet-shipped. */
.page-header .badge-coming-soon {
  margin-bottom: var(--sp-3);
}

/* Neutral grey badge variant: distinct from the three featured-block badges
   (Available teal, Beta deep-blue, Phase 2 orange) and respectful of the
   sparing-orange rule — accent stays reserved for .badge-phase2. */
.badge-coming-soon {
  background: var(--color-muted);
  color: var(--color-white);
}

.page-body {
  background: var(--bg-page);
  padding-block: var(--sp-12);
}

.page-body > .container > * + * {
  margin-top: var(--sp-6);
}

.page-back {
  font-size: var(--fs-300);
  margin-top: var(--sp-8);
}

@media (min-width: 48em) {
  .page-header {
    padding-block: var(--sp-16);
  }

  .page-body {
    padding-block: var(--sp-16);
  }
}


/* -----------------------------------------------------------------------------
   15. Reveal-on-scroll — sections start with .reveal applied; an
       IntersectionObserver in main.js (wired in Block 8d) swaps the
       class to .reveal-in when the element enters the viewport. The
       transition runs only on that .reveal -> .reveal-in flip.
   -------------------------------------------------------------------------- */
/* Initial-hidden state is gated under BOTH html.js and
   prefers-reduced-motion: no-preference. So: (a) with JS off / for crawlers,
   html has no `js` class -> .reveal stays fully visible (SEO + no-JS safe);
   (b) reduced-motion users get instant, motionless content. `js` is set by
   main.js, and inline pre-paint in index.html so the home page's hand-authored
   .reveal sections never flash. main.js's IntersectionObserver adds .reveal-in.
   ~320ms, Linear-style ease-out — restrained, no overshoot. */
@media (prefers-reduced-motion: no-preference) {
  html.js .reveal {
    opacity: 0;
    transform: translateY(14px);
    transition: opacity 320ms ease-out,
                transform 320ms cubic-bezier(0.16, 1, 0.3, 1);
    will-change: opacity, transform;
  }

  html.js .reveal.reveal-in {
    opacity: 1;
    transform: none;
  }

  /* Per-item stagger: an element may set --reveal-index inline; the
     transition-delay scales with it (70ms steps). */
  html.js .reveal[style*="--reveal-index"] {
    transition-delay: calc(var(--reveal-index) * 70ms);
  }
}


/* -----------------------------------------------------------------------------
   16. Motion layer (Block 16) — site-wide, compositor-only, restrained.
       Animates opacity/transform only (never layout properties). Every effect
       is gated for prefers-reduced-motion. No new tokens, no dependencies.
   -------------------------------------------------------------------------- */

/* 16a. Smooth in-page scrolling + anchor offset. The site-header isn't sticky
   but the library toolbar is, so a small scroll-margin keeps anchored targets
   (e.g. chip-page "#files") clear of any sticky chrome. The reduced-motion
   block in section 3 already forces scroll-behavior: auto !important. */
@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}

:where([id]) { scroll-margin-top: var(--sp-8); }

/* 16b. Cross-page crossfade via native View Transitions — zero JS. Same-origin
   navigations only (GitHub Pages / localhost qualify); unsupported engines just
   navigate normally. Default root crossfade tuned short and calm. */
@view-transition { navigation: auto; }

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 110ms;
}

/* 16c. Fallback for engines without View Transitions (e.g. Firefox today): a
   brief fade-in on load. The @supports gate prevents double-animation on
   browsers where the View-Transition crossfade already runs. */
@media (prefers-reduced-motion: no-preference) {
  @supports not (view-transition-name: none) {
    @keyframes mfda-page-fade {
      from { opacity: 0; }
      to   { opacity: 1; }
    }
    body { animation: mfda-page-fade 200ms ease-out both; }
  }
}

/* 16d. Reduced-motion: neutralize View Transitions explicitly — this is NOT
   auto-handled by the prefers-reduced-motion media query. Reduced-motion users
   get instant, motionless navigation. */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

/* 16e. Library grid — one-time staggered card entrance on the first paint.
   library.js adds .lib-grid--enter for the initial render only (then removes it
   so filter/search re-renders don't re-animate). The nth-child stagger repeats
   every 4 (the widest column count) so the delay is capped (~135ms) and the
   last card is never laggy. fill-mode is `backwards`: the entrance never RETAINS
   a transform, so it cannot fight the Block-15 hover-lift once it finishes. */
@media (prefers-reduced-motion: no-preference) {
  @keyframes mfda-card-enter {
    from { opacity: 0; transform: translateY(14px); }
    to   { opacity: 1; transform: none; }
  }

  /* Direct-child selectors, deliberately. The stagger below counts with
     :nth-child, which is only correct when the cards are siblings directly
     inside the element carrying .lib-grid--enter. In sectioned mode library.js
     puts that class on each per-section .lib-grid, not on the #lib-grid
     container; the `>` makes that contract explicit so nesting a wrapper can't
     silently kill the animation again. */
  .lib-grid--enter > .lib-card {
    animation: mfda-card-enter 340ms cubic-bezier(0.16, 1, 0.3, 1) backwards;
  }

  .lib-grid--enter > .lib-card:nth-child(4n + 1) { animation-delay: 0ms; }
  .lib-grid--enter > .lib-card:nth-child(4n + 2) { animation-delay: 45ms; }
  .lib-grid--enter > .lib-card:nth-child(4n + 3) { animation-delay: 90ms; }
  .lib-grid--enter > .lib-card:nth-child(4n + 4) { animation-delay: 135ms; }
}


/* -----------------------------------------------------------------------------
   TODO (deferred from v0.1):
     - prefers-color-scheme: dark theme
     - @media print rules
     - .container size variants (narrow/wide) if a content page needs them
     - Hero parallax: evaluated in Block 16 and CUT for restraint (the navy hero
       already has mesh-drift + a scroll indicator; scroll-driven parallax on top
       read as busy and risked compositing conflicts).
   -------------------------------------------------------------------------- */


/* -----------------------------------------------------------------------------
   16. Two-layer disclosure — plain summary on top, technical detail underneath

   Every card and page reads plainly first. Nothing technical was deleted in
   that rewrite; it moved in here. Native <details> so it works with JavaScript
   off, which is also why the caveats that matter stay OUTSIDE this element —
   a warning a reader has to click to see is a warning you have hidden.
   -------------------------------------------------------------------------- */
.tech-details {
  margin-top: var(--sp-4);
  border-top: 1px solid var(--bg-soft);
  padding-top: var(--sp-2);
}

.tech-details > summary {
  cursor: pointer;
  font-family: var(--font-mono);
  font-size: var(--fs-300);
  letter-spacing: 0.02em;
  color: var(--fg-muted);
  padding: var(--sp-2) 0;
  list-style: none;
}

.tech-details > summary::-webkit-details-marker { display: none; }

/* Own marker, so the affordance survives the list-style reset above. */
.tech-details > summary::before {
  content: "\203A";           /* › */
  display: inline-block;
  width: 1em;
  color: var(--color-teal);
  transition: transform 0.15s ease;
}

.tech-details[open] > summary::before { transform: rotate(90deg); }
.tech-details > summary:hover { color: var(--color-teal); }

.tech-details > summary:focus-visible {
  outline: 2px solid var(--color-teal);
  outline-offset: 2px;
  border-radius: var(--radius);
}

.tech-details__body { padding-bottom: var(--sp-2); }
.tech-details__body > * + * { margin-top: var(--sp-3); }

/* On a dark section the hairline and muted text need the on-dark tokens. */
.cb-stage .tech-details { border-top-color: rgba(240, 247, 250, 0.16); }

.cb-stage .tech-details > summary { color: var(--fg-on-dark); }


/* The plain-language summary layer ------------------------------------------
   .plain-lede is the ~40-word answer to "what is this and what is it for".
   .plain-caveat is the honesty line that must never be moved into <details>. */
.plain-lede {
  font-family: var(--font-sans);
  font-size: var(--fs-500);
  line-height: 1.55;
  color: var(--fg-body);
  max-width: 62ch;
}

.plain-caveat {
  font-family: var(--font-sans);
  font-size: var(--fs-400);
  line-height: 1.5;
  color: var(--color-accent-ink);
  background: var(--bg-soft);
  border-radius: var(--radius);
  padding: var(--sp-2) var(--sp-3);
  margin-top: var(--sp-3);
  display: inline-block;
}

.plain-caveat strong { color: inherit; }

/* --------------------------------------------------------------------------
   Design review 2026-10-03, item 3: one heading scale, readable prose widths.
   A content page's title is larger than its section headings, and prose
   (not tables or tools) stops at a readable measure. The library, product and
   hardware pages carry the same rules in their own stylesheets, which load
   after this one.
   -------------------------------------------------------------------------- */
.page-header h1 {
  font-size: clamp(2rem, 1.6rem + 2vw, 2.75rem);
  line-height: 1.15;
  letter-spacing: -.015em;
}

.page-body > .container > h2 {
  font-size: clamp(1.5rem, 1.15rem + 1.3vw, 2rem);
  line-height: 1.2;
}

.page-body > .container > p,
.page-body > .container > ul { max-width: 68ch; }
.page-body > .container > ul:not([class]) { padding-left: 1.25rem; }
.page-body > .container > ul:not([class]) > li + li { margin-top: .75rem; }

/* --------------------------------------------------------------------------
   Design review 2026-10-03, item 7: the menu shows where you are. The link
   carrying aria-current="page" (the page's section) is bold and underlined,
   so it does not rest on colour alone. On a phone the menu button is a 44 px
   target with the icon kept small, the open menu is tighter, and the footer's
   resource links sit in two columns.
   -------------------------------------------------------------------------- */
.primary-nav a[aria-current="page"] {
  color: var(--color-deep-blue);
  font-weight: 700;
  text-decoration: underline;
  text-decoration-thickness: 3px;
  text-underline-offset: 8px;
}

.nav-toggle {
  width: 44px;
  height: 44px;
  padding: 10px;
  justify-content: center;
  align-items: center;
  gap: 6px;
}

.nav-toggle__bar { width: 24px; height: 2px; flex: 0 0 2px; }

.nav-toggle[aria-expanded="true"] .nav-toggle__bar:nth-child(1) {
  transform: translateY(8px) rotate(45deg);
}

.nav-toggle[aria-expanded="true"] .nav-toggle__bar:nth-child(3) {
  transform: translateY(-8px) rotate(-45deg);
}

@media (max-width: 47.999em) {
  .site-header__inner { padding-block: .75rem; }
  .primary-nav ul { gap: 0; padding-block: .5rem; }
  .primary-nav a { display: flex; align-items: center; min-height: 44px; padding: .5rem; }
  .footer-col:nth-child(3) .footer-links {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: .5rem 1rem;
  }
  .footer-col:nth-child(3) .footer-links a {
    display: flex;
    align-items: center;
    min-height: 44px;
  }
}

/* --------------------------------------------------------------------------
   Design review 2026-10-03, item 10: shared spacing and colour roles. White
   for content cards, ice for supporting panels, navy for dark sections, deep
   blue for the leading action in a light card (.btn-action). The spacing
   between the last section and the footer is set once, not twice. On the
   dark sections a disclosure's summary takes the light ink: on the build
   page's closing note it measured 2.34:1 and now measures 11:1.
   -------------------------------------------------------------------------- */
.page-header { padding-block: 3rem; }
.page-body { padding-block: 3rem; }
.page-body > .container > h2:not(:first-child) { margin-top: 2.5rem; }
.site-footer { margin-top: 0; }
.site-footer__grid { gap: 1.5rem; }
/* The cards' and controls' rules are in the stylesheets that own them
   (library.css, products.css, the channel maker's page). */

/* The leading action in a light card. */
.tool-card__foot a.btn-action {
  background: var(--color-deep-blue);
  color: var(--color-white);
  border-color: var(--color-deep-blue);
}

.tool-card__foot a.btn-action:hover,
.tool-card__foot a.btn-action:focus-visible {
  background: var(--color-navy);
  color: var(--color-white);
  border-color: var(--color-navy);
}

:is(.cb-stage, .cb-outro) .tech-details { border-top-color: rgba(240, 247, 250, 0.25); }
:is(.cb-stage, .cb-outro) .tech-details > summary { color: var(--fg-on-dark); }
:is(.cb-stage, .cb-outro) .tech-details > summary::before { color: currentColor; }
:is(.cb-stage, .cb-outro) .tech-details > summary:hover,
:is(.cb-stage, .cb-outro) .tech-details > summary:focus-visible {
  color: var(--color-white);
  outline-color: var(--color-accent);
}

@media (max-width: 40rem) {
  .page-header, .page-body { padding-block: 2rem; }
}
