/* ============================================================
   MagicBlocks component library — shared tokens + page chrome
   Imported by every /components/*.html page.
   Change tokens here; every page reflects the change.
   ============================================================ */

:root {
  /* ------------------------------------------------- colour */
  /* ink/foundation */
  --ink:         #191E32;
  --slate:       #466099;
  --lavender:    #6E90CC;
  --paper:       #FFFFFF;

  /* warm neutrals — the default surface family.
     These are PINNED values: they stay warm in both light and dark
     modes so the brand's cream-paper identity carries through.
     Any component that sets one of these as its own background must
     also scope its type tokens (--fg, --fg-soft, --fg-dim, --hair)
     to ink values so copy stays legible when the theme's --fg flips. */
  --warm-1:      #FBF6F0;  /* lightest — hairline tint on paper */
  --warm-2:      #F9F1E8;  /* light — marketing footer, soft stages */
  --warm-3:      #F4ECE4;  /* default — page background in light mode */
  --warm-5:      #E8DBCB;  /* mid — panel sunken, demo-stage alt */
  --warm-7:      #D2BFAA;  /* deep — ambient blocks, hero shadows */

  /* block quad — pink is hero */
  --pink-300:    #FE84A9;
  --pink-500:    #FF5B8D;
  --pink-700:    #FF3F7A;

  --yellow-300:  #FFD878;
  --yellow-500:  #F9C33E;
  --yellow-700:  #F9AD03;

  --green-300:   #7DF4D0;
  --green-500:   #47DDB2;
  --green-700:   #37BC9B;

  --blue-300:    #5BD9FC;
  --blue-500:    #30C4F2;
  --blue-700:    #1FAAE8;

  /* semantic (warm/light mode — default) */
  --bg:          var(--warm-3);
  --bg-paper:    var(--paper);
  --bg-sunk:     var(--warm-5);
  --bg-sunken:   var(--warm-5);           /* alias of --bg-sunk */
  --bg-warm:     var(--warm-2);            /* light warm tint for subtle surfaces */
  --bg-deep:     var(--warm-7);
  --fg:          var(--ink);
  /* --fg-soft was var(--slate) = #466099 (a strong blue) which made every
     body-secondary paragraph read as a link. Changed to ink at 78% alpha
     for a neutral ink-tinted body color (~6 ratio on white, passes AA-body
     comfortably). The --slate token is still available for explicit
     slate-blue use where intentional. */
  --fg-soft:     rgba(25, 30, 50, 0.78);
  /* --fg-dim was 0.55 (3.77 ratio on white = fail AA-body). Bumped to 0.7
     for ~5.6 ratio on white — passes AA-body 4.5+. Used for muted-but-
     readable labels (chapter eyebrow, input hints, stat labels, etc.). */
  --fg-dim:      rgba(25, 30, 50, 0.7);
  /* --fg-faint was 0.35 (~2 ratio = fail everything). Bumped to 0.6 for
     ~3.7 ratio on white — passes AA-large 3.0+. Used for decorative
     captions, dividers, section numbers — text that's intentionally
     quiet but still readable. */
  --fg-faint:    rgba(25, 30, 50, 0.6);
  --hair:        rgba(25, 30, 50, 0.12);
  --hair-soft:   rgba(25, 30, 50, 0.06);
  /* warm-tinted hairline variants — for panels where cool ink-tinted
     borders read slightly muddy on warm-cream ground. Opt-in only. */
  --hair-warm:      rgba(139, 115, 85, 0.18);
  --hair-warm-soft: rgba(139, 115, 85, 0.10);
  /* per-industry hero wash. Set this on a page root to colour-wash
     .hero-bloom-canvas backgrounds without touching the component. */
  --industry-accent: var(--accent);
  --accent:      var(--pink-700);
  --accent-soft: rgba(255, 63, 122, 0.12);
  --success:     var(--green-700);
  --success-soft: rgba(55, 188, 155, 0.14);
  --warning:     var(--yellow-700);
  --warning-soft: rgba(249, 173, 3, 0.16);
  --info:        var(--blue-700);
  --info-soft:   rgba(31, 170, 232, 0.14);
  --error:       #D64545;
  --error-soft:  rgba(214, 69, 69, 0.12);

  /* Darker semantic TEXT variants — for body/caption text on LIGHT
     backgrounds. The bright brand colors (--success, --info, --warning)
     fail WCAG AA when used as small text on cream / paper, so we
     pre-darken them here. In dark mode these alias back to the brand
     colors (which read fine on ink).
     ── EXCEPTION: --accent-text was #B42463 (burgundy) for AA reasons,
     but that hue read off-brand (close to a wine-red). Per brand
     direction we now alias --accent-text → --accent (pink-700), so the
     brand pink is used everywhere and the burgundy is removed from the
     kit entirely. Trade-off: small (<14 px) accent text may not strictly
     hit AA-body on white — brand consistency wins. */
  --accent-text:   var(--accent);
  --success-text:  #1E6B44;
  --info-text:     #1463A1;
  --warning-text:  #8A5A00;
  --error-text:    var(--error);  /* already #D64545 — passes AA-body on white */

  /* ambient brand glow — sits behind the whole page via body::before.
     Subtle in light mode, more pronounced in dark where the dark base
     makes the pink feel lit rather than washed. Shared so every page
     gets the same through-line without heroes needing bespoke gradients. */
  --glow-pink:   rgba(255, 63, 122, 0.06);
  --glow-blue:   rgba(31, 170, 232, 0.04);
  --glow-warm:   rgba(249, 195, 62, 0.03);

  /* neutral hash pattern for demo stages — theme-aware so the texture
     stays visible on both cream and ink backgrounds without overpowering. */
  --stage-hash:  rgba(25, 30, 50, 0.035);

  /* ------------------------------------------------- space */
  --s-1:   4px;
  --s-2:   8px;
  --s-3:   12px;
  --s-4:   16px;
  --s-5:   20px;
  --s-6:   24px;
  --s-7:   32px;
  --s-8:   40px;
  --s-9:   48px;
  --s-10:  64px;
  --s-11:  80px;
  --s-12:  96px;
  --s-13:  128px;

  /* ------------------------------------------------- radii */
  --r-xs:  4px;
  --r-sm:  6px;
  --r-md:  10px;
  --r-lg:  14px;
  --r-xl:  20px;
  --r-2xl: 28px;
  --r-pill: 999px;

  /* ------------------------------------------------- shadow / elevation */
  --sh-0: none;
  --sh-1: 0 1px 2px rgba(25, 30, 50, 0.06), 0 1px 1px rgba(25, 30, 50, 0.04);
  --sh-2: 0 4px 10px rgba(25, 30, 50, 0.08), 0 1px 2px rgba(25, 30, 50, 0.06);
  --sh-3: 0 10px 28px rgba(25, 30, 50, 0.12), 0 2px 4px rgba(25, 30, 50, 0.06);
  --sh-4: 0 24px 56px rgba(25, 30, 50, 0.18), 0 4px 8px rgba(25, 30, 50, 0.08);
  --sh-pink: 0 12px 40px rgba(255, 63, 122, 0.28);
  --sh-focus: 0 0 0 3px rgba(255, 63, 122, 0.28);

  /* ------------------------------------------------- motion */
  --dur-1: 100ms;
  --dur-2: 160ms;
  --dur-3: 240ms;
  --dur-4: 400ms;
  --ease:  cubic-bezier(0.2, 0.8, 0.2, 1);

  /* ------------------------------------------------- type */
  --f-display: "Bricolage Grotesque", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Inter, sans-serif;
  --f-body:    "DM Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Inter, sans-serif;
  --f-mono:    "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --f-serif:   "Fraunces", ui-serif, Georgia, "Times New Roman", serif;

  /* ------------------------------------------------- z-index */
  --z-base:    1;
  --z-sticky:  10;
  --z-overlay: 100;
  --z-modal:   200;
  --z-toast:   300;

  /* ============================================================
     App-pattern semantic aliases (CRM brief — DESIGN-SYSTEM-ADDITIONS.md)
     ------------------------------------------------------------
     These layer on top of existing tokens for the CRM-flavoured
     components in chapters 13–15 (and the small atoms in 07/08).
     All purely-derived from existing semantic colours — no new
     hex values introduced. Backward-compatible additions only.
     ============================================================ */

  /* hover wash for list rows (used by inbox row, kanban card hover, etc.) */
  --surface-list-row-hover: color-mix(in oklab, var(--bg-paper) 95%, var(--accent));

  /* stronger focus ring (3px, 30% accent) for primary interactive moments */
  --ring-accent-strong: 0 0 0 3px color-mix(in oklab, var(--accent) 30%, transparent);

  /* soft brand gradient — used by AI surfaces and the Sage drawer header band */
  --gradient-glow-soft: linear-gradient(135deg,
    color-mix(in oklab, var(--accent) 12%, transparent) 0%,
    color-mix(in oklab, var(--info)   10%, transparent) 100%);
  --ai-glow-bg: linear-gradient(135deg,
    color-mix(in oklab, var(--accent) 8%, transparent) 0%,
    color-mix(in oklab, var(--info)   6%, transparent) 100%);

  /* score / risk semantic aliases (drives Score ring, Health badge, SLA ring colour) */
  --score-low:     var(--success);
  --score-medium:  var(--warning);
  --score-high:    var(--error);

  /* badge tints — 12% colour over paper, used by risk badges, status pills */
  --badge-bg-success: color-mix(in oklab, var(--success) 12%, transparent);
  --badge-bg-warning: color-mix(in oklab, var(--warning) 12%, transparent);
  --badge-bg-danger:  color-mix(in oklab, var(--error)   12%, transparent);

  /* sparkline stroke — 1.2px in light, 1.5px in dark (per spec § 24) */
  --spark-stroke: 1.2px;

  /* density-mode row heights. Default = comfortable; the
     body[data-density="compact"] override below switches to compact.
     Components consume var(--row-h) so they automatically respond. */
  --row-h-comfortable: 52px;
  --row-h-compact:     36px;
  --row-h: var(--row-h-comfortable);
}

/* ink/dark mode — triggered by [data-theme="dark"] on <body> */
body[data-theme="dark"] {
  --bg:          var(--ink);
  --bg-paper:    #232842;
  --bg-sunk:     #141828;
  --bg-sunken:   #141828;
  --bg-warm:     #232842;                  /* dark-mode "warm" reads as raised paper */
  --bg-deep:     #0F1221;
  --fg:          #F4ECE4;
  --fg-soft:     #C9CFE2;
  --fg-dim:      rgba(244, 236, 228, 0.78);  /* was 0.7 — bumped for AA-body across all bgs */
  --fg-faint:    rgba(244, 236, 228, 0.62);  /* was 0.5 — bumped to clear AA-body 4.5+ on bg-paper */
  --hair:        rgba(244, 236, 228, 0.14);
  --hair-soft:   rgba(244, 236, 228, 0.07);
  --hair-warm:      rgba(210, 191, 170, 0.22);
  --hair-warm-soft: rgba(210, 191, 170, 0.12);
  --accent:      #FF4F87;  /* was #FF6E9D — pushed darker so white-on-accent
                              passes WCAG AA-large for chat bubbles, CTAs,
                              and pill chips. Pink-300 was too light. */
  --accent-soft: rgba(255, 63, 122, 0.24);
  --success:     var(--green-500);
  --success-soft: rgba(125, 244, 208, 0.16);
  --warning:     var(--yellow-500);
  --warning-soft: rgba(255, 216, 120, 0.18);
  --info:        var(--blue-500);
  --info-soft:   rgba(91, 217, 252, 0.16);
  --error:       #FF7A7A;
  --error-soft:  rgba(255, 120, 120, 0.16);

  /* In dark mode, the brand semantic colors are bright enough to read
     as text on the ink surface — alias the -text tokens back so the
     same rules work in both themes without per-mode overrides. */
  --accent-text:   var(--accent);
  --success-text:  var(--success);
  --info-text:     var(--info);
  --warning-text:  var(--warning);
  --error-text:    var(--error);

  /* dark mode glows — deeper and slightly more saturated so the brand
     warmth carries through the ink surface without feeling gauzy. */
  --glow-pink:   rgba(255, 110, 157, 0.14);
  --glow-blue:   rgba(91, 217, 252, 0.07);
  --glow-warm:   rgba(255, 216, 120, 0.05);

  /* hash pattern — lift to a light tint so the texture stays visible
     on ink without going full-contrast dots. */
  --stage-hash:  rgba(244, 236, 228, 0.04);

  /* App-pattern adjustments per spec § 24 (Dark-mode notes):
     • AI glow bg pumped ~1.5× so it reads at the same perceived brightness
       on the ink surface (light gradient on dark looks weaker than vice-versa)
     • Sparkline stroke +0.3px in dark to keep visual weight
     • Stronger ring uses the dark-mode --accent which is already shifted */
  --gradient-glow-soft: linear-gradient(135deg,
    color-mix(in oklab, var(--accent) 18%, transparent) 0%,
    color-mix(in oklab, var(--info)   15%, transparent) 100%);
  --ai-glow-bg: linear-gradient(135deg,
    color-mix(in oklab, var(--accent) 12%, transparent) 0%,
    color-mix(in oklab, var(--info)    9%, transparent) 100%);
  --spark-stroke: 1.5px;

  /* list-row hover — slightly more visible on the ink surface */
  --surface-list-row-hover: color-mix(in oklab, var(--bg-paper) 88%, var(--accent));

  /* badge tints — slightly stronger so the colour reads on dark paper */
  --badge-bg-success: color-mix(in oklab, var(--success) 18%, transparent);
  --badge-bg-warning: color-mix(in oklab, var(--warning) 18%, transparent);
  --badge-bg-danger:  color-mix(in oklab, var(--error)   18%, transparent);
}

/* ============================================================
   Density-mode override — opt-in via body[data-density="compact"].
   Components reference var(--row-h) for row/card heights so they
   automatically swap when this attribute flips. Toggle is demoed
   in 13-app-shell.html.
   ============================================================ */
body[data-density="compact"] {
  --row-h: var(--row-h-compact);
}

/* ============================================================ reset + base */

*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
body {
  margin: 0;
  font-family: var(--f-body);
  font-optical-sizing: auto;
  color: var(--fg);
  background: var(--bg);
  line-height: 1.55;
  position: relative;
  transition: background var(--dur-3) var(--ease), color var(--dur-3) var(--ease);
  /* Defensive: prevent rogue 1-2px horizontal overflow at the body level.
     Components that need horizontal scroll (tables, kanban, command palette
     row) opt-in via their own overflow-x: auto. The page itself never
     scrolls horizontally on mobile. */
  overflow-x: hidden;
}
/* ambient brand atmosphere — lives behind every page, never clips.
   Pink in the top-right, cool blue in the bottom-left, and a whisper
   of warm yellow through the middle. Fixed so it doesn't move on
   scroll and lights the whole document like a soft-box. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(60vw 55vh at 88% 8%,  var(--glow-pink), transparent 65%),
    radial-gradient(50vw 50vh at 8% 92%,  var(--glow-blue), transparent 65%),
    radial-gradient(70vw 50vh at 50% 50%, var(--glow-warm), transparent 70%);
  transition: opacity var(--dur-3) var(--ease);
}
/* every page-content root needs to sit above the glow */
body > * { position: relative; z-index: 1; }
img, svg { max-width: 100%; display: block; }
a { color: var(--fg); text-decoration-color: var(--hair); text-underline-offset: 3px; }
a:hover { color: var(--accent-text); text-decoration-color: currentColor; }

/* keyboard focus ring — visible on all focusable elements */
:focus-visible {
  outline: none;
  box-shadow: var(--sh-focus);
  border-radius: var(--r-xs);
}
a:focus-visible, button:focus-visible, [role="button"]:focus-visible,
input:focus-visible, select:focus-visible, textarea:focus-visible,
[tabindex]:focus-visible {
  outline: none;
  box-shadow: var(--sh-focus);
}

/* ============================================================ page chrome */

.page {
  max-width: 1160px;
  margin: 0 auto;
  padding: 0 var(--s-7);
}
@media (max-width: 640px) {
  .page { padding: 0 var(--s-4); }
}

/* top bar */
.topnav {
  position: sticky; top: 0; z-index: var(--z-sticky);
  background: color-mix(in oklab, var(--bg) 88%, transparent);
  backdrop-filter: saturate(140%) blur(12px);
  -webkit-backdrop-filter: saturate(140%) blur(12px);
  border-bottom: 1px solid var(--hair);
}
.topnav-inner {
  max-width: 1160px; margin: 0 auto;
  display: flex; align-items: center; gap: var(--s-6);
  padding: var(--s-4) var(--s-7);
}
@media (max-width: 640px) {
  .topnav-inner { gap: var(--s-3); padding: var(--s-3) var(--s-4); }
  .topnav .crumb { display: none; }
  .topnav .brand { font-size: 14px; }
}
.topnav .brand {
  display: inline-flex; align-items: center; gap: 10px;
  font-family: var(--f-display);
  font-weight: 700;
  font-size: 17px;
  letter-spacing: -0.02em;
  color: var(--fg);
  text-decoration: none;
  flex-shrink: 0;
}
.topnav .brand-mark {
  width: 26px; height: 26px; display: inline-block;
  background: url("../02-icon/svg/magicblocks-icon-color.svg") center / contain no-repeat;
  flex-shrink: 0;
}
.topnav .brand-word { font-family: var(--f-display); font-weight: 700; }
/* legacy .dot fallback — invisible if not used */
.topnav .brand .dot { display: none; }

/* "Components" subsection label, sits AFTER the brand to give orientation
   context. Visible breadcrumb-style separator + label. Tells the user:
   "you're in the Components section of the MagicBlocks brand kit." */
.topnav .brand-sep {
  color: var(--fg-faint);
  font: 400 18px/1 var(--f-display);
  margin: 0 4px;
  user-select: none;
}
.topnav .brand-section {
  font: 600 13px/1 var(--f-mono);
  text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--fg-soft);
  text-decoration: none;
  padding: 6px 12px;
  border-radius: var(--r-pill);
  border: 1px solid var(--hair);
  background: var(--bg-paper);
  transition: color var(--dur-2) var(--ease), border-color var(--dur-2) var(--ease);
  flex-shrink: 0;
}
.topnav .brand-section:hover {
  color: var(--fg);
  border-color: color-mix(in oklab, var(--accent) 40%, var(--hair));
}

.topnav .crumb {
  color: var(--fg-dim);
  font-size: 13px;
  display: flex; align-items: center; gap: var(--s-2);
}
.topnav .crumb .sep { color: var(--fg-faint); }
.topnav .spacer { flex: 1; }

/* ── Chapters mega-dropdown ────────────────────
   Mirror of the brand-kit-root pattern, but lists the 12 component
   chapters instead of brand-kit chapters. Wired by components/_shared.js
   (data-variant-tab handler is separate; this dropdown uses the
   nav-chapters-trigger handler in _shared.js). */
.site-menu {
  flex: 1;
  display: flex; align-items: center; gap: var(--s-3); justify-content: flex-end;
  font-family: var(--f-body); font-size: 13px;
  position: relative;
}
.nav-chapters { position: relative; }
.nav-chapters-trigger {
  appearance: none; -webkit-appearance: none;
  background: var(--bg-paper);
  border: 1px solid var(--hair);
  border-radius: var(--r-pill);
  padding: 7px 14px 7px 16px;
  font: 500 13px/1 var(--f-body);
  color: var(--fg);
  cursor: pointer;
  display: inline-flex; align-items: center; gap: 8px;
  transition: border-color var(--dur-2) var(--ease), background var(--dur-2) var(--ease);
}
.nav-chapters-trigger:hover { border-color: color-mix(in oklab, var(--accent) 30%, var(--hair)); }
.nav-chapters-trigger:focus-visible { outline: 0; box-shadow: var(--sh-focus); }
.nav-chapters-trigger .nav-chapters-num {
  font: 600 11px/1 var(--f-mono);
  color: var(--fg-dim);
  letter-spacing: 0.04em;
}
.nav-chapters-trigger .nav-chapters-caret {
  width: 10px; height: 10px; flex-shrink: 0;
  transition: transform var(--dur-2) var(--ease);
}
.nav-chapters.is-open .nav-chapters-trigger {
  background: var(--bg-sunk);
  border-color: var(--accent);
}
.nav-chapters.is-open .nav-chapters-caret { transform: rotate(180deg); }

.nav-chapters-panel {
  position: absolute;
  top: calc(100% + 10px); right: 0;
  /* Mega panel: 3-column grid for 12 chapters → fits all in one view */
  width: min(680px, calc(100vw - var(--s-7) * 2));
  background: var(--bg-paper);
  border: 1px solid var(--hair);
  border-radius: var(--r-lg);
  padding: var(--s-5);
  box-shadow: 0 24px 48px -20px color-mix(in oklab, var(--ink) 50%, transparent),
              0 8px 16px -8px color-mix(in oklab, var(--ink) 25%, transparent);
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--s-5) var(--s-4);
  opacity: 0; transform: translateY(-6px) scale(0.98);
  transform-origin: top right;
  pointer-events: none;
  transition: opacity 200ms var(--ease), transform 200ms var(--ease);
  z-index: 10;
}
.nav-chapters.is-open .nav-chapters-panel {
  opacity: 1; transform: translateY(0) scale(1);
  pointer-events: auto;
}
.nav-chapters-group { display: flex; flex-direction: column; gap: 2px; }
.nav-chapters-group .nav-chapters-eyebrow {
  font: 600 10px/1 var(--f-mono);
  text-transform: uppercase; letter-spacing: 0.12em;
  color: var(--fg-dim);
  padding: 0 10px var(--s-2);
}
.nav-chapters-panel a {
  display: grid; grid-template-columns: 28px 1fr; align-items: baseline;
  gap: 10px; padding: 8px 10px;
  border-radius: var(--r-sm);
  text-decoration: none;
  transition: background var(--dur-1) var(--ease);
}
.nav-chapters-panel a:hover { background: var(--bg-warm); }
.nav-chapters-panel a[aria-current="page"] { background: var(--accent-soft); }
.nav-chapters-panel a .nav-chapters-num {
  font: 600 11.5px/1 var(--f-mono);
  color: var(--fg-dim);
}
.nav-chapters-panel a[aria-current="page"] .nav-chapters-num { color: var(--accent-text); }
.nav-chapters-panel a .nav-chapters-name {
  font: 600 13.5px/1.2 var(--f-display);
  color: var(--fg);
  letter-spacing: -0.005em;
}
.nav-chapters-panel a[aria-current="page"] .nav-chapters-name { color: var(--accent-text); }
.nav-chapters-panel a .nav-chapters-sub {
  display: block;
  font: 400 11px/1.3 var(--f-body);
  color: var(--fg-soft);
  margin-top: 2px;
}

/* "← Brand kit" back link, sits subtly far-right of the panel */
.nav-back {
  font: 500 12.5px/1 var(--f-body);
  color: var(--fg-dim);
  text-decoration: none;
  padding: 6px 12px;
  border-radius: var(--r-pill);
  transition: color var(--dur-2) var(--ease), background var(--dur-2) var(--ease);
  flex-shrink: 0;
  display: inline-flex; align-items: center; gap: 6px;
}
.nav-back:hover { color: var(--fg); background: var(--bg-sunk); }

/* hamburger (mobile) — reuses the brand-kit pattern */
.nav-toggle {
  display: none;
  appearance: none; -webkit-appearance: none; background: transparent;
  border: 1px solid var(--hair); border-radius: var(--r-md);
  width: 36px; height: 36px; padding: 0;
  flex-direction: column; justify-content: center; align-items: center; gap: 4px;
  cursor: pointer; color: var(--fg);
  margin-left: auto;
}
.nav-toggle-bar {
  display: block; width: 16px; height: 1.75px; background: currentColor;
  border-radius: 1px;
  transition: transform var(--dur-2) var(--ease), opacity var(--dur-2) var(--ease);
}
.nav-toggle[aria-expanded="true"] .nav-toggle-bar:nth-child(1) { transform: translateY(5.75px) 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(-5.75px) rotate(-45deg); }

@media (max-width: 960px) {
  .topnav-inner { gap: var(--s-4); }
  .nav-toggle { display: inline-flex; }
  .topnav .brand-section, .topnav .brand-sep { display: none; }
  .site-menu {
    position: absolute; top: 100%; left: 0; right: 0;
    flex-direction: column; align-items: stretch; gap: var(--s-3);
    background: var(--bg);
    border-bottom: 1px solid var(--hair);
    padding: var(--s-5);
    transform: translateY(-8px); opacity: 0; pointer-events: none;
    transition: transform var(--dur-2) var(--ease), opacity var(--dur-2) var(--ease);
    box-shadow: 0 20px 40px -20px rgba(20, 25, 55, 0.18);
  }
  .site-menu.is-open { transform: translateY(0); opacity: 1; pointer-events: auto; }
  .nav-chapters { width: 100%; }
  .nav-chapters-trigger { width: 100%; justify-content: center; }
  .nav-chapters-panel {
    position: static; width: 100%; grid-template-columns: 1fr;
    box-shadow: none; border-color: var(--hair-soft);
    margin-top: var(--s-3);
    opacity: 1; transform: none; pointer-events: auto;
  }
  .nav-back { width: 100%; text-align: center; justify-content: center; padding: 10px 12px; border: 1px solid var(--hair-soft); border-radius: var(--r-md); }
}

/* light / dark toggle */
.theme-toggle {
  display: inline-flex; align-items: center; gap: 0;
  background: var(--bg-sunk);
  border: 1px solid var(--hair);
  border-radius: var(--r-pill);
  padding: 3px;
  font-family: var(--f-body);
  font-size: 12px;
  font-weight: 500;
}
.theme-toggle button {
  appearance: none; -webkit-appearance: none;
  background: transparent; border: none;
  color: var(--fg-dim);
  padding: 5px 12px;
  border-radius: var(--r-pill);
  cursor: pointer;
  display: inline-flex; align-items: center; gap: 5px;
  transition: color var(--dur-2) var(--ease), background var(--dur-2) var(--ease);
}
.theme-toggle button.is-active {
  background: var(--bg-paper);
  color: var(--fg);
  box-shadow: var(--sh-1);
}
.theme-toggle button svg { width: 13px; height: 13px; }

/* chapter header */
.chapter-head {
  padding: var(--s-11) 0 var(--s-9);
  border-bottom: 1px solid var(--hair);
}
.chapter-eyebrow {
  font-family: var(--f-mono);
  font-size: 12px;
  font-weight: 500;
  color: var(--fg-dim);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  margin: 0 0 var(--s-4);
}
.chapter-title {
  font-family: var(--f-display);
  font-weight: 700;
  font-size: clamp(36px, 5vw, 56px);
  line-height: 1.05;
  letter-spacing: -0.025em;
  margin: 0 0 var(--s-5);
  max-width: 22ch;
}
.chapter-title em {
  font-family: var(--f-serif);
  font-style: italic;
  font-weight: 400;
  /* Bright brand pink — display sizes (36–56px) easily clear AA-large at
     3:1, so we keep the on-brand --accent here. The darker --accent-text
     is reserved for body-sized text that needs the stricter 4.5:1 ratio. */
  color: var(--accent);
  font-variation-settings: "SOFT" 80;
}
.chapter-lede {
  font-size: 19px;
  line-height: 1.6;
  color: var(--fg-soft);
  max-width: 62ch;
  margin: 0 0 var(--s-6);
}

/* section divider (per-subsection) */
.section {
  padding: var(--s-11) 0;
  border-bottom: 1px solid var(--hair);
}
.section:last-of-type { border-bottom: 0; }
.section-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--s-6); margin-bottom: var(--s-6);
  flex-wrap: wrap;
}
.section-title {
  font-family: var(--f-display);
  font-weight: 700;
  font-size: 28px;
  letter-spacing: -0.01em;
  line-height: 1.2;
  margin: 0;
  display: flex; align-items: baseline; gap: var(--s-3);
}
.section-title .sn {
  font-family: var(--f-mono);
  font-size: 13px;
  color: var(--fg-faint);
  font-weight: 500;
}

/* section permalink — every .section gets a hash anchor next to its title.
   Shows on hover/focus; click copies the URL. JS adds the button; this
   styles it. */
.section-anchor {
  appearance: none;
  background: transparent;
  border: 0; padding: 0 var(--s-2);
  color: var(--fg-faint);
  font-family: var(--f-mono);
  font-size: 16px;
  cursor: pointer;
  opacity: 0;
  transition: opacity var(--dur-2) var(--ease), color var(--dur-2) var(--ease);
  margin-left: var(--s-2);
  border-radius: var(--r-xs);
}
.section-anchor:hover,
.section:hover .section-anchor,
.section-anchor:focus-visible {
  opacity: 1;
  color: var(--accent-text);
}
.section-anchor.is-copied {
  opacity: 1;
  color: var(--success-text);
}
.section-anchor::before { content: '#'; }
.section-anchor.is-copied::before { content: '✓ copied'; font-size: 11px; }
/* keep the ID target visually off the sticky topnav when scrolled to */
.section { scroll-margin-top: 80px; }
.section-desc {
  color: var(--fg-soft);
  font-size: 16px;
  max-width: 60ch;
  line-height: 1.65;
  margin: 0 0 var(--s-7);
}

/* element block: the standard component slab */
.el {
  margin-bottom: var(--s-9);
}
.el-head {
  display: flex; align-items: baseline; gap: var(--s-4);
  flex-wrap: wrap;
  margin-bottom: var(--s-2);
}
.el-name {
  font-family: var(--f-display);
  font-weight: 600;
  font-size: 18px;
  letter-spacing: -0.005em;
  margin: 0;
}
.el-tag {
  font-family: var(--f-mono);
  font-size: 11px;
  font-weight: 500;
  color: var(--fg-dim);
  padding: 2px 7px;
  background: var(--bg-sunk);
  border-radius: var(--r-sm);
  border: 1px solid var(--hair-soft);
}
.el-desc {
  font-size: 14.5px;
  line-height: 1.6;
  color: var(--fg-soft);
  margin: 0 0 var(--s-4);
  max-width: 68ch;
}
/* empty descriptions (generated in bulk) were adding ~40px of dead space
   above every demo — collapse them so spacing stays consistent. */
.el-desc:empty, .section-desc:empty { display: none; }

/* the render + code panel */
.demo {
  border: 1px solid var(--hair);
  border-radius: var(--r-lg);
  overflow: hidden;
  background: var(--bg-paper);
}
.demo-stage {
  padding: var(--s-7);
  display: flex; flex-wrap: wrap;
  gap: var(--s-5);
  align-items: flex-start;
  background:
    linear-gradient(var(--bg-paper), var(--bg-paper)),
    repeating-linear-gradient(
      45deg,
      transparent 0 6px,
      var(--stage-hash) 6px 7px
    );
  background-blend-mode: normal;
}
.demo-stage.center { justify-content: center; }
.demo-stage.vert { flex-direction: column; align-items: stretch; }
.demo-stage.plain { background: var(--bg-paper); }
/* .warm and .ink are deliberate surface variants — they pin to a specific
   tonal treatment regardless of theme so the component demo always reads
   the way the brand intends. In dark mode we nudge each one slightly so
   it stays visually separated from the surrounding page. */
.demo-stage.warm { background: var(--warm-3); color: var(--ink); }

/* ── warm-pinned surface scope ─────────────────────────────
   The warm cream tokens (--warm-1…7) DON'T flip in dark mode — by
   design, the brand's cream-paper identity carries through both
   themes. But that means any descendant reading var(--fg) on a
   warm-bg container gets cream-on-cream in dark mode (light text on
   sand bg = unreadable, e.g. .infra-map labels).
   Apply .mb-pin-warm — or list a known surface in the selector below —
   to scope --fg / --fg-soft / --fg-dim / --hair / --bg-paper back to
   ink-tinted values so every nested element stays legible. */
/* surfaces that genuinely OWN a warm bg — pinning ink-text inside them
   keeps copy legible in dark mode. Components without their own bg
   (e.g. .cost-compare which is just a layout) must NOT be in this list,
   or their text will pin to ink and disappear on the dark page bg. */
.mb-pin-warm,
.infra-map,
.hs-screen,
.hld-app,
.sc-screen,
.hero-bloom-canvas[data-variant="warm"],
.journey-map,
.roi-calc .roi-inputs,
.demo-stage.warm,
.cc-col.generic,
/* Scoreboard column-header cells own a pinned warm-3 bg (see 11-narrative-systems
   .scoreboard .sb-head > *). Rescoping the whole .scoreboard would break the
   .sb-row .good cells (which read var(--fg) and need cream in dark mode), so
   the rescope is tightened to just the head cells. .scoreboard.dark variant
   sets its own ink bg + paper text and is excluded. */
.scoreboard:not(.dark) .sb-head > * {
  --fg:        #191E32;
  --fg-soft:   #466099;
  --fg-dim:    rgba(25, 30, 50, 0.62);
  --fg-faint:  rgba(25, 30, 50, 0.45);
  --hair:      rgba(25, 30, 50, 0.12);
  --hair-soft: rgba(25, 30, 50, 0.06);
  --bg-paper:  #FFFFFF;
  --bg-sunk:   #E8DBCB;
  --bg-warm:   #F9F1E8;
}
.demo-stage.ink {
  background: var(--ink); color: var(--warm-3);
  --fg: var(--warm-3); --fg-soft: #C9CFE2; --hair: rgba(244,236,228,.14);
}
body[data-theme="dark"] .demo-stage.ink {
  /* lift off the page bg (also ink) with a subtle highlight ring */
  background: #1F2338;
  box-shadow: inset 0 0 0 1px rgba(244, 236, 228, 0.06);
}
body[data-theme="dark"] .demo-stage.warm {
  /* slightly richer warm so it feels intentional against the ink page */
  background: #E6D6C3;
}

/* variant tabs — for components with multiple modes/variants that
   each need to be seen at full size. Sits ABOVE the demo stage; the
   active variant's panel renders below. CSS-only swap based on
   .is-active class on tab + matching .is-active on .variant-panel.
   Wired in components/_shared.js (clicks on [data-variant-tab]). */
.variant-tabs {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 4px;
  padding: 4px;
  background: var(--bg-sunk);
  border: 1px solid var(--hair);
  border-radius: var(--r-pill);
  margin-bottom: var(--s-4);
  font-family: var(--f-mono);
  font-size: 12px;
}
.variant-tabs button {
  appearance: none; background: transparent; border: 0;
  padding: 7px 14px;
  border-radius: var(--r-pill);
  color: var(--fg-dim);
  cursor: pointer;
  font: 500 11.5px/1 var(--f-mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  transition: color 160ms var(--ease), background 160ms var(--ease);
  display: inline-flex; align-items: center; gap: 6px;
}
.variant-tabs button:hover { color: var(--fg); }
.variant-tabs button.is-active {
  background: var(--accent);
  color: var(--paper);
  box-shadow: 0 4px 12px -4px color-mix(in oklab, var(--accent) 50%, transparent);
}
.variant-tabs button:focus-visible { outline: 0; box-shadow: var(--sh-focus); }
.variant-tabs .vt-num {
  font: 700 10.5px/1 var(--f-mono);
  opacity: 0.7;
}
.variant-tabs button.is-active .vt-num { opacity: 0.85; }

/* the panel each tab reveals */
.variant-panel { display: none; }
.variant-panel.is-active { display: block; animation: vp-fade 320ms var(--ease); }
@keyframes vp-fade {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .variant-panel.is-active { animation: none; }
}

/* code tabs */
.demo-tabs {
  border-top: 1px solid var(--hair);
  display: flex; align-items: stretch;
  background: var(--bg-sunk);
  gap: 0;
  font-family: var(--f-mono); font-size: 12px;
}
.demo-tabs button {
  appearance: none; background: transparent; border: 0;
  padding: var(--s-3) var(--s-5);
  color: var(--fg-dim); cursor: pointer;
  display: inline-flex; align-items: center; gap: var(--s-2);
  border-right: 1px solid var(--hair);
  font-family: inherit; font-size: inherit;
}
.demo-tabs button.is-active { color: var(--fg); background: var(--bg-paper); }
.demo-tabs .spacer { flex: 1; }
.demo-tabs .copy {
  border-left: 1px solid var(--hair); border-right: 0;
  padding: 0 var(--s-5);
  color: var(--fg-dim);
}
.demo-tabs .copy:hover { color: var(--accent-text); }
.demo-tabs .copy.is-copied { color: var(--success-text); }

.demo-code {
  display: none;
  margin: 0;
  padding: var(--s-5) var(--s-6);
  background: var(--ink);
  color: #F4ECE4;
  font-family: var(--f-mono);
  font-size: 12.5px;
  line-height: 1.65;
  /* overflow-x: auto only works when width is constrained — pair with
     max-width:100% + min-width:0 so long lines scroll instead of widening
     the parent grid track. */
  max-width: 100%;
  min-width: 0;
  overflow-x: auto;
  white-space: pre;
  tab-size: 2;
  -moz-tab-size: 2;
}
.demo-code.is-active { display: block; }
.demo-code code { display: block; min-width: 0; }
/* tiny syntax hints inside .demo-code */
.demo-code .t { color: #FF9AB8; }  /* tags */
.demo-code .a { color: #FFD878; }  /* attr */
.demo-code .s { color: #7DF4D0; }  /* strings */
.demo-code .c { color: rgba(244,236,228,0.4); font-style: italic; } /* comment */
.demo-code .p { color: #5BD9FC; }  /* property */

/* anatomy callouts */
.anatomy {
  position: relative;
  border: 1px dashed var(--hair);
  border-radius: var(--r-lg);
  padding: var(--s-10) var(--s-10);
  margin-top: var(--s-4);
  background:
    linear-gradient(var(--bg-paper), var(--bg-paper)) padding-box,
    linear-gradient(135deg, rgba(255,63,122,0.3), rgba(31,170,232,0.3)) border-box;
  border: 1px dashed transparent;
}
.anatomy-legend {
  list-style: none; padding: 0; margin: var(--s-6) 0 0;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: var(--s-3) var(--s-5);
  font-family: var(--f-mono); font-size: 12px;
  color: var(--fg-soft);
}
.anatomy-legend li {
  display: flex; gap: var(--s-3); align-items: baseline;
}
.anatomy-legend .n {
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; flex: 0 0 20px;
  border-radius: 50%;
  background: var(--accent); color: #fff;
  font-weight: 600; font-size: 10px;
}
.anatomy-chip {
  position: absolute;
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px;
  border-radius: 50%;
  background: var(--accent); color: #fff;
  font-family: var(--f-mono); font-weight: 600; font-size: 10px;
  box-shadow: 0 0 0 3px var(--bg-paper);
  pointer-events: none;
  z-index: 2;
}
/* Inline numbered pin — sits adjacent to a feature instead of floating
   over it. Use on tables / cards where absolute positioning would drift
   when the parent reflows. Preserves the same visual language as
   .anatomy-chip and .anatomy-legend .n. */
.n-pin {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px;
  border-radius: 50%;
  background: var(--accent); color: #fff;
  font: 600 10px/1 var(--f-mono);
  box-shadow: 0 0 0 2px var(--bg-paper);
  flex-shrink: 0;
  margin-right: 8px;
  vertical-align: middle;
}
.n-pin-tight { margin-right: 4px; }
/* Right-aligned cells get the pin to the LEFT of the value so numbers
   still right-align with their sibling rows. */
.tbl-right .n-pin { margin-right: 6px; }

/* ============================================================ utilities */

.row { display: flex; gap: var(--s-4); flex-wrap: wrap; align-items: center; }
.grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--s-6); }
.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s-6); }
.grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--s-5); }
.grid-auto { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: var(--s-5); }
@media (max-width: 720px) {
  .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; }
}

.mono  { font-family: var(--f-mono); }
.small { font-size: 12px; }

/* ── avatars (shared atom) ─────────────────────────────────
   Originally defined only in 07-data-display.html, but used in
   content blocks (09), page templates (10), and anywhere a byline,
   presenter bug, or team card appears. Lifted here so every page
   inherits the base shape without having to re-declare it. */
.av {
  width: 32px; height: 32px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font: 600 12px/1 var(--f-display); letter-spacing: 0.02em;
  flex-shrink: 0;
  overflow: hidden;                           /* clip image children cleanly */
}
.av img { width: 100%; height: 100%; object-fit: cover; }
.av-xs { width: 20px; height: 20px; font-size: 9px; }
.av-sm { width: 26px; height: 26px; font-size: 10.5px; }
.av-lg { width: 40px; height: 40px; font-size: 14px; }
.av-xl { width: 56px; height: 56px; font-size: 19px; }
.av-ring { box-shadow: 0 0 0 2px var(--bg-paper), 0 0 0 4px var(--accent); }
.av-row { display: flex; align-items: center; gap: var(--s-3); }
.av-stack { display: inline-flex; }
.av-stack .av { margin-left: -8px; box-shadow: 0 0 0 2px var(--bg-paper); }
.av-stack .av:first-child { margin-left: 0; }
.av-more {
  background: var(--bg-sunken) !important; color: var(--fg-soft) !important;
  font-family: var(--f-mono); font-weight: 600;
}

/* ──────────────────────────────────────────────────────────────
   Shared primitives — forms, buttons, empty states
   ──────────────────────────────────────────────────────────────
   Previously these lived inside individual chapter files (forms in 04,
   empties in 07, buttons in 03). That fragmented approach meant when
   chapter 10 referenced `.input` or `.empty-ic`, the selectors had no
   style — the settings shell dropped to native-browser chrome, the
   empty-state SVGs rendered as unstyled rectangles. Lifting these
   primitives into _shared.css means every chapter inherits them
   consistently, the way the brand-kit is supposed to work. Chapter-
   specific variants can still live in the individual chapter <style>
   blocks and will compose cleanly on top. ────────────────────── */

/* Buttons — full family. Canonical definitions promoted from chapter 3
   so every chapter picks up the same transitions, padding, and variant
   suite. Includes primary (pink CTA), secondary (outlined paper), ghost,
   danger (filled), danger-outline, size modifiers (sm / default / lg /
   xl), and composition helpers (.btn-link, .btn-group, .btn-split). */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--s-2);
  font: 600 14.5px/1 var(--f-display);
  letter-spacing: -0.005em;
  padding: 11px var(--s-5);
  border: 1px solid transparent;
  border-radius: var(--r-md);
  cursor: pointer;
  text-decoration: none;
  transition: background var(--dur-2) var(--ease),
              border-color var(--dur-2) var(--ease),
              transform var(--dur-2) var(--ease),
              box-shadow var(--dur-2) var(--ease),
              color var(--dur-2) var(--ease);
  user-select: none;
  white-space: nowrap;
  appearance: none; -webkit-appearance: none;
}
.btn:focus-visible { outline: 0; box-shadow: var(--sh-focus); }
.btn:disabled, .btn[aria-disabled="true"] { opacity: .55; cursor: not-allowed; pointer-events: none; }

.btn-primary {
  background: var(--accent); color: var(--paper);
  box-shadow: var(--sh-pink);
}
.btn-primary:hover   { transform: translateY(-1px); filter: brightness(1.04); }
.btn-primary:active  { transform: translateY(0); filter: brightness(0.96); }

.btn-secondary {
  background: var(--bg-paper);
  color: var(--fg);
  border-color: var(--hair);
}
.btn-secondary:hover {
  border-color: var(--fg);
  background: var(--bg-paper);
  transform: translateY(-1px);
}
.btn-secondary:active { transform: translateY(0); }

.btn-ghost {
  background: transparent;
  color: var(--fg);
  border-color: transparent;
}
.btn-ghost:hover {
  background: var(--bg-sunk);
  border-color: var(--hair);
}

.btn-danger { background: var(--error); color: var(--paper); }
.btn-danger:hover { filter: brightness(1.05); transform: translateY(-1px); }

.btn-danger-outline {
  background: transparent; color: var(--error-text);
  border-color: rgba(214, 69, 69, 0.3);
}
.btn-danger-outline:hover {
  background: var(--error-soft);
  border-color: var(--error);
}

/* Sizes — the bare `.btn` line is deliberately re-declared to reset
   padding/font if a preceding modifier set them (e.g. inside .btn-group). */
.btn-sm { padding: 7px var(--s-4); font-size: 13px; border-radius: var(--r-sm); }
.btn    { padding: 11px var(--s-5); font-size: 14.5px; }                          /* default */
.btn-lg { padding: 13px var(--s-6); font-size: 15.5px; }
.btn-xl {
  padding: 16px var(--s-7);
  font-size: 17px;
  border-radius: var(--r-lg);
  gap: var(--s-3);
}

/* Icon-only buttons — square, uses the neutral ghost-style body. */
.icon-btn {
  width: 36px; height: 36px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--bg-paper);
  border: 1px solid var(--hair);
  border-radius: var(--r-md);
  color: var(--fg);
  cursor: pointer;
  transition: background var(--dur-2) var(--ease),
              border-color var(--dur-2) var(--ease),
              color var(--dur-2) var(--ease);
}
.icon-btn:hover { border-color: var(--fg); color: var(--fg); }
.icon-btn:focus-visible { outline: 0; box-shadow: var(--sh-focus); }
.icon-btn-primary {
  background: var(--accent); color: var(--paper);
  border-color: transparent;
}
.icon-btn-primary:hover { filter: brightness(1.05); color: var(--paper); }

/* Text-link variant styled like a button — underline tail + arrow shift. */
.btn-link {
  display: inline-flex; align-items: center; gap: var(--s-2);
  color: var(--fg);
  font: 600 14.5px/1 var(--f-display);
  text-decoration: none;
  padding: 2px 0;
  border-bottom: 1px solid var(--hair);
  transition: color var(--dur-2) var(--ease),
              border-color var(--dur-2) var(--ease),
              transform var(--dur-2) var(--ease);
}
.btn-link svg { transition: transform var(--dur-2) var(--ease); }
/* Use the bright brand --accent (pink-700) instead of the AA-darkened
   --accent-text. The link button is 14.5px/600 weight → falls under
   WCAG AA-large (3:1) which the bright accent passes (~4:1 on paper),
   so we can keep brand pop without the wine/burgundy cast. */
.btn-link:hover { color: var(--accent); border-color: var(--accent); }
.btn-link:hover svg { transform: translateX(3px); }
.btn-link-accent { color: var(--accent); border-color: var(--accent-soft); }
.btn-link-accent:hover { border-color: var(--accent); }

/* Button groups + split buttons */
.btn-group { display: inline-flex; }
.btn-group .btn {
  border-radius: 0;
  border-right-width: 0;
}
.btn-group .btn:first-child { border-top-left-radius: var(--r-md); border-bottom-left-radius: var(--r-md); }
.btn-group .btn:last-child  { border-top-right-radius: var(--r-md); border-bottom-right-radius: var(--r-md); border-right-width: 1px; }
.btn-group .btn:hover       { z-index: 1; position: relative; }

.btn-split { display: inline-flex; }
.btn-split-main { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.btn-split-icon {
  padding: 11px var(--s-3);
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
  border-left: 1px solid rgba(255,255,255,0.25);
}

.btn-loading { pointer-events: none; }

/* Inputs — textual + select. The select variant adds a brand-aware chevron. */
.input {
  display: block; width: 100%;
  font: 400 14.5px/1.4 var(--f-body);
  color: var(--fg);
  background: var(--bg-paper);
  border: 1px solid var(--hair);
  border-radius: var(--r-sm);
  padding: 10px 14px;
  transition: border-color var(--dur-2) var(--ease),
              box-shadow var(--dur-2) var(--ease);
  appearance: none; -webkit-appearance: none; -moz-appearance: none;
}
.input::placeholder { color: var(--fg-faint); }
.input:hover:not(:disabled):not(:focus) { border-color: var(--fg-dim); }
.input:focus { outline: 0; border-color: var(--accent); box-shadow: var(--sh-focus); }
/* warm-3 (lightest warm) for the disabled wash — warm-5/bg-sunk reads
   as "dirty/heavy" against white surfaces. Dark mode flips back. */
.input:disabled { background: var(--warm-3); color: var(--fg-dim); cursor: not-allowed; }
body[data-theme="dark"] .input:disabled { background: var(--bg-sunk); }
textarea.input { min-height: 88px; resize: vertical; line-height: 1.5; }
select.input,
.input[type="date"],
.input[type="time"] {
  padding-right: 40px;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23466099' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'><path d='m5 8 5 5 5-5'/></svg>");
  background-repeat: no-repeat;
  background-position: right 12px center;
  background-size: 16px 16px;
  cursor: pointer;
}
body[data-theme="dark"] select.input,
body[data-theme="dark"] .input[type="date"],
body[data-theme="dark"] .input[type="time"] {
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23C9CFE2' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'><path d='m5 8 5 5 5-5'/></svg>");
}

/* Input composition ecosystem — label / hint / error / success / group /
   affix / prefix / suffix / meta / counter. Lifted from chapter 4 so
   every chapter can build labelled-and-validated form rows consistently. */
.input-wrap { display: flex; flex-direction: column; gap: 6px; }
.input-label { font: 500 13px/1.3 var(--f-body); color: var(--fg); }
.input-hint  { font: 400 12.5px/1.4 var(--f-body); color: var(--fg-dim); }
.input-error   { font: 500 12.5px/1.4 var(--f-body); color: var(--error-text);   display: inline-flex; align-items: center; gap: 4px; }
.input-success { font: 500 12.5px/1.4 var(--f-body); color: var(--success-text); display: inline-flex; align-items: center; gap: 4px; }

.input-wrap.is-error .input         { border-color: var(--error); }
.input-wrap.is-error .input:focus   { box-shadow: 0 0 0 3px var(--error-soft); }
.input-wrap.is-success .input       { border-color: var(--success); }

/* Group — lets leading / trailing icons or prefixes/suffixes attach flush. */
.input-group {
  position: relative;
  display: flex;
  background: var(--bg-paper);
  border: 1px solid var(--hair);
  border-radius: var(--r-sm);
  transition: border-color var(--dur-2) var(--ease),
              box-shadow   var(--dur-2) var(--ease);
}
.input-group:focus-within {
  border-color: var(--accent);
  box-shadow: var(--sh-focus);
}
.input-group .input { border: 0; background: transparent; }
.input-group .input:focus { box-shadow: none; }

.input-affix {
  position: absolute; top: 0; bottom: 0;
  display: inline-flex; align-items: center; justify-content: center;
  width: 38px; color: var(--fg-dim);
  pointer-events: none;
}
.input-group .input-affix:first-child { left: 0; }
.input-group .input-affix:last-child  { right: 0; }
.input-affix-btn { pointer-events: auto; background: transparent; border: 0; cursor: pointer; border-radius: var(--r-xs); }
.input-affix-btn:hover { color: var(--fg); }

.input.has-leading  { padding-left: 38px; }
.input.has-trailing { padding-right: 38px; }

.input-prefix,
.input-suffix {
  display: inline-flex; align-items: center;
  padding: 0 12px;
  font: 400 14px/1 var(--f-mono);
  color: var(--fg-dim);
  /* warm-3 (lightest warm) for the affix wash — warm-5/bg-sunk reads
     as "dirty/heavy" against the white input surface. Dark mode flips. */
  background: var(--warm-3);
  border-right: 1px solid var(--hair);
}
body[data-theme="dark"] .input-prefix,
body[data-theme="dark"] .input-suffix { background: var(--bg-sunk); }
.input-suffix { border-right: 0; border-left: 1px solid var(--hair); }
.input.has-prefix { padding-left: 12px; }

.input-meta {
  display: flex; justify-content: space-between; align-items: center;
  gap: var(--s-4);
}
.input-counter { font-size: 12px; color: var(--fg-dim); }

/* Empty states — .empty wrapper with icon + title + lede + actions.
   Originally in 07-data-display.html; lifted so chapter 10's empty-
   templates page and anywhere else can use the same primitive. */
.empty {
  display: flex; flex-direction: column; align-items: center;
  text-align: center;
  padding: var(--s-9) var(--s-5);
  background: var(--bg-paper);
  border: 1px dashed var(--hair);
  border-radius: var(--r-lg);
  width: 100%;                       /* fill the grid slot / column */
  max-width: min(640px, 100%);       /* cap on very wide single-column pages */
  margin: 0 auto;
}
.empty-ic {
  width: 72px; height: 72px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--warm-3);
  color: color-mix(in oklab, var(--ink) 55%, transparent);
  margin-bottom: var(--s-4);
  flex-shrink: 0;
}
body[data-theme="dark"] .empty-ic {
  background: var(--bg-sunk);
  color: color-mix(in oklab, var(--fg) 70%, transparent);
}
.empty-title {
  font: 600 20px/1.2 var(--f-display); letter-spacing: -0.01em;
  margin: 0 0 6px; color: var(--fg);
}
.empty-lede {
  font: 400 14.5px/1.55 var(--f-body); color: var(--fg-soft);
  margin: 0 0 var(--s-5); max-width: 36ch;
}
.empty-actions {
  display: flex; gap: var(--s-4); align-items: center;
  flex-wrap: wrap; justify-content: center;
}
.empty-btn {
  background: var(--accent); color: var(--paper);
  font: 600 13.5px/1 var(--f-display);
  padding: 10px var(--s-4); border-radius: var(--r-md);
  text-decoration: none; box-shadow: var(--sh-pink);
  transition: transform var(--dur-2) var(--ease);
  display: inline-flex; align-items: center; gap: var(--s-2);
  border: 0; cursor: pointer;
}
.empty-btn:hover { transform: translateY(-1px); color: var(--paper); }
.empty-ghost {
  background: transparent; border: 1px solid var(--hair);
  color: var(--fg); font: 600 13px/1 var(--f-display);
  padding: 10px 16px; border-radius: var(--r-md); cursor: pointer;
}
.empty-ghost:hover { background: var(--bg-warm); }
.empty-link {
  color: var(--fg-soft); font: 500 13.5px/1 var(--f-body);
  text-decoration: none; display: inline-flex; align-items: center; gap: 4px;
}
.empty-link:hover { color: var(--accent-text); }

/* chapter TOC (side nav on every page) */
.toc {
  position: sticky; top: 72px;
  font-family: var(--f-mono);
  font-size: 12px;
  line-height: 1.8;
  padding: var(--s-5) 0;
}
.toc h4 {
  font-family: var(--f-body);
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--fg-faint);
  margin: 0 0 var(--s-3);
}
.toc a {
  display: block; color: var(--fg-soft);
  text-decoration: none; padding: 2px 0;
  border-left: 2px solid transparent;
  padding-left: var(--s-3);
  margin-left: calc(-1 * var(--s-3));
}
.toc a:hover { color: var(--accent-text); }
.toc a.is-active { color: var(--fg); border-left-color: var(--accent); }

/* layout: two-col with toc on left.
   NOTE: minmax(0, 1fr) (not just 1fr) on the content column is critical —
   grid tracks default to minmax(auto, 1fr), which lets intrinsic content
   (wide <pre> blocks inside .demo-code) push the column past its share
   and break the page width. min-width:0 on .content is belt-and-braces. */
.layout {
  display: grid;
  grid-template-columns: 200px minmax(0, 1fr);
  gap: var(--s-9);
  padding: var(--s-6) 0 var(--s-13);
}
.layout > .content { min-width: 0; }
@media (max-width: 960px) {
  .layout { grid-template-columns: minmax(0, 1fr); gap: 0; }
  .toc { display: none; }
}

/* prev/next chapter footer */
.page-nav {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: var(--s-5);
  margin: var(--s-11) 0 var(--s-13);
}
.page-nav a {
  display: block; padding: var(--s-6);
  border: 1px solid var(--hair); border-radius: var(--r-lg);
  background: var(--bg-paper);
  text-decoration: none; color: var(--fg);
  transition: border-color var(--dur-2) var(--ease), transform var(--dur-2) var(--ease);
}
.page-nav a:hover { border-color: var(--accent); transform: translateY(-1px); }
.page-nav .dir {
  font-family: var(--f-mono); font-size: 11px; text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--fg-faint); margin-bottom: 6px;
}
.page-nav .title {
  font-family: var(--f-display); font-weight: 600; font-size: 18px;
}
.page-nav .next { text-align: right; }
@media (max-width: 720px) {
  .page-nav { grid-template-columns: 1fr; }
  .page-nav .next { text-align: left; }
}

/* footer */
.foot {
  padding: var(--s-9) 0;
  border-top: 1px solid var(--hair);
  color: var(--fg-dim);
  font-size: 13px;
  display: flex; justify-content: space-between; gap: var(--s-6);
  flex-wrap: wrap;
}
.foot .mono { font-size: 12px; }

/* ============================================================ motion primitives
   Shared reveal + svg-draw used by chapter 11 narrative systems (and anywhere
   else that wants a "when in view" transition). Honours prefers-reduced-motion
   — _shared.js short-circuits and marks every .reveal as is-visible immediately
   when the user prefers less motion. */

.reveal {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 480ms var(--ease), transform 480ms var(--ease);
  transition-delay: calc(var(--i, 0) * 60ms);
  will-change: opacity, transform;
}
.reveal.is-visible { opacity: 1; transform: none; }

/* SVG line draw — pair with .reveal so the draw plays on intersection */
.svg-draw path,
.svg-draw .draw {
  stroke-dasharray: var(--draw-len, 1200);
  stroke-dashoffset: var(--draw-len, 1200);
  transition: stroke-dashoffset 1200ms ease-out;
  transition-delay: calc(var(--i, 0) * 60ms);
}
.svg-draw.is-visible path,
.svg-draw.is-visible .draw { stroke-dashoffset: 0; }

@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; transition: none; }
  .svg-draw path, .svg-draw .draw { stroke-dasharray: none; stroke-dashoffset: 0; transition: none; }
}

/* ============================================================
   Floating page utilities — back-to-top + section nav
   Auto-injected by _shared.js on any chapter page with 3+ sections.
   Lives on the left edge so it never sits over reading content;
   the popover opens upward + to the right of the pill. */
.mb-float {
  position: fixed; left: 20px; bottom: 20px; z-index: 70;
  display: flex; flex-direction: column; align-items: flex-start; gap: 10px;
  pointer-events: none;
  font-family: var(--f-body, system-ui);
}
.mb-float button,
.mb-float a {
  pointer-events: auto;
  appearance: none; -webkit-appearance: none;
  font: 600 11.5px/1 var(--f-mono, ui-monospace, "JetBrains Mono", monospace);
  letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--fg);
  background: var(--bg-paper);
  border: 1px solid var(--hair);
  border-radius: 999px;
  padding: 10px 14px;
  display: inline-flex; align-items: center; gap: 6px;
  box-shadow: 0 12px 28px -16px rgba(25, 30, 50, 0.45),
              0 4px 10px -4px rgba(25, 30, 50, 0.25);
  cursor: pointer;
  text-decoration: none;
  transition: transform 200ms cubic-bezier(0.22, 1, 0.36, 1),
              opacity 200ms ease,
              background 160ms ease,
              border-color 160ms ease,
              color 160ms ease;
  opacity: 0; transform: translateY(8px) scale(0.96);
}
.mb-float.is-visible button,
.mb-float.is-visible a {
  opacity: 1; transform: translateY(0) scale(1);
}
.mb-float button:hover,
.mb-float a:hover {
  background: var(--accent);
  color: var(--paper);
  border-color: transparent;
  transform: translateY(-1px);
}
.mb-float .mb-float-top svg,
.mb-float .mb-float-toc svg {
  width: 14px; height: 14px;
  stroke: currentColor; fill: none;
  stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
}

.mb-float-pop {
  pointer-events: auto;
  position: absolute; left: 0; bottom: 100%;
  margin-bottom: 10px;
  width: min(280px, calc(100vw - 40px));
  max-height: 60vh;
  overflow-y: auto;
  background: var(--bg-paper);
  border: 1px solid var(--hair);
  border-radius: 14px;
  box-shadow: 0 24px 48px -20px rgba(25, 30, 50, 0.50),
              0 8px 16px -8px rgba(25, 30, 50, 0.25);
  padding: 8px;
  opacity: 0; transform: translateY(8px) scale(0.96);
  transform-origin: bottom left;
  pointer-events: none;
  transition: opacity 180ms ease, transform 180ms cubic-bezier(0.22, 1, 0.36, 1);
}
.mb-float.is-open .mb-float-pop {
  opacity: 1; transform: translateY(0) scale(1);
  pointer-events: auto;
}
.mb-float-pop .mb-float-pop-title {
  font: 600 10.5px/1 var(--f-mono, ui-monospace, "JetBrains Mono", monospace);
  letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--fg-dim);
  padding: 8px 10px 6px;
}
.mb-float-pop a {
  display: flex; align-items: baseline; gap: 8px;
  padding: 8px 10px;
  border-radius: 8px;
  font: 500 13px/1.35 var(--f-body, system-ui);
  color: var(--fg-soft);
  text-transform: none;
  letter-spacing: normal;
  border: 0;
  background: transparent;
  box-shadow: none;
  opacity: 1;
  transform: none;
}
.mb-float-pop a .num {
  font: 500 11px/1 var(--f-mono, ui-monospace, "JetBrains Mono", monospace);
  color: var(--fg-dim);
  flex-shrink: 0; min-width: 28px;
}
.mb-float-pop a:hover {
  background: var(--bg-warm);
  color: var(--fg);
  transform: none;
  border-color: transparent;
}
.mb-float-pop a.is-active {
  background: var(--accent-soft);
  color: var(--accent-text);
}
.mb-float-pop a.is-active .num { color: var(--accent-text); }

@media print { .mb-float { display: none !important; } }
@media (prefers-reduced-motion: reduce) {
  .mb-float button, .mb-float a, .mb-float-pop {
    transition: none !important;
  }
}
@media (max-width: 480px) {
  .mb-float { left: 14px; bottom: 14px; gap: 8px; }
  .mb-float button, .mb-float a { padding: 9px 12px; font-size: 11px; }
}
