/* ═══════════════════════════════════════════════════════════════════════════
   ARTIFICER · Design tokens · v0.18.1 (apothecary × deco × speakeasy)
   AuDHD-optimized, Ghostty-rooted, with warm Jazz-Age accents.
   Default theme = dark. Add data-theme="light" on <html> for ivory paper mode.

   v0.6 palette — locked Nov 2024:
     • indigo ink #20203e replaces navy #0d1b2a (cooler, richer)
     • apothecary green #4a8a5e is the one success color (was olive #b7bd73)
     • brick #a04540 is destructive (was vermillion #c4392a / #e8836f)
     • copper #b87333 retired from status duty — available decoratively
     • gold #dbbb6f (was #e0b558) — slightly warmer
     • rose #c4808a promoted to warning (was #e5afb7)
     • deep purple #331567 + lifted purple #5a3a9a (replaces lilac #c9b5e8)
     • steel #b8cad4 (slightly brighter than #9fb6c4)
     • light: ivory paper #f5ead0 floor, champagne #eddcc0 raised
   ═══════════════════════════════════════════════════════════════════════════ */

/* ═══════════════════════════════════════════════════════════════════════════
   CONTENTS · the map (#78 — primitive roles shouldn't need a grep)
   Source order. Jump by searching the section banner, e.g. "═══ Buttons" or
   "NAVIGATION —". Per-primitive role taxonomies live inline in each banner.

   SETUP         Self-hosted fonts · :root design tokens · light mode (@media) ·
                 reduced-motion (@media)
   FOUNDATION    Base · Type utilities
   COMPONENTS    Buttons · Cards · Inputs · Badges · Kbd · Dividers · Focus-pane ·
                 Selection · Scrollbar · Theme toggle
   LAYOUT        Layout primitives · Overlays (modal & popover) · Form controls
                 (extended) · Data display · States (empty · skeleton · progress) ·
                 Accessibility utilities
   COMPOSITION   density modes · table extensions · dashboard layouts ·
                 live-update atoms
   CHARTS        tokens + primitives
   DIAGRAMS      primitives for hand-drawn SVG
   NAVIGATION    the spine — .crumb (where am I) · .sidenav (what sections) ·
                 .appbar (global chrome) · .tabs (switch view within a surface)
   BRAND         wordmark (lowercase "artificer." + burnished stop)
   RESPONSIVE    breakpoints · touch escalation · safe-area · table reflow
   PRIMITIVES    v0.10.0 — avatar · accordion · selectable rows · date/time
   ═══════════════════════════════════════════════════════════════════════════ */

/* ─── Self-hosted fonts ──────────────────────────────────────────────────
   JetBrains Mono — code, identifiers, numerals, dense tool body
   iA Writer Quattro — humanist sans, document body & UI labels
   Both SIL OFL 1.1. Files live in src/assets/fonts/.
   See FONTS.md for the rationale and how to ship Quattro S/V if desired. */
@font-face {
  font-family: 'JetBrains Mono';
  src: url('assets/fonts/jetbrains-mono-400.woff2') format('woff2');
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'JetBrains Mono';
  src: url('assets/fonts/jetbrains-mono-500.woff2') format('woff2');
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'JetBrains Mono';
  src: url('assets/fonts/jetbrains-mono-700.woff2') format('woff2');
  font-weight: 700; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'iA Writer Quattro';
  src: url('assets/fonts/ia-writer-quattro-400.woff2') format('woff2');
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'iA Writer Quattro';
  src: url('assets/fonts/ia-writer-quattro-400-italic.woff2') format('woff2');
  font-weight: 400; font-style: italic; font-display: swap;
}
@font-face {
  font-family: 'iA Writer Quattro';
  src: url('assets/fonts/ia-writer-quattro-700.woff2') format('woff2');
  font-weight: 700; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'iA Writer Quattro';
  src: url('assets/fonts/ia-writer-quattro-700-italic.woff2') format('woff2');
  font-weight: 700; font-style: italic; font-display: swap;
}

/* iA Writer Quattro S — tabular-numeral variant, preferred on tool surfaces
   for stacked numeric data. Same metrics as Quattro, so the fallback chain
   doesn't reflow when this variant is missing. */
@font-face {
  font-family: 'iA Writer Quattro S';
  src: url('assets/fonts/ia-writer-quattro-s-400.woff2') format('woff2');
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'iA Writer Quattro S';
  src: url('assets/fonts/ia-writer-quattro-s-400-italic.woff2') format('woff2');
  font-weight: 400; font-style: italic; font-display: swap;
}
@font-face {
  font-family: 'iA Writer Quattro S';
  src: url('assets/fonts/ia-writer-quattro-s-700.woff2') format('woff2');
  font-weight: 700; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'iA Writer Quattro S';
  src: url('assets/fonts/ia-writer-quattro-s-700-italic.woff2') format('woff2');
  font-weight: 700; font-style: italic; font-display: swap;
}

/* iA Writer Quattro V — display variant (slightly tighter, more contrast).
   Only 400 weight ships from iA; use for hero/marquee type where Quattro
   feels too even. */
@font-face {
  font-family: 'iA Writer Quattro V';
  src: url('assets/fonts/ia-writer-quattro-v-400.woff2') format('woff2');
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'iA Writer Quattro V';
  src: url('assets/fonts/ia-writer-quattro-v-400-italic.woff2') format('woff2');
  font-weight: 400; font-style: italic; font-display: swap;
}

/* ─── DARK (default) ──────────────────────────────────────────────────── */
:root {
  /* Surfaces — Ghostty dark grey spine */
  --bg:              #292c33;   /* base surface */
  --bg-raised:       #313540;   /* cards, sidebar */
  --bg-overlay:      #3c4150;   /* modals, command palette */
  --bg-inactive:     #1d1f21;   /* unfocused panes */

  /* Foreground — bone, not chrome. Softer than #fff but still AAA. */
  --fg:              #e8e6e1;   /* warm off-white, 11.2:1 on --bg */
  --fg-secondary:    #c5c8c6;
  --fg-muted:        #5a7a8a;   /* steel-blue muted — meta/comment text, between secondary and disabled */
  --fg-disabled:     #666666;

  /* Structure */
  --border:          #4a4f5c;
  --border-lifted:   #5a606e;   /* lifted border — diagram rails, perceptible on canvas */

  /* ── Jazz Age accents, contrast-tuned for dark bg ──────────────────── */
  --accent:          #dbbb6f;   /* burnished gold — interactive */
  --accent-bright:   #e3c885;   /* hover/focus, slightly lifted */
  --accent-fill:     #c4932a;   /* deep gold, fill-only (buttons, badges, selection) */
  --attention:       #c4808a;   /* dusty rose — "look when you can" */
  --attention-fill:  #c4808a;   /* rose, fill-only */
  --urgent:          #a04540;   /* brick — destructive, errors */
  --urgent-fill:     #a04540;   /* brick fill, buttons */
  --success:         #4a8a5e;   /* apothecary green — completed, merged, autoAccept */
  --success-fill:    #2d6644;   /* deep apothecary fill — badges/dots, ivory-on 5.67 AA */
  --steel:           #b8cad4;   /* honest middle; meta / secondary UI */
  --steel-fill:      #5a7a8a;

  /* Brand — personal signature (gold + purple = Cameron). Not semantic.
     bare = deep, NOT a foreground role (chart series-3, whimsy gradient stop).
     -bright = foreground (syntax keyword). -fill = surface. */
  --brand-purple:        #5a3a9a;  /* deep — chart series-3, whimsy gradient stop */
  --brand-purple-bright: #b095e0;  /* lifted — syntax keyword foreground · 5.47:1 AA · v0.10.1 */
  --brand-purple-fill:   #331567;  /* deep — surface / button fill */

  /* Diff backgrounds — derived tints of success/urgent. Same hue, low L, kept C. */
  --diff-add-bg:     #1f3a28;   /* derived from --success #4a8a5e */
  --diff-del-bg:     #3a1f1d;   /* derived from --urgent  #a04540 */

  /* Ivory / ink anchors — only used for on-color text */
  --ivory:           #f5ead0;
  --ivory-raised:    #eddcc0;   /* champagne — only meaningful in light mode */
  --ink:             #20203e;   /* midnight indigo — was --navy */
  --navy:            #20203e;   /* alias for back-compat */

  /* On-color text — chosen for AAA where possible, strong AA otherwise */
  --on-accent:       #20203e;   /* indigo on gold */
  --on-attention:    #20203e;   /* indigo on rose */
  --on-urgent:       #f5ead0;   /* ivory on brick */
  --on-success:      #f5ead0;   /* ivory on apothecary green */

  /* Type, radii, spacing, motion — identical across themes */
  --font-mono:  'JetBrains Mono', 'Berkeley Mono', ui-monospace, 'SF Mono', Menlo, monospace;
  /* iA Writer Quattro — humanist sans designed alongside Quattro S/V.
     If you self-install Quattro S (tabular nums) or V (display), they take
     precedence here automatically. Falls back to serif → system-ui. */
  --font-sans:  'iA Writer Quattro S', 'iA Writer Quattro V', 'iA Writer Quattro',
                'Iowan Old Style', 'Charter', 'Source Sans 3',
                system-ui, -apple-system, sans-serif;

  /* Type sizes are REM (root = browser default, never overridden) so text
     honors the user's browser font-size preference, not just zoom. 1rem = 16px
     at default. Spacing/radii/component-internal px stay px on purpose. */
  --t-headline-lg-size: 1.75rem;    --t-headline-lg-lh: 1.3;  --t-headline-lg-ls: -0.01em; /* 28px */
  --t-headline-md-size: 1.375rem;   --t-headline-md-lh: 1.35;                               /* 22px */
  --t-body-lg-size:     1rem;       --t-body-lg-lh:     1.65;                               /* 16px */
  --t-body-md-size:     0.875rem;   --t-body-md-lh:     1.6;                                /* 14px */
  --t-label-md-size:    0.8125rem;  --t-label-md-lh:    1.3;                                /* 13px */
  --t-label-sm-size:    0.75rem;    --t-label-sm-lh:    1.3;                                /* 12px */
  --t-label-xs-size:    0.6875rem;  --t-label-xs-lh:    1.3;                                /* 11px — micro-chrome (v0.9.0 tier; restored v0.10.1) */
  --t-code-size:        0.875rem;   --t-code-lh:        1.5;                                /* 14px */

  --letter-spacing-body: 0.01em;
  --word-spacing-body:   0.16em;

  --radius-sm: 4px;  --radius-md: 8px;  --radius-lg: 12px;  --radius-pill: 999px; /* fully-rounded pills: toggle track, chip */

  --s-xs: 4px; --s-sm: 8px; --s-md: 16px; --s-lg: 24px; --s-xl: 32px; --s-2xl: 48px; --s-3xl: 96px;

  --dur-instant: 80ms; --dur-fast: 160ms; --dur-max: 300ms;
  --ease: cubic-bezier(0.2, 0.7, 0.3, 1);
  --ease-linear: linear;          /* continuous translation only (motion pattern #02) */

  /* Focus ring — tokenized so themes/consumers retune; a11y floor: never 0. */
  --focus-width: 2px;
  --focus-offset: 2px;            /* default — drawn outside the box */
  --focus-offset-loose: 4px;      /* sliders / thumbs that need air */
  --focus-offset-inset: -2px;     /* rings drawn inside (nav items, table rows) */

  --shadow-overlay: 0 4px 12px rgba(0, 0, 0, 0.35);
  --shadow-popover: 0 2px 8px rgba(0, 0, 0, 0.30);

  /* z-index scale — six rungs, no improvising */
  --z-base:    1;     /* in-flow */
  --z-raised:  10;    /* sticky elements, dropdown triggers */
  --z-overlay: 100;   /* sidebars, theme toggle */
  --z-popover: 1000;  /* tooltips, popovers, dropdowns */
  --z-modal:   2000;  /* dialogs, scrim + content */
  --z-toast:   3000;  /* always-on-top notifications */

  /* Artificer v0.10.1 — baseline contract: see CLAUDE.md § Baseline contract */
  --art-version: "0.18.1";

  color-scheme: dark;
}

/* ─── LIGHT — ivory paper floor, champagne raised, indigo ink ────────── */
:root[data-theme="light"] {
  --bg:              #f5ead0;   /* ivory paper · the floor */
  --bg-raised:       #eddcc0;   /* champagne · the lifted card */
  --bg-overlay:      #e4d4b0;   /* modal, palette */
  --bg-inactive:     #dcc99c;   /* dimmed panes */

  --fg:              #20203e;   /* midnight indigo */
  --fg-secondary:    #4a3f2a;   /* warm muted */
  --fg-muted:        #5a7a8a;
  --fg-disabled:     #8a8070;

  --border:          #cbb88a;
  --border-lifted:   #b8a674;   /* lifted border — diagram rails */

  --accent:          #7a5a10;   /* deep gold, AA 5.33 on ivory */
  --accent-bright:   #866010;   /* slightly lighter */
  --accent-fill:     #c4932a;   /* standard gold for button fills */
  --attention:       #8a6618;   /* dark gold doubles as attention in light */
  --attention-fill:  #dbbb6f;
  --urgent:          #8a2418;   /* deep brick, AAA on ivory */
  --urgent-fill:     #a04540;   /* brick fill */
  --success:         #2a5a3a;   /* deep apothecary green */
  --success-fill:    #2a5a3a;   /* fill == bare success in light; ivory-on 6.70 AA */
  --steel:           #2e4a5a;
  --steel-fill:      #5a7a8a;

  --brand-purple:        #4a25a0;  /* deep — chart series-3, whimsy gradient stop · 8.51:1 on ivory */
  --brand-purple-bright: #5a35b0;  /* lifted — syntax keyword foreground · 6.83:1 on ivory · v0.10.1 */
  --brand-purple-fill:   #331567;  /* deep purple, fill */

  --diff-add-bg:     #d8e4cc;   /* derived light tint of apothecary green */
  --diff-del-bg:     #ecd6d4;   /* derived light tint of brick */

  --ivory:           #f5ead0;
  --ivory-raised:    #eddcc0;
  --ink:             #20203e;
  --navy:            #20203e;   /* alias */

  --on-accent:       #f5ead0;   /* ivory on gold-fill button */
  --on-attention:    #20203e;
  --on-urgent:       #f5ead0;
  --on-success:      #f5ead0;

  --shadow-overlay: 0 4px 12px rgba(32, 32, 62, 0.18);
  --shadow-popover: 0 2px 8px rgba(32, 32, 62, 0.14);

  color-scheme: light;
}

/* Honor OS preference when no explicit theme set */
@media (prefers-color-scheme: light) {
  :root:not([data-theme]) {
    --bg:              #f5ead0;
    --bg-raised:       #eddcc0;
    --bg-overlay:      #e4d4b0;
    --bg-inactive:     #dcc99c;
    --fg:              #20203e;
    --fg-secondary:    #4a3f2a;
    --fg-muted:        #5a7a8a;
    --fg-disabled:     #8a8070;
    --border:          #cbb88a;
    --border-lifted:   #b8a674;
    --accent:          #7a5a10;
    --accent-bright:   #866010;
    --accent-fill:     #c4932a;
    --attention:       #8a6618;
    --attention-fill:  #dbbb6f;
    --urgent:          #8a2418;
    --urgent-fill:     #a04540;
    --success:         #2a5a3a;
    --success-fill:    #2a5a3a;
    --steel:           #2e4a5a;
    --steel-fill:      #5a7a8a;
    --brand-purple:        #4a25a0;
    --brand-purple-bright: #5a35b0;  /* v0.10.1 — syntax keyword foreground · 6.83:1 */
    --brand-purple-fill:   #331567;
    --diff-add-bg:     #d8e4cc;
    --diff-del-bg:     #ecd6d4;
    --ivory-raised:    #eddcc0;
    --ink:             #20203e;
    --on-accent:       #f5ead0;
    --on-attention:    #20203e;
    --on-urgent:       #f5ead0;
    --on-success:      #f5ead0;
    --shadow-overlay:  0 4px 12px rgba(32, 32, 62, 0.18);
    --shadow-popover:  0 2px 8px rgba(32, 32, 62, 0.14);
    color-scheme: light;
  }
}

@media (prefers-reduced-motion: reduce) {
  :root { --dur-instant: 0ms; --dur-fast: 0ms; --dur-max: 0ms; }
}

/* ═══ Base ══════════════════════════════════════════════════════════════ */
*, *::before, *::after { box-sizing: border-box; }

/* The root is NEVER overridden (#211). html stays at the browser preference
   (100% of the user's setting; 16px by default) so every rem-bound --t-*-size
   renders at its labeled px and the whole system scales with the preference.
   The old shared `html, body` font-size silently made the root 14px (every
   token rendered at 87.5% of its label) and double-applied on body (12.25px).
   Body takes the body-md default exactly once, here. */
html { font-size: 100%; }

html, body {
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-sans);
  line-height: var(--t-body-md-lh);
  letter-spacing: var(--letter-spacing-body);
  margin: 0; padding: 0;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}

body { font-size: var(--t-body-md-size); }

/* Tool-surface override: dashboards, terminals, data tables, settings.
   Apply <body class="surface-tool"> (or to a top-level container) to flip the
   body face to mono. Documents inherit the sans default. */
.surface-tool, .surface-tool * { font-family: var(--font-mono); }
.surface-tool .btn,
.surface-tool .field > label, .surface-tool .field > .hint, .surface-tool .field > .error,
.surface-tool .badge, .surface-tool .table th, .surface-tool .checkbox, .surface-tool .radio,
.surface-tool .toggle, .surface-tool .theme-toggle, .surface-tool .tooltip {
  /* labels/hints/microcopy on tool surfaces stay sans, per the rule */
  font-family: var(--font-sans);
}

/* ═══ Type utilities ════════════════════════════════════════════════════
   Headlines and body run on --font-sans (Quattro) by default — document mode.
   On tool surfaces, wrap the surface in <… class="surface-tool"> to flip to mono.
   .t-mono-* are explicit mono utilities for code blocks and identifier displays. */
.t-headline-lg { font-family: var(--font-sans); font-weight: 700; font-size: var(--t-headline-lg-size); line-height: var(--t-headline-lg-lh); letter-spacing: var(--t-headline-lg-ls); color: var(--fg); }
.t-headline-md { font-family: var(--font-sans); font-weight: 700; font-size: var(--t-headline-md-size); line-height: var(--t-headline-md-lh); color: var(--fg); }
.t-body-lg     { font-family: var(--font-sans); font-weight: 400; font-size: var(--t-body-lg-size); line-height: var(--t-body-lg-lh); }
.t-body-md     { font-family: var(--font-sans); font-weight: 400; font-size: var(--t-body-md-size); line-height: var(--t-body-md-lh); }
.t-label-md    { font-family: var(--font-sans); font-weight: 500; font-size: var(--t-label-md-size); line-height: var(--t-label-md-lh); }
.t-label-sm    { font-family: var(--font-sans); font-weight: 400; font-size: var(--t-label-sm-size); line-height: var(--t-label-sm-lh); }
.t-label-xs    { font-family: var(--font-sans); font-weight: 500; font-size: var(--t-label-xs-size); line-height: var(--t-label-xs-lh); } /* micro-chrome: keycaps, timestamps, dense table meta. Never body. */
.t-code        { font-family: var(--font-mono); font-weight: 400; font-size: var(--t-code-size); line-height: var(--t-code-lh); color: var(--accent); }
.t-mono-headline-lg { font-family: var(--font-mono); font-weight: 700; font-size: var(--t-headline-lg-size); line-height: var(--t-headline-lg-lh); letter-spacing: var(--t-headline-lg-ls); color: var(--fg); }
.t-mono-body-md     { font-family: var(--font-mono); font-weight: 400; font-size: var(--t-body-md-size); line-height: var(--t-body-md-lh); }

.anchor { font-weight: 700; color: var(--fg); }
.meta { color: var(--fg-secondary); font-family: var(--font-sans); font-size: var(--t-body-md-size); max-width: 66ch; }
.note { color: var(--fg-secondary); font-size: var(--t-label-sm-size); }   /* inherits body family — mono on tool surfaces, sans on doc surfaces. */
.num  { font-variant-numeric: tabular-nums; }

/* Doc-page chrome — section header convention promoted from live-spec
   (10+ identical instances; same shape as the .crumb-at-14 promotion). */
.section-title {
  font-family: var(--font-mono); font-weight: 700; font-size: var(--t-headline-md-size);
  letter-spacing: .04em; text-transform: uppercase;
  color: var(--fg-secondary);
  margin: var(--s-2xl) 0 var(--s-md);
  padding-bottom: var(--s-sm);
  border-bottom: 1px solid var(--border);
}

code, .code {
  font-family: var(--font-mono);
  font-size: 0.95em;
  color: var(--accent);
  background: transparent;
}

a {
  color: var(--accent);
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease);
}
a:hover, a:focus-visible { color: var(--accent-bright); text-decoration: underline; text-underline-offset: 2px; }
a:focus-visible { outline: var(--focus-width) solid var(--accent); outline-offset: var(--focus-offset); border-radius: 2px; }

/* ═══ Buttons ═══════════════════════════════════════════════════════════ */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s-sm);
  font-family: var(--font-sans); font-size: var(--t-label-md-size); font-weight: 500; line-height: 1;
  padding: 10px 20px; border-radius: var(--radius-sm); border: 1px solid transparent;
  cursor: pointer; white-space: nowrap;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.btn:focus-visible { outline: var(--focus-width) solid var(--accent); outline-offset: var(--focus-offset); }

.btn--primary       { background: var(--accent-fill); color: var(--on-accent); }
.btn--primary:hover { background: var(--accent-bright); }

.btn--secondary       { background: transparent; color: var(--accent); border-color: var(--accent); }
.btn--secondary:hover { background: color-mix(in oklch, var(--accent) 14%, transparent); color: var(--accent-bright); border-color: var(--accent-bright); }

.btn--ghost       { background: transparent; color: var(--fg-secondary); }
.btn--ghost:hover { background: var(--bg-raised); color: var(--fg); }

/* Icon-only button — square, meets the 44px nav touch floor (a11y #8).
   Compose with any variant (.btn.btn--icon.btn--ghost). Always needs an
   aria-label, since there's no text. v0.10.0. */
.btn--icon { width: 44px; height: 44px; min-width: 44px; padding: 0; }

.btn--destructive       { background: var(--urgent-fill); color: var(--on-urgent); }
.btn--destructive:hover { filter: brightness(1.08); }

/* Size axis — orthogonal to role. Compose freely: .btn--secondary.btn--sm.
   .btn--sm is for dense/overlay/mouse contexts (toolbar, card corner, inline).
   The @media (pointer: coarse) touch escalation (below) still applies — on a
   finger, .btn--sm gets the same min-height: 44px floor as the base size. */
.btn--sm { padding: 6px 12px; /* tuned */ font-size: var(--t-label-sm-size); }
.btn--lg { padding: 14px 28px; /* tuned */ font-size: var(--t-body-md-size); }

.btn[disabled], .btn--disabled {
  background: var(--bg-raised); color: var(--fg-disabled); border-color: transparent; cursor: not-allowed;
}

/* ═══ Cards ═════════════════════════════════════════════════════════════ */
.card {
  background: var(--bg-raised); color: var(--fg);
  border-radius: var(--radius-md); padding: var(--s-lg);
  border: 1px solid transparent;
}
.card--active    { background: var(--bg); border-left: 2px solid var(--accent); }
.card--attention { background: var(--bg); border-left: 2px solid var(--attention); }
.card--urgent    { background: var(--bg); border-left: 2px solid var(--urgent); }
.card--success   { background: var(--bg); border-left: 2px solid var(--success); }

/* ═══ Inputs ════════════════════════════════════════════════════════════ */
.input, .textarea, .select {
  background: var(--bg); color: var(--fg);
  font-family: var(--font-mono); font-size: var(--t-body-md-size); line-height: 1.4;
  padding: 10px 14px;
  border: 1px solid var(--border); border-radius: var(--radius-sm); width: 100%;
  transition: border-color var(--dur-fast) var(--ease);
}
.input:focus, .textarea:focus, .select:focus { outline: none; border-color: var(--accent); }
.input::placeholder { color: var(--fg-disabled); }
.input[aria-invalid="true"] { border-color: var(--urgent); }
/* Custom indicator for .select — appearance:none lets iOS WebKit honour
   author min-height (fixes the touch-target floor, #123). Two linear
   gradients form a downward chevron using --fg-secondary only; no hex,
   no SVG, no wrapper element.
     Layer 1 (135deg): lower-right triangle = left arm of the ∨
     Layer 2 ( 45deg): lower-left triangle  = right arm of the ∨
   Together they tile into a ▽ at the element's right edge. */
.select {
  appearance: none;
  -webkit-appearance: none;
  background-image:
    linear-gradient(135deg, transparent 50%, var(--fg-secondary) 50%),
    linear-gradient(45deg, var(--fg-secondary) 50%, transparent 50%);
  background-position: calc(100% - 26px) center, calc(100% - 21px) center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat, no-repeat;
  padding-right: var(--s-xl); /* 32px — clears the 10px indicator at 16px from right */
  cursor: pointer;
}
.select:disabled { color: var(--fg-disabled); cursor: not-allowed; }

.field { display: flex; flex-direction: column; gap: var(--s-sm); }
.field > label { font-family: var(--font-sans); font-size: var(--t-label-md-size); font-weight: 500; color: var(--fg); }
.field > .hint  { font-family: var(--font-sans); font-size: var(--t-label-sm-size); color: var(--fg-secondary); }
.field > .error { font-family: var(--font-sans); font-size: var(--t-label-sm-size); color: var(--urgent); }

/* ═══ Badges ════════════════════════════════════════════════════════════ */
.badge {
  display: inline-flex; align-items: center; gap: var(--s-xs);
  padding: 2px var(--s-sm);
  font-family: var(--font-sans); font-size: var(--t-label-sm-size); font-weight: 500; line-height: 1.3;
  border-radius: var(--radius-sm); white-space: nowrap;
}
.badge--accent    { background: var(--accent-fill);    color: var(--on-accent); }
.badge--attention { background: var(--attention-fill); color: var(--on-attention); }
.badge--urgent    { background: var(--urgent-fill);    color: var(--on-urgent); }
.badge--success   { background: var(--success-fill);   color: var(--on-success); }
.badge--ghost     { background: transparent; color: var(--fg-secondary); border: 1px solid var(--border); }

.dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.dot--accent    { background: var(--accent-fill); }
.dot--attention { background: var(--attention-fill); }
.dot--urgent    { background: var(--urgent-fill); }
.dot--success   { background: var(--success-fill); }

/* ═══ Notification ══════════════════════════════════════════════════════ */
/*
 * Tier by ACTION REQUIRED, not severity.
 *   --urgent      Blocking. Errors, failed builds. role="alert"; persists until dismissed.
 *   --attention   Look when you can. Agent waiting, review needed. role="status"; persists.
 *   --info        Task complete. Auto-dismiss after 5 s, subtle badge. role="status".
 *   --background  Silent badge. Progress, status. NO role — never announced, never a toast.
 *
 * Anatomy: .notif > .dot.dot--{tier}  .notif__body > .notif__title + .notif__msg
 * A11y: dot + text always — never color alone. Roles are the CONSUMER's job at
 * insert time: urgent → "alert", attention/info → "status", background → none.
 * One contract with .toast-region (see that block + notifications.html).
 */
.notif {
  display: flex; align-items: flex-start; gap: var(--s-sm);
  padding: 10px 14px; /* tuned */
  border-radius: var(--radius-sm);
  border: 1px solid var(--border); background: var(--bg);
  font-family: var(--font-mono); font-size: var(--t-label-md-size); line-height: var(--t-body-md-lh);
  color: var(--fg);
}
.notif--urgent    { border-color: var(--urgent); }
.notif--attention { border-color: var(--attention); }
.notif--info      { border-color: var(--accent); }
.notif--background { color: var(--fg-secondary); }
.notif--background .dot   { background: var(--border); }

.notif__body  { display: flex; flex-direction: column; gap: var(--s-xs); flex: 1; min-width: 0; }
.notif__title { margin: 0; font-weight: 700; color: var(--fg); }
.notif--background .notif__title { color: var(--fg-secondary); }
.notif__msg   { margin: 0; color: var(--fg-secondary); }

/* ═══ Kbd ══════════════════════════════════════════════════════════════ */
kbd, .kbd {
  font-family: var(--font-mono); font-size: var(--t-label-xs-size); font-weight: 500;
  padding: 2px 6px; background: var(--bg); color: var(--fg-secondary);
  border: 1px solid var(--border); border-radius: 3px; line-height: 1.2; white-space: nowrap;
}

/* ═══ Dividers ══════════════════════════════════════════════════════════ */
hr, .divider { border: none; border-top: 1px solid var(--border); margin: var(--s-lg) 0; }

/* ═══ Focus-pane ════════════════════════════════════════════════════════ */
.pane--inactive {
  opacity: 0.55; background: var(--bg-inactive); filter: saturate(0.6);
  transition: opacity var(--dur-fast) var(--ease), filter var(--dur-fast) var(--ease);
}
.pane--active { border-left: 2px solid var(--accent); }

/* ═══ Selection ═════════════════════════════════════════════════════════ */
::selection { background: var(--accent-fill); color: var(--on-accent); }

/* ═══ Scrollbar ═════════════════════════════════════════════════════════ */
* { scrollbar-width: thin; scrollbar-color: var(--border) transparent; }
*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb { background: var(--border); border-radius: var(--radius-sm); border: 2px solid transparent; background-clip: padding-box; }
*::-webkit-scrollbar-thumb:hover { background: var(--fg-disabled); background-clip: padding-box; border: 2px solid transparent; }

/* ═══ Theme toggle — shared across pages ═══════════════════════════════
   Pins to the top-right of the viewport on every page that drops the
   button into the document. Sits on the overlay rung so it survives
   over a sticky .appbar or sidenav. Opt out with .theme-toggle--inline
   when a page wants the control to live in a specific row instead. */
.theme-toggle {
  position: fixed; top: var(--s-md); right: var(--s-md); z-index: var(--z-overlay);
  display: inline-flex; align-items: center; gap: 6px;
  background: var(--bg-raised); color: var(--fg-secondary);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 6px 10px; cursor: pointer;
  font-family: var(--font-sans); font-size: var(--t-label-xs-size); font-weight: 500;
  letter-spacing: 0.08em; text-transform: uppercase;
  transition: color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
  box-shadow: 0 1px 2px rgba(0,0,0,.18);
}
.theme-toggle:hover { color: var(--fg); border-color: var(--accent); box-shadow: 0 2px 6px rgba(0,0,0,.22); }
.theme-toggle .dot { width: 10px; height: 10px; border-radius: 50%; background: var(--accent-fill); }
.theme-toggle--inline { position: static; top: auto; right: auto; z-index: auto; box-shadow: none; }
@media (max-width: 640px) {
  .theme-toggle { top: var(--s-sm); right: var(--s-sm); }
}

/* ═══ Layout primitives ════════════════════════════════════════════════ */
/* Stack: vertical rhythm. --gap defaults to --s-md. */
.stack       { display: flex; flex-direction: column; gap: var(--gap, var(--s-md)); }
.stack--xs   { --gap: var(--s-xs); }
.stack--sm   { --gap: var(--s-sm); }
.stack--md   { --gap: var(--s-md); }
.stack--lg   { --gap: var(--s-lg); }
.stack--xl   { --gap: var(--s-xl); }
.stack--2xl  { --gap: var(--s-2xl); }

/* Cluster: horizontal flow that wraps. */
.cluster     { display: flex; flex-wrap: wrap; gap: var(--gap, var(--s-md)); align-items: center; }
.cluster--sm { --gap: var(--s-sm); }
.cluster--lg { --gap: var(--s-lg); }
.cluster--between { justify-content: space-between; }
.cluster--end     { justify-content: flex-end; }

/* Auto-grid: equal columns, --min controls breakpoint.
   minmax(0,1fr) not bare 1fr: a bare 1fr is minmax(AUTO,1fr), whose min is the
   cell's max-content — so one wide child (a <pre>, fixed SVG, wide table) refuses
   to let the track shrink and overflows the page. minmax(0,…) lets it shrink and
   the child scrolls/wraps within its cell. (#116 — the overflow-safe track.) */
.grid-auto   { display: grid; gap: var(--gap, var(--s-md)); grid-template-columns: repeat(auto-fill, minmax(var(--min, 240px), 1fr)); }
.grid-2      { display: grid; gap: var(--gap, var(--s-md)); grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid-3      { display: grid; gap: var(--gap, var(--s-md)); grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (max-width: 640px) { .grid-2, .grid-3 { grid-template-columns: 1fr; } }

/* Container: max-width wrapper, three sizes. */
.container       { width: 100%; margin-inline: auto; padding-inline: var(--s-lg); }
.container--sm   { max-width: 560px; }
.container--md   { max-width: 820px; }
.container--lg   { max-width: 1120px; }

/* Page shell: sticky masthead + scrolling main. */
.page-shell      { display: grid; grid-template-rows: auto 1fr; min-height: 100vh; }
.page-shell > header { position: sticky; top: 0; z-index: var(--z-overlay); background: var(--bg); border-bottom: 1px solid var(--border); }

/* ═══ Overlays · modal & popover ═══════════════════════════════════════ */
.scrim {
  position: fixed; inset: 0; z-index: var(--z-modal);
  background: rgba(0,0,0,.55);
  display: flex; align-items: center; justify-content: center;
  padding: var(--s-lg);
  animation: scrim-in var(--dur-fast) var(--ease);
}
:root[data-theme="light"] .scrim { background: rgba(13,27,42,.4); }
/* Flush scrim — no gutter padding; the child owns its own insets.
   Use for full-screen mobile sheets or media viewers that fill the viewport. */
.scrim--flush { padding: 0; }
@keyframes scrim-in { from { opacity: 0 } to { opacity: 1 } }

.modal {
  background: var(--bg-overlay); color: var(--fg);
  border-radius: var(--radius-lg); padding: var(--s-xl);
  width: 100%; max-width: 480px;
  box-shadow: var(--shadow-overlay);
  border: 1px solid var(--border);
  animation: modal-in var(--dur-fast) var(--ease);
}
.modal--lg   { max-width: 720px; }
.modal--xl   { max-width: min(96vw, 1100px); }
.modal--full { max-width: 100vw; border-radius: 0; }
@keyframes modal-in { from { opacity: 0; transform: translateY(8px) } to { opacity: 1; transform: none } }
.modal__title  { font-family: var(--font-mono); font-weight: 700; font-size: var(--t-headline-md-size); line-height: var(--t-headline-md-lh); margin: 0 0 var(--s-sm); }
.modal__body   { color: var(--fg-secondary); margin: 0 0 var(--s-lg); line-height: 1.6; }
.modal__footer { display: flex; gap: var(--s-sm); justify-content: flex-end; }

.popover {
  background: var(--bg-overlay); color: var(--fg);
  border-radius: var(--radius-md); padding: var(--s-md);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-popover);
  font-family: var(--font-sans); font-size: var(--t-label-sm-size); line-height: 1.5;
  z-index: var(--z-popover);
  max-width: 280px;
}

.tooltip {
  position: absolute; z-index: var(--z-popover);
  background: var(--fg); color: var(--bg);
  padding: var(--s-xs) var(--s-sm); border-radius: var(--radius-sm);
  font-family: var(--font-sans); font-size: var(--t-label-xs-size); line-height: 1.3;
  pointer-events: none; white-space: nowrap;
  box-shadow: var(--shadow-popover);
}

/* ═══ Form controls — extended ═════════════════════════════════════════ */
/* Checkbox & radio — custom-rendered via accent-color so the focus ring
   stays consistent with the rest of the system. */
.checkbox, .radio {
  display: inline-flex; align-items: flex-start; gap: var(--s-sm);
  font-family: var(--font-sans); font-size: var(--t-label-md-size); line-height: 1.4;
  cursor: pointer; user-select: none;
}
.checkbox > input, .radio > input {
  appearance: none; -webkit-appearance: none;
  width: 16px; height: 16px; flex-shrink: 0; margin: 2px 0 0;
  background: var(--bg); border: 1px solid var(--border);
  cursor: pointer; transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.checkbox > input { border-radius: 3px; }
.radio    > input { border-radius: 50%; }
.checkbox > input:checked, .radio > input:checked { background: var(--accent-fill); border-color: var(--accent-fill); }
.checkbox > input:checked::after {
  content: ""; display: block; width: 4px; height: 8px; margin: 1px auto 0;
  border: solid var(--on-accent); border-width: 0 1.8px 1.8px 0; transform: rotate(45deg);
}
.radio > input:checked::after {
  content: ""; display: block; width: 6px; height: 6px; border-radius: 50%;
  background: var(--on-accent); margin: var(--s-xs) auto 0;
}
.checkbox > input:focus-visible, .radio > input:focus-visible { outline: var(--focus-width) solid var(--accent); outline-offset: var(--focus-offset); }
.checkbox > input:disabled + *, .radio > input:disabled + * { color: var(--fg-disabled); cursor: not-allowed; }

/* Toggle / switch */
.toggle {
  display: inline-flex; align-items: center; gap: var(--s-sm);
  font-family: var(--font-sans); font-size: var(--t-label-md-size);
  cursor: pointer; user-select: none;
}
.toggle > input {
  appearance: none; -webkit-appearance: none;
  width: 32px; height: 18px; border-radius: var(--radius-pill);
  background: var(--border); border: none; position: relative;
  cursor: pointer; transition: background var(--dur-fast) var(--ease);
  flex-shrink: 0;
}
.toggle > input::after {
  content: ""; position: absolute; top: 2px; left: 2px;
  width: 14px; height: 14px; border-radius: 50%;
  background: var(--bg); transition: transform var(--dur-fast) var(--ease);
}
.toggle > input:checked { background: var(--accent-fill); }
.toggle > input:checked::after { transform: translateX(14px); background: var(--on-accent); }
.toggle > input:focus-visible { outline: var(--focus-width) solid var(--accent); outline-offset: var(--focus-offset); }

/* Search input — input + leading icon slot */
.search {
  display: flex; align-items: center; gap: var(--s-sm);
  background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 0 12px;
  transition: border-color var(--dur-fast) var(--ease);
}
.search:focus-within { border-color: var(--accent); }
.search > input {
  flex: 1; min-width: 0; background: transparent; border: none; outline: none;
  font-family: var(--font-mono); font-size: var(--t-body-md-size);
  padding: 10px 0; color: var(--fg);
}
.search > input::placeholder { color: var(--fg-disabled); }
.search > .icon { color: var(--fg-secondary); flex-shrink: 0; }

/* Slider */
.slider { width: 100%; appearance: none; -webkit-appearance: none; height: 4px; background: var(--border); border-radius: 2px; }
.slider::-webkit-slider-thumb { appearance: none; width: 14px; height: 14px; border-radius: 50%; background: var(--accent-fill); cursor: pointer; transition: transform var(--dur-fast) var(--ease); }
.slider::-moz-range-thumb { width: 14px; height: 14px; border: none; border-radius: 50%; background: var(--accent-fill); cursor: pointer; }
.slider:focus-visible { outline: var(--focus-width) solid var(--accent); outline-offset: var(--focus-offset-loose); border-radius: 2px; }

/* File field */
.file-field {
  display: flex; align-items: center; gap: var(--s-sm);
  border: 1px dashed var(--border); border-radius: var(--radius-sm);
  padding: var(--s-md); background: var(--bg);
  font-family: var(--font-sans); font-size: var(--t-label-md-size); color: var(--fg-secondary);
}
.file-field input[type="file"] { display: none; }
/* Dropzone — the same field as a drag target. .file-field--drop makes it a
   taller centered well; .is-dragover (consumer toggles on dragenter/dragover,
   clears on dragleave/drop) lights the accent. Texture never encodes the drag
   state — color does. v0.15.0. */
.file-field--drop { flex-direction: column; align-items: center; justify-content: center; text-align: center; gap: var(--s-xs); padding: var(--s-xl); min-height: 120px; }
.file-field.is-dragover { border-color: var(--accent); color: var(--fg); background: color-mix(in oklch, var(--accent) 8%, var(--bg)); }

/* ═══ Data display ═════════════════════════════════════════════════════ */
/* Wide-content escape hatch — the content scrolls inside it, never the page.
   Generalizes the .code-block precedent for tables, diffs, and any block whose
   natural width can exceed the container (the #116 bug class). Wrap, don't fight:
   <div class="scroll-x" tabindex="0"><table class="table">…</table></div>
   The tabindex="0" is load-bearing: a scrollable region must be keyboard-
   reachable so keyboard users can scroll it (axe scrollable-region-focusable). */
.scroll-x { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.scroll-x:focus-visible, .code-block:focus-visible {
  outline: var(--focus-width) solid var(--accent); outline-offset: var(--focus-offset);
}

.code-block {
  font-family: var(--font-mono); font-size: var(--t-label-md-size); line-height: 1.6;
  background: var(--bg-inactive); color: var(--fg);
  padding: var(--s-md) var(--s-lg);
  border-radius: var(--radius-md); border: 1px solid var(--border);
  overflow-x: auto;
  white-space: pre;
  margin: 0;
}
/* Syntax — the full 12-role $roles.syntax map surfaced into the WEB layer
   (the same map that drives the editor themes; this is the web bridge).
   Existing class names (.tok-comment/keyword/string/number/fn) are kept as
   aliases. Four roles bind to canonical tokens not yet surfaced as web vars
   (successBright · attentionAlt · cyan · urgentBright) — they fall back to
   the nearest existing token via var(…, …) until those 4 are minted (a
   flagged palette-surface decision; values in _palette.json). No new tokens,
   no hardcoded hex here. comment/operator = --fg-muted (v0.7.2 reroute). */
.code-block .tok-comment,
.code-block .tok-operator  { color: var(--fg-muted); }
.code-block .tok-comment   { font-style: italic; }
.code-block .tok-keyword   { color: var(--brand-purple-bright); }
.code-block .tok-string    { color: var(--success-bright, var(--success)); }
.code-block .tok-type      { color: var(--accent-bright); }
.code-block .tok-number,
.code-block .tok-constant  { color: var(--attention-alt, var(--attention)); }
.code-block .tok-fn,
.code-block .tok-function  { color: var(--accent); }
.code-block .tok-namespace { color: var(--cyan, var(--steel)); }
.code-block .tok-param     { color: var(--steel); }
.code-block .tok-variable  { color: var(--fg); }
.code-block .tok-tag       { color: var(--urgent-bright, var(--urgent)); }
.code-block .tok-invalid   { color: var(--urgent-bright, var(--urgent)); text-decoration: wavy underline; } /* reinforced — AA-L floor */

/* Doc-page example container — promoted from live-spec .demo (15 pages).
   Pulls markup toward semantic <figure> + <figcaption class="meta">. */
.figure {
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--s-lg);
  margin-top: var(--s-sm);
}
.figure--frame { padding: 0; overflow: hidden; position: relative; }
.figure--flush { padding: 0; overflow: hidden; }

.table {
  width: 100%; border-collapse: collapse;
  font-family: var(--font-mono); font-size: var(--t-body-md-size);
}
.table th, .table td { text-align: left; padding: var(--s-sm) var(--s-md); border-bottom: 1px solid var(--border); }
.table th { font-family: var(--font-sans); font-size: var(--t-label-sm-size); font-weight: 500; letter-spacing: 0.06em; text-transform: uppercase; color: var(--fg-secondary); }
.table tbody tr:hover { background: var(--bg-raised); }
.table--zebra tbody tr:nth-child(even) { background: var(--bg-raised); }

.kv { display: grid; grid-template-columns: max-content 1fr; gap: var(--s-sm) var(--s-lg); font-family: var(--font-mono); font-size: var(--t-body-md-size); }
.kv dt { color: var(--fg-secondary); }
.kv dd { margin: 0; color: var(--fg); }

/* ═══ States — empty, skeleton, progress ══════════════════════════════ */
.empty-state {
  display: flex; flex-direction: column; align-items: flex-start;
  gap: var(--s-sm); padding: var(--s-2xl) var(--s-lg);
  border: 1px dashed var(--border); border-radius: var(--radius-md);
  background: transparent;
}
.empty-state__title { font-family: var(--font-mono); font-weight: 700; font-size: var(--t-body-lg-size); margin: 0; }
.empty-state__body  { font-family: var(--font-mono); font-size: var(--t-body-md-size); color: var(--fg-secondary); margin: 0; max-width: 50ch; line-height: 1.55; }

.skeleton {
  display: block; background: var(--bg-raised); border-radius: var(--radius-sm);
  position: relative; overflow: hidden;
  height: 1em; width: 100%;
}
.skeleton::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, color-mix(in oklch, var(--fg) 6%, transparent), transparent);
  animation: skeleton-shimmer 1.4s var(--ease-linear) infinite;
}
@keyframes skeleton-shimmer { from { transform: translateX(-100%) } to { transform: translateX(100%) } }
@media (prefers-reduced-motion: reduce) { .skeleton::after { animation: none; opacity: .25; } }
.skeleton--text  { height: 0.85em; }
.skeleton--title { height: 1.5em; width: 60%; }
.skeleton--block { height: 80px; }

.progress {
  width: 100%; height: 4px; background: var(--bg-raised); border-radius: 2px; overflow: hidden;
}
.progress__bar { height: 100%; background: var(--accent-fill); transition: width var(--dur-max) var(--ease); }

/* Attention pulse — for urgent state, accessibility-aware */
@keyframes attention-pulse {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in oklch, var(--urgent) 35%, transparent); }
  50%      { box-shadow: 0 0 0 6px color-mix(in oklch, var(--urgent) 0%, transparent); }
}
.pulse { animation: attention-pulse 1.6s var(--ease) infinite; border-radius: inherit; }
@media (prefers-reduced-motion: reduce) { .pulse { animation: none; } }

/* ═══ Accessibility utilities ═════════════════════════════════════════ */
.sr-only {
  position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
.skip-link {
  position: absolute; top: -40px; left: var(--s-md);
  background: var(--accent-fill); color: var(--on-accent);
  padding: var(--s-sm) 12px; border-radius: var(--radius-sm);
  font-family: var(--font-sans); font-size: var(--t-label-md-size); font-weight: 500;
  text-decoration: none; z-index: var(--z-toast);
  transition: top var(--dur-fast) var(--ease);
}
.skip-link:focus { top: var(--s-md); }

/* Global focus-ring strategy — opt every interactive element in.
   Already wired on a/.btn; this catches everything else. */
button:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible,
[tabindex]:focus-visible,
[role="button"]:focus-visible,
[role="link"]:focus-visible,
[role="menuitem"]:focus-visible,
[role="option"]:focus-visible,
[role="tab"]:focus-visible {
  outline: var(--focus-width) solid var(--accent);
  outline-offset: var(--focus-offset);
  border-radius: var(--radius-sm);
}

/* ═══════════════════════════════════════════════════════════════════════
   COMPOSITION · density modes
   Three densities for tables, lists, and dashboards. Set on a container.
   compact = consoles, log views, ops; cozy = default; comfortable = docs.
   ═══════════════════════════════════════════════════════════════════════ */
.density-compact     { --row-y: 4px;  --row-x: 10px; --gap: var(--s-sm); }
.density-cozy        { --row-y: 8px;  --row-x: 16px; --gap: var(--s-md); }
.density-comfortable { --row-y: 14px; --row-x: 20px; --gap: var(--s-lg); }

/* When a density is set, table cells and list items pick it up */
[class*="density-"] .table th,
[class*="density-"] .table td { padding: var(--row-y) var(--row-x); }
[class*="density-"] .stack    { gap: var(--gap); }

/* ═══════════════════════════════════════════════════════════════════════
   COMPOSITION · table extensions
   Sticky header + sticky first column + numeric column + group header.
   Layered onto .table — additive, no replacement.
   ═══════════════════════════════════════════════════════════════════════ */
.table--sticky-head thead th {
  position: sticky; top: 0; z-index: var(--z-raised);
  background: var(--bg);
  box-shadow: 0 1px 0 var(--border);
}
.table--sticky-col tbody td:first-child,
.table--sticky-col thead th:first-child {
  position: sticky; left: 0; z-index: var(--z-raised);
  background: var(--bg);
}
.table--sticky-head.table--sticky-col thead th:first-child { z-index: calc(var(--z-raised) + 1); }
.table .num,
.table th.num,
.table td.num { font-variant-numeric: tabular-nums; text-align: right; }
.table tr.group-row td {
  font-family: var(--font-sans); font-size: var(--t-label-sm-size);
  text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--fg-secondary); background: var(--bg-inactive);
  padding: var(--s-sm) var(--row-x, var(--s-md));
  border-bottom: 1px solid var(--border);
}
.table tfoot td {
  font-weight: 700; border-top: 2px solid var(--border); border-bottom: none;
  background: var(--bg-raised);
}

/* ═══════════════════════════════════════════════════════════════════════
   COMPOSITION · dashboard layouts
   Page shells used by the composition page. They read on top of .container.
   ═══════════════════════════════════════════════════════════════════════ */
/* KPI strip — 2–6 stat cards across, wraps to 2-up at narrow widths */
.kpi-strip { display: grid; gap: var(--s-md); grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); }

/* Split-pane — sidebar + main, 280px / fr; collapses to stack on mobile.
   minmax(0,1fr) on the content track, not bare 1fr — see the .grid-* note: a
   wide child (log line, table, diagram) in the main pane would otherwise grow
   the track to its content width and overflow the page. (#116) */
.split-pane { display: grid; gap: var(--s-lg); grid-template-columns: 280px minmax(0, 1fr); }
@media (max-width: 800px) { .split-pane { grid-template-columns: 1fr; } }

/* ── Dashboard frame ── one shell, many bodies. Promoted v0.10.0 (was
   demo-only in composition.html). .dash is the framed surface; .dash__topbar
   carries the title + live/range actions; the body (1fr) is built from
   primitives — .kpi-strip, .split-pane, .table, or a chart grid. The "five
   shells" (KPI · ops · observe · table-first · split) are RECIPES = .dash +
   a density-* + one of those bodies, NOT five classes. */
.dash {
  display: grid; grid-template-rows: auto 1fr; gap: var(--s-md);
  padding: var(--s-md) var(--s-lg) var(--s-lg);
  background: var(--bg);
}
.dash__topbar {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s-md); padding-bottom: var(--s-sm);
  border-bottom: 1px solid var(--border);
}
.dash__title {
  font-family: var(--font-mono); font-weight: 700;
  font-size: var(--t-body-md-size); color: var(--fg);
  margin: 0; letter-spacing: 0.04em;
}
.dash__actions { display: flex; gap: var(--s-sm); align-items: center; }

/* Filter bar — horizontal cluster of chips, search, time-range */
.filter-bar {
  display: flex; flex-wrap: wrap; gap: var(--s-sm); align-items: center;
  padding: var(--s-sm) var(--s-md);
  background: var(--bg-raised); border: 1px solid var(--border);
  border-radius: var(--radius-md);
}
.filter-bar > .grow { flex: 1; min-width: 160px; }

.chip {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--font-sans); font-size: var(--t-label-sm-size); font-weight: 500;
  padding: var(--s-xs) 10px; border-radius: var(--radius-pill);
  background: transparent; color: var(--fg-secondary);
  border: 1px solid var(--border);
  cursor: pointer; white-space: nowrap;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.chip:hover { color: var(--fg); border-color: var(--fg-secondary); }
.chip[aria-pressed="true"], .chip--active {
  background: color-mix(in oklch, var(--accent) 14%, transparent);
  color: var(--accent); border-color: var(--accent);
}
.chip__count { font-variant-numeric: tabular-nums; color: var(--fg-secondary); }

/* Time-range segmented control */
.timerange { display: inline-flex; border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; }
.timerange button {
  background: transparent; color: var(--fg-secondary);
  font-family: var(--font-mono); font-size: var(--t-label-sm-size); font-weight: 500;
  padding: 6px 12px; border: none; cursor: pointer;
  border-right: 1px solid var(--border);
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.timerange button:last-child { border-right: none; }
.timerange button:hover { color: var(--fg); background: var(--bg-raised); }
.timerange button[aria-pressed="true"] {
  background: var(--bg-raised); color: var(--accent);
}

/* ═══════════════════════════════════════════════════════════════════════
   COMPOSITION · live-update atoms
   ═══════════════════════════════════════════════════════════════════════ */
/* Pulsing dot — for "this is live" indicators */
.live-tick {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--font-sans); font-size: var(--t-label-xs-size); letter-spacing: .06em;
  text-transform: uppercase; color: var(--fg-secondary);
}
.live-tick::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: var(--success);
  box-shadow: 0 0 0 0 color-mix(in oklch, var(--success) 60%, transparent);
  animation: live-pulse 2s var(--ease) infinite;
}
@keyframes live-pulse {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in oklch, var(--success) 60%, transparent); }
  50%      { box-shadow: 0 0 0 6px color-mix(in oklch, var(--success) 0%, transparent); }
}
@media (prefers-reduced-motion: reduce) { .live-tick::before { animation: none; } }

.last-updated {
  font-family: var(--font-mono); font-size: var(--t-label-xs-size);
  color: var(--fg-secondary); font-variant-numeric: tabular-nums;
}

/* Number that swaps in place — soft fade so changes are perceived */
.live-value { transition: color var(--dur-max) var(--ease); }
.live-value.flash { color: var(--accent); }

/* Refreshing-value pattern (D1) — the re-fetch gap. A value already on screen
   that you re-fetch sits static ~800ms while the user expects change. Skeleton
   would blank it (flicker, lost context); .progress doesn't fit (nothing to
   count); the disabled label sits on the button, not the value. Recede the
   stale value — the same 55% instinct as .pane--inactive — and pulse a neutral
   dot beside it; the fresh value fades in via .live-value's existing color
   transition, the dot stops. Drive [data-refreshing] from the fetch lifecycle.
   Give the consuming element aria-live="polite" (+ aria-atomic="true") — the
   visual tick is otherwise silent to assistive tech (the dot is decoration;
   the VALUE is the announcement). Zero new tokens; a mint, not a carve.
   v0.15.0; ARIA guidance #193. */
.live-value[data-refreshing] { opacity: .55; }
.live-value__dot { display: none; width: 6px; height: 6px; border-radius: 50%; background: var(--fg-secondary); vertical-align: baseline; flex: none; }
.live-value[data-refreshing] + .live-value__dot {
  display: inline-block;
  animation: refresh-pulse 2s var(--ease) infinite;
}
@keyframes refresh-pulse {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in oklch, var(--fg-secondary) 50%, transparent); }
  50%      { box-shadow: 0 0 0 5px color-mix(in oklch, var(--fg-secondary) 0%, transparent); }
}
@media (prefers-reduced-motion: reduce) { .live-value[data-refreshing] + .live-value__dot { animation: none; } }

/* ═══════════════════════════════════════════════════════════════════════
   CHARTS · tokens + primitives
   Platform-agnostic: any library that lets you set stroke/axis/grid colors
   can read these. Pages use them in inline SVG; framework adapters can
   forward them to ECharts/Chart.js/etc.
   ═══════════════════════════════════════════════════════════════════════ */
:root {
  /* Structural — the chart's chrome */
  --chart-grid:        color-mix(in oklch, var(--border) 70%, transparent);
  --chart-grid-strong: var(--border);
  --chart-axis:        var(--fg-secondary);
  --chart-axis-label:  var(--fg-secondary);
  --chart-plot-bg:     transparent;

  /* Series — categorical 5-color palette, contrast-tuned for both themes.
     Order matters: series 1 is the accent. Keep total ≤ 5; beyond that,
     re-encode (split charts, group, or use sequential ramp). */
  --series-1: var(--accent);          /* gold */
  --series-2: var(--steel);           /* steel-blue */
  --series-3: var(--brand-purple);    /* purple */
  --series-4: var(--success);         /* olive */
  --series-5: var(--attention);       /* rose */

  /* Sequential ramp — single-hue, 5 stops light→dark.
     Use for quantitative encoding (heatmap, choropleth). */
  --ramp-1: color-mix(in oklch, var(--accent) 25%, var(--bg));
  --ramp-2: color-mix(in oklch, var(--accent) 45%, var(--bg));
  --ramp-3: color-mix(in oklch, var(--accent) 65%, var(--bg));
  --ramp-4: color-mix(in oklch, var(--accent) 85%, var(--bg));
  --ramp-5: var(--accent);

  /* Diverging — for signed deltas. Vermillion-low → neutral → olive-high. */
  --diverge-low:  var(--urgent);
  --diverge-mid:  var(--fg-disabled);
  --diverge-high: var(--success);
}

/* Chart shell — gives it a card-like surface, optional header row,
   and a fixed aspect-ratio plot area so SVG charts size predictably. */
.chart {
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--s-md) var(--s-lg) var(--s-lg);
  display: flex; flex-direction: column; gap: var(--s-sm);
}
.chart__header { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s-md); }
.chart__title  { font-family: var(--font-mono); font-size: var(--t-label-sm-size); font-weight: 700;
                 letter-spacing: .08em; text-transform: uppercase; color: var(--fg-secondary); margin: 0; }
.chart__value  { font-family: var(--font-mono); font-size: var(--t-headline-lg-size); font-weight: 700;
                 color: var(--fg); font-variant-numeric: tabular-nums; line-height: 1; }
.chart__plot   { width: 100%; aspect-ratio: 16 / 7; position: relative; }
.chart__plot--tall { aspect-ratio: 16 / 9; }
.chart__plot--square { aspect-ratio: 1 / 1; }
.chart__legend {
  display: flex; flex-wrap: wrap; gap: var(--s-md);
  font-family: var(--font-sans); font-size: var(--t-label-sm-size); color: var(--fg-secondary);
}
.chart__legend > span { display: inline-flex; align-items: center; gap: 6px; }
.chart__legend i {
  width: 10px; height: 2px; background: currentColor; border-radius: 1px;
  display: inline-block;
}
.chart__legend i.dot { width: 8px; height: 8px; border-radius: 50%; }

/* Series swatches — reachable as utilities and inline SVG fills */
.series-1 { color: var(--series-1); fill: var(--series-1); stroke: var(--series-1); }
.series-2 { color: var(--series-2); fill: var(--series-2); stroke: var(--series-2); }
.series-3 { color: var(--series-3); fill: var(--series-3); stroke: var(--series-3); }
.series-4 { color: var(--series-4); fill: var(--series-4); stroke: var(--series-4); }
.series-5 { color: var(--series-5); fill: var(--series-5); stroke: var(--series-5); }

/* Sparkline — small inline trend, no axis. Used in tables and stat cards. */
.sparkline {
  display: inline-block; vertical-align: middle;
  width: 80px; height: 22px;
}
.sparkline path.line { fill: none; stroke: var(--series-1); stroke-width: 1.5; }
.sparkline path.area { fill: var(--series-1); opacity: .14; stroke: none; }
.sparkline circle.last-point { fill: var(--series-1); }

/* Sparkbar — micro bar chart, same footprint as sparkline */
.sparkbars { display: inline-flex; align-items: flex-end; gap: 2px; height: 22px; vertical-align: middle; }
.sparkbars > span { width: 4px; background: var(--series-1); border-radius: 1px 1px 0 0; opacity: .9; }

/* Annotation tag (for editorial mode) — text label + leader line */
.chart-annotation {
  font-family: var(--font-sans); font-size: var(--t-label-xs-size); line-height: 1.3;
  fill: var(--fg-secondary); font-weight: 500;
}
.chart-annotation.strong { fill: var(--fg); font-weight: 700; }
.chart-leader { stroke: var(--fg-secondary); stroke-width: 1; stroke-dasharray: 2 2; fill: none; }

/* Gauge — percentage indicator in a half-ring */
.gauge { width: 120px; aspect-ratio: 2 / 1.1; display: block; }
.gauge .track { fill: none; stroke: var(--bg-inactive); stroke-width: 12; stroke-linecap: round; }
.gauge .fill  { fill: none; stroke: var(--series-1); stroke-width: 12; stroke-linecap: round;
                transition: stroke-dashoffset var(--dur-max) var(--ease); }

/* ═══════════════════════════════════════════════════════════════════════
   DIAGRAMS · primitives for hand-drawn SVG
   Use as inline classes on SVG <rect>, <path>, <text> for boxes-and-arrows.
   Mermaid + React Flow override the same tokens via their respective theming.
   ═══════════════════════════════════════════════════════════════════════ */
:root {
  --dia-node-bg:     var(--bg-raised);
  --dia-node-fg:     var(--fg);
  --dia-node-border: var(--border);
  --dia-edge:        var(--fg-secondary);
  --dia-edge-strong: var(--fg);
  --dia-rail:        var(--border-lifted);
}
.dia-node {
  fill: var(--dia-node-bg);
  stroke: var(--dia-node-border);
  stroke-width: 1;
}
.dia-node--accent { stroke: var(--accent); stroke-width: 1.5; }
.dia-node--ghost  { fill: transparent; stroke-dasharray: 4 3; }
.dia-node-label { fill: var(--dia-node-fg); font-family: var(--font-mono); font-size: var(--t-label-sm-size); font-weight: 500; }
.dia-node-meta  { fill: var(--fg-secondary); font-family: var(--font-mono); font-size: 10px; /* tuned */ }

.dia-edge {
  stroke: var(--dia-edge); stroke-width: 1.25; fill: none;
}
.dia-edge--strong { stroke: var(--dia-edge-strong); stroke-width: 1.5; }
.dia-edge--dashed { stroke-dasharray: 4 3; }
.dia-edge-label   { fill: var(--fg-secondary); font-family: var(--font-mono); font-size: 10px; /* tuned */
                    paint-order: stroke; stroke: var(--bg); stroke-width: 4; }

.dia-rail { stroke: var(--dia-rail); stroke-width: 1; stroke-dasharray: 2 4; }
.dia-swimlane-label {
  fill: var(--fg-secondary); font-family: var(--font-sans); font-size: var(--t-label-xs-size);
  letter-spacing: .08em; text-transform: uppercase; font-weight: 600;
}


/* ═══════════════════════════════════════════════════════════════════════
   NAVIGATION — the spine. Promoted v0.10.0 from orphan demos (.crumb was
   copied verbatim ~14x; .sidenav lived in composition.html). Core, not a
   carve: nav is load-bearing chrome.
   Four surfaces, distinct jobs:
     .crumb    — where am I (hierarchy / location)
     .sidenav  — what sections exist (between-surface spine)  [+ --rail, drawer]
     .appbar   — global chrome (brand · search · actions)
     .tabs     — switch the VIEW within one surface
   (For switching a PARAMETER of one view — time window, density — use the
    .timerange segmented control, not .tabs.)
   No new tokens: --s-* for gaps/padding, raw px only for fixed dimensions
   (bar height, rail width), exactly as .split-pane / .btn already do.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── Breadcrumb ── location in a hierarchy. Inline; last item is current. */
.crumb {
  font-family: var(--font-sans); font-size: var(--t-label-sm-size);
  letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--fg-secondary); margin: 0 0 var(--s-md);
}
.crumb a { color: var(--fg-secondary); text-decoration: none; }
.crumb a:hover, .crumb a:focus-visible { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
.crumb [aria-current="page"] { color: var(--fg); }
.crumb__sep { color: var(--fg-disabled); margin: 0 0.4em; }

/* ── Sidebar nav ── the between-surface spine. Sections of one product. */
.sidenav {
  display: flex; flex-direction: column; gap: 2px;
  padding: var(--s-md);
  background: var(--bg-raised); border: 1px solid var(--border);
  border-radius: var(--radius-md);
}
/* Group label. Hierarchy at scale (#94): a long spine of uniform 44px rows
   reads flat with only the active row as an anchor, so the header is made to
   work harder — STICKY, so the current section's label pins as the list scrolls
   ("where am I" without reading every row), and a DIVIDER above every group
   after the first, so the chunk boundaries are legible even unscrolled. Short,
   non-scrolling navs are unaffected (nothing pins, the divider just separates). */
.sidenav__group {
  font-family: var(--font-sans); font-size: var(--t-label-sm-size);
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--fg-secondary);
  padding: var(--s-sm) var(--s-sm) var(--s-xs);
  position: sticky; top: 0; z-index: var(--z-raised);
  background: var(--bg-raised);   /* opaque over rows scrolling beneath; matches .sidenav surface */
}
.sidenav__group:not(:first-child) {
  margin-top: var(--s-sm); padding-top: var(--s-md);
  border-top: 1px solid var(--border);
}
/* .sidenav supports both <a> (document nav) and <button> (SPA section-switch).
   Selectors mirror .tabs which already uses "button, a" for the same reason.
   Note: a closed .nav-drawer should carry the `inert` attribute to remove its
   buttons from the tab order — there's no CSS-only way to prevent phantom tabs. */
.sidenav a,
.sidenav button {
  display: flex; align-items: center; gap: var(--s-sm);
  min-height: 44px; padding: 6px 10px;   /* 44px nav touch target (a11y #8) */
  border-radius: var(--radius-sm);
  color: var(--fg-secondary); font-family: var(--font-sans);
  font-size: var(--t-label-md-size); text-decoration: none;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
/* Button reset — bring <button> in line with the <a> presentation. */
.sidenav button {
  appearance: none; background: none; border: 0;
  width: 100%; text-align: left; font: inherit; cursor: pointer;
}
.sidenav a > .label,
.sidenav button > .label { flex: 1; min-width: 0; }
.sidenav a > [data-icon], .sidenav a > .i,
.sidenav button > [data-icon], .sidenav button > .i { flex: none; }
.sidenav a > .count,
.sidenav button > .count {
  margin-left: auto; font-variant-numeric: tabular-nums;
  font-size: var(--t-label-sm-size); color: var(--fg-disabled);
}
.sidenav a:hover,
.sidenav button:hover { background: var(--bg); color: var(--fg); }
.sidenav a:focus-visible,
.sidenav button:focus-visible { outline: var(--focus-width) solid var(--accent); outline-offset: var(--focus-offset-inset); }
.sidenav a[aria-current="page"],
.sidenav button[aria-current="page"] {
  background: var(--bg); color: var(--accent);
  box-shadow: inset 2px 0 0 var(--accent);   /* active rail — no layout shift */
}

/* Rail — collapsed, icon-only (~56px). Labels go SR-only, not display:none,
   so the link keeps an accessible name. Counts → a dot. */
.sidenav--rail { width: 56px; padding: var(--s-sm) 6px; }
.sidenav--rail .sidenav__group { display: none; }
.sidenav--rail a,
.sidenav--rail button { justify-content: center; gap: 0; padding: 0; width: 44px; margin: 0 auto; }
.sidenav--rail a > .label,
.sidenav--rail button > .label {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}
.sidenav--rail a > .count,
.sidenav--rail button > .count {
  margin: 0; position: absolute; top: 6px; right: 6px;
  width: 6px; height: 6px; padding: 0; border-radius: 50%;
  background: var(--accent); color: transparent; overflow: hidden;
}

/* Drawer — off-canvas sidenav for mobile. Toggle [data-nav-open] on a
   shared ancestor (or <body>); a trigger sets aria-expanded + aria-controls.
   Slide = modal-entry motion (pattern #05); collapses under reduced-motion. */
.nav-scrim {
  position: fixed; inset: 0; z-index: var(--z-modal);
  background: rgba(0,0,0,.55);
  opacity: 0; visibility: hidden;
  transition: opacity var(--dur-fast) var(--ease), visibility var(--dur-fast) var(--ease);
}
:root[data-theme="light"] .nav-scrim { background: rgba(13,27,42,.4); }
.nav-drawer {
  position: fixed; top: 0; bottom: 0; left: 0;
  z-index: calc(var(--z-modal) + 1);
  width: min(280px, 80vw); overflow-y: auto;
  transform: translateX(-100%);
  transition: transform var(--dur-max) var(--ease);
}
.nav-drawer > .sidenav { height: 100%; border: none; border-radius: 0; }
[data-nav-open] .nav-scrim  { opacity: 1; visibility: visible; }
[data-nav-open] .nav-drawer { transform: none; }

/* ── App bar ── global top chrome. Brand · (search) · spacer · actions.
   At most one .btn--primary here (one-CTA rule). The brand wordmark is a
   sanctioned .whimsy home; nothing else in the bar is. */
.appbar {
  display: flex; align-items: center; gap: var(--s-md);
  height: 56px; padding: 0 var(--s-md);
  background: var(--bg); border-bottom: 1px solid var(--border);
  position: sticky; top: 0; z-index: var(--z-overlay);
}
.appbar__brand {
  display: flex; align-items: center; gap: var(--s-sm);
  flex: 1; min-width: 0; /* allow brand to shrink on narrow viewports; without
    min-width:0 a flex child cannot shrink below min-content, which causes long
    wordmarks to push the appbar past the viewport edge (#127) */
  overflow: hidden; /* clip any brand text that exceeds the shrunk container */
  font-family: var(--font-mono); font-weight: 700;
  font-size: var(--t-body-lg-size); letter-spacing: -0.01em;
  color: var(--fg); text-decoration: none;
}
.appbar__search { flex: 0 1 360px; min-width: 0; }
.appbar__spacer { flex: 1; }
.appbar__actions { display: flex; align-items: center; gap: var(--s-xs); }
.appbar__menu-btn { display: none; }
@media (max-width: 800px) {
  .appbar__menu-btn { display: inline-flex; }
  .appbar__search   { display: none; }
}
/* Contained — inside a padded .container the bar's own inline padding
   double-insets; zero it so the container owns the gutter exclusively.
   The safe-area block below still sets max(0, env(safe-area-inset-*))
   so notch clearance is preserved. */
.appbar--contained { padding-inline: 0; }
/* Static — opts out of sticky positioning for compact tool surfaces or
   layouts where a second sticky plane (sidenav) is already the anchor. */
.appbar--static { position: static; top: auto; }

/* ── Tabs ── switch the VIEW within one surface. role=tablist (button +
   aria-selected) or links (aria-current). Underline marks the active view.
   NOT for filtering one view's parameters — that's .timerange. */
.tabs {
  display: flex; flex-wrap: nowrap; gap: var(--s-md); align-items: stretch;
  overflow-x: auto;
  /* Thin, token-colored scrollbar — hidden until tabs overflow the container. */
  scrollbar-width: thin; scrollbar-color: var(--border) transparent;
  border-bottom: 1px solid var(--border);
}
.tabs button, .tabs a {
  appearance: none; background: none; border: none; cursor: pointer;
  display: inline-flex; align-items: center; min-height: 44px;
  padding: var(--s-sm) 2px; margin-bottom: -1px; flex-shrink: 0;
  font-family: var(--font-sans); font-size: var(--t-label-md-size); font-weight: 500;
  color: var(--fg-secondary); text-decoration: none;
  border-bottom: 2px solid transparent;
  transition: color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.tabs button:hover, .tabs a:hover { color: var(--fg); }
.tabs [aria-selected="true"], .tabs [aria-current="page"] {
  color: var(--accent); border-bottom-color: var(--accent);
}
/* Inset focus ring (--focus-offset-inset: -2px) — the correct token for tabs
   (see token cheatsheet "nav items, table rows, tabs"). Drawn inside the
   button's border-box so it is never clipped by the overflow-x: auto scroll
   container. Matches the pattern used by .sidenav and .table rows. */
.tabs button:focus-visible, .tabs a:focus-visible { outline: var(--focus-width) solid var(--accent); outline-offset: var(--focus-offset-inset); }


/* ═══════════════════════════════════════════════════════════════════════
   BRAND · wordmark — the mark is lowercase "artificer" closed by a
   burnished full stop (the accent period). Mono, tight tracking. The stop
   is the signature; it also caps display page titles ("Navigation.").
   Override the stop's color per surface if needed; never stretch the
   mark's tracking or set it in sans. v0.10.0.
   ═══════════════════════════════════════════════════════════════════════ */
.wordmark {
  display: inline-block; width: max-content; /* guard: keeps ::after inline,
    not a separate flex item when .wordmark is placed on a flex container */
  font-family: var(--font-mono); font-weight: 700;
  text-transform: lowercase; letter-spacing: -0.01em;
  color: var(--fg); text-decoration: none; white-space: nowrap;
}
.wordmark::after { content: "."; color: var(--accent); }
/* Period-bearing marks (e.g. "blog.") add the stop as real text and should
   suppress the pseudo to avoid a double-stop. `.whimsy` in artificer-whimsy.css
   also checks this modifier before re-establishing the gradient on ::after. */
.wordmark--stop-none::after { content: none; }


/* ═══════════════════════════════════════════════════════════════════════
   RESPONSIVE — breakpoints · touch escalation · safe-area · table reflow.
   v0.10.0. Reconciles Lane 3's "mobile layer" audit, with two corrections:
     1. The 44px target is a NAV rule (a11y #8: "in nav"), NOT a blanket
        floor. Desktop controls stay dense by design; they grow only on a
        finger — gated to @media (pointer: coarse), so a mouse desktop is
        untouched.
     2. CSS @media can't read var(--bp-*). The --bp-* tokens (and tokens.json)
        are the source of truth for JS / Tailwind / Figma; the literal @media
        widths below MUST be kept equal to them.
   ═══════════════════════════════════════════════════════════════════════ */

:root {
  --bp-mobile: 640px;    /* phone */
  --bp-tablet: 800px;    /* small tablet / split view */
  --bp-wide:  1200px;    /* wide desktop */
}

/* Safe-area — sticky/fixed chrome clears the notch + home indicator.
   Requires <meta name="viewport" content="..., viewport-fit=cover">. */
.appbar {
  padding-left:  max(var(--s-md), env(safe-area-inset-left));
  padding-right: max(var(--s-md), env(safe-area-inset-right));
}
/* .appbar--contained zeroes the inline padding so a padded container owns the
   gutter. Safe-area: use max(0px, inset) so we still clear the notch edge when
   the container is also full-bleed on a notched device (rare but safe). */
.appbar--contained {
  padding-left:  max(0px, env(safe-area-inset-left));
  padding-right: max(0px, env(safe-area-inset-right));
}
.nav-drawer { padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom); }

/* Touch escalation — on coarse pointers (finger) interactive controls grow
   to the 44×44px target (WCAG 2.5.5/2.5.8 — both dimensions).
   Fine-pointer (mouse) desktops keep Artificer density. */
@media (pointer: coarse) {
  /* Every button is a touch target, classed or not — demo buttons, page-local
     buttons, and library buttons all get the floor. Both dimensions per WCAG 2.5.5. */
  button, .btn, .theme-toggle { min-height: 44px; min-width: 44px; }
  /* All text-like inputs (text/search/email/number/password/date/url/tel...).
     Width left to the layout (inputs fill their container; min-width here
     would fight flex/grid shrink and break compact search bars). */
  .input, .textarea, .select,
  input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="file"]),
  textarea { min-height: 44px; }
  .checkbox, .radio { min-height: 44px; min-width: 44px; align-items: center; }
  .checkbox > input, .radio > input { width: 22px; height: 22px; }
  .checkbox > input:checked::after { width: 5px; height: 10px; margin-top: 2px; }
  .radio > input:checked::after { width: 8px; height: 8px; margin-top: 6px; }
  .toggle { min-height: 44px; min-width: 44px; align-items: center; }
  .toggle > input { width: 44px; height: 26px; }
  .toggle > input::after { width: 22px; height: 22px; }
  .toggle > input:checked::after { transform: translateX(18px); }
  .slider { height: 6px; }
  .slider::-webkit-slider-thumb { width: 24px; height: 24px; }
  .slider::-moz-range-thumb { width: 24px; height: 24px; }
  .chip { min-height: 44px; min-width: 44px; }
  /* Nav targets are 44px always (a11y rule #8) — brand link + breadcrumbs get a
     full-height touch area without changing their visual size. */
  .appbar__brand { min-height: 44px; min-width: 44px; display: inline-flex; align-items: center; }
  .crumb a { min-height: 44px; min-width: 44px; display: inline-flex; align-items: center; }
}

/* Table -> cards reflow. Below --bp-mobile (640px) a wide data table
   restacks: each row becomes a card, each cell labelled by its column.
   Opt-in: add .table--responsive and a data-label on every <td>. */
@media (max-width: 640px) {
  .table--responsive thead {
    position: absolute; width: 1px; height: 1px;
    overflow: hidden; clip: rect(0,0,0,0);
  }
  .table--responsive tbody tr {
    display: block; margin-bottom: var(--s-md);
    border: 1px solid var(--border); border-radius: var(--radius-md);
    padding: var(--s-sm) var(--s-md);
  }
  .table--responsive tbody td {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--s-md); border: none; padding: 6px 0; text-align: right;
  }
  .table--responsive tbody td::before {
    content: attr(data-label); text-align: left;
    font-family: var(--font-sans); font-size: var(--t-label-sm-size);
    letter-spacing: 0.04em; text-transform: uppercase; color: var(--fg-secondary);
  }
}


/* ═══════════════════════════════════════════════════════════════════════
   RESPONSIVE LAYER — wide-screen tier (#111, Lane 1 ruling) + app-shell (#116).
   One spine: the ONLY thing that caps width is the reading measure. Documents
   cap at --editorial-measure (66ch); tools fill; the middle is intrinsic
   (auto-fit/minmax). --bp-wide is the system's single MIN-WIDTH breakpoint
   (reveal a structural region), NOT a content cap. No 100vw anywhere —
   scrollbar-safe by construction. (token-mirror rule: literal @media widths
   below are kept equal to --bp-tablet 800 / --bp-wide 1200 in tokens.json.)
   ═══════════════════════════════════════════════════════════════════════ */

/* Bleed-track grids. Two contexts share one mechanism so .full-bleed is ONE
   class: a centered/left content column flanked by gutter tracks plus named
   [bleed-*] lines a child can span to reach the surface edge — no negative
   margins, no 100vw.
     .measure-grid     document — content column capped at 66ch (#111)
     .bleed-grid       tool     — content column minmax(0,1fr), uncapped (#116)
     .app-shell__content         the app body, a bleed-grid by default          */
.measure-grid {
  display: grid;
  grid-template-columns:
    [bleed-start] minmax(var(--s-lg), 1fr)
    [content-start measure-start] minmax(0, var(--editorial-measure, 66ch)) [measure-end content-end]
    minmax(var(--s-lg), 1fr) [bleed-end];
}
.bleed-grid,
.app-shell__content {
  display: grid;
  align-content: start; row-gap: var(--s-lg); padding-block: var(--s-lg);
  grid-template-columns:
    [bleed-start] var(--s-lg)
    [content-start] minmax(0, 1fr) [content-end]
    var(--s-lg) [bleed-end];
}
.measure-grid > *,
.bleed-grid > *,
.app-shell__content > * { grid-column: content-start / content-end; }
/* Break out to the surface edge — works in any bleed-track grid above; a no-op
   (grid-column ignored) in a non-grid parent, so it can't misfire. */
.full-bleed { grid-column: bleed-start / bleed-end; }

/* Container query opt-in for NEW components that restack on their OWN width
   (not the viewport). Forward-only; existing @media-stacked primitives unchanged. */
.cq { container-type: inline-size; }

/* Inspector split — master/detail that reveals a third (detail) pane only on a
   genuinely wide viewport (the single min-width step, == --bp-wide). Content
   track is minmax(0,1fr), never bare 1fr — overflow-safe (#116). */
.split-pane--inspector { display: grid; gap: var(--s-lg); grid-template-columns: 280px minmax(0, 1fr); }
/* The inspector (third) pane exists only at the wide tier — below it, the pane
   is hidden, NOT re-gridded into an implicit row. "Reveals at --bp-wide" means
   exactly that. (Caught by regressions/wide-screen.spec.mjs.) */
.split-pane--inspector > :nth-child(3) { display: none; }
@media (max-width: 800px)  { .split-pane--inspector { grid-template-columns: 1fr; } }
@media (min-width: 1200px) {
  .split-pane--inspector { grid-template-columns: 280px minmax(0, 1fr) 320px; }
  .split-pane--inspector > :nth-child(3) { display: block; }
}

/* App-shell — the responsive page scaffold (#116). Composes the existing chrome
   (.appbar / .sidenav / .nav-drawer) into a page that survives a phone: a
   sidebar+content grid whose content track is overflow-safe, collapsing the
   sidebar to the off-canvas .nav-drawer at --bp-tablet. Tools fill; nothing here
   caps width — that's the reading measure's job. */
.app-shell {
  display: grid;
  min-height: 100dvh;
  grid-template-rows: [bar] auto [body] 1fr;
  grid-template-columns: [rail] var(--shell-rail, 240px) [main-start] minmax(0, 1fr) [main-end];
}
.app-shell > .appbar  { grid-row: bar;  grid-column: 1 / -1; }
.app-shell > .sidenav { grid-row: body; grid-column: rail; }
.app-shell__content {
  grid-row: body; grid-column: main-start / main-end;
  min-width: 0;        /* belt-and-suspenders with the minmax(0,1fr) main track */
  overflow-x: clip;    /* a rogue wide child clips here, never scrolls the page */
}
@media (max-width: 800px) {
  .app-shell { grid-template-columns: [main-start] minmax(0, 1fr) [main-end]; }
  .app-shell > .sidenav { display: none; }   /* the .nav-drawer takes over */
}

/* Media fit — opt-in: shrink an image/diagram to its container instead of
   forcing sideways scroll. Scoped to .media-fit (NOT a blanket svg rule, which
   would distort the fixed-size chrome icons). Wraps media or sits on it. */
.media-fit :where(img, svg, video, canvas),
:where(img, svg, video, canvas).media-fit { max-width: 100%; height: auto; }


/* ═══════════════════════════════════════════════════════════════════════
   PRIMITIVES (v0.10.0) — avatar, accordion, selectable rows, date/time.
   From Lane 3's "missing primitives" audit. Built the small/proven ones;
   the floating-list family (combobox/menu/palette/select) is deferred and
   UNIFIED onto one option-popover primitive. Token-pure.
   ═══════════════════════════════════════════════════════════════════════ */

/* Avatar — image or initials. .dot is an 8px status; .badge is a pill;
   this is the 24-64px person/entity mark. Default 32px, circular. */
.avatar {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; flex: none; overflow: hidden;
  border-radius: 50%; border: 1px solid var(--border);
  background: var(--bg-raised); color: var(--fg-secondary);
  font-family: var(--font-mono); font-weight: 700;
  font-size: var(--t-label-sm-size); text-transform: uppercase;
  letter-spacing: 0.02em; user-select: none;
}
.avatar > img { width: 100%; height: 100%; object-fit: cover; }
.avatar--sm     { width: 24px; height: 24px; font-size: 10px; /* tuned */ }
.avatar--lg     { width: 48px; height: 48px; font-size: var(--t-label-md-size); }
.avatar--xl     { width: 64px; height: 64px; font-size: var(--t-body-lg-size); }
.avatar--square { border-radius: var(--radius-sm); }

/* Accordion — built on native <details>/<summary>: accessible + keyboard
   for free, no JS. Wrap <details> items in .accordion; body in .accordion__body. */
.accordion { border: 1px solid var(--border); border-radius: var(--radius-md); overflow: hidden; }
.accordion > details { border-bottom: 1px solid var(--border); }
.accordion > details:last-child { border-bottom: none; }
.accordion summary {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s-md);
  min-height: 44px; padding: var(--s-sm) var(--s-md); cursor: pointer; list-style: none;
  font-family: var(--font-sans); font-weight: 500; color: var(--fg);
  transition: background var(--dur-fast) var(--ease);
}
.accordion summary::-webkit-details-marker { display: none; }
.accordion summary::after {
  content: ""; flex: none; width: 7px; height: 7px;
  border-right: 1.5px solid var(--fg-secondary); border-bottom: 1.5px solid var(--fg-secondary);
  transform: rotate(45deg); transition: transform var(--dur-fast) var(--ease);
}
.accordion details[open] > summary::after { transform: rotate(-135deg); }
.accordion summary:hover { background: var(--bg-raised); }
.accordion summary:focus-visible { outline: var(--focus-width) solid var(--accent); outline-offset: var(--focus-offset-inset); }
.accordion__body { padding: 0 var(--s-md) var(--s-md); color: var(--fg-secondary); }

/* Selectable table rows — when rows are interactive (tabindex / role="row"),
   they must be keyboard-reachable + show focus. (No effect on plain rows.) */
.table tbody tr:focus-visible {
  background: var(--bg-raised);
  outline: var(--focus-width) solid var(--accent); outline-offset: var(--focus-offset-inset);
}

/* Date / time fields — take .input like any field; keep mono numerals and a
   tappable picker icon. The native picker popup stays browser-default. */
.input[type="date"], .input[type="time"], .input[type="datetime-local"] {
  font-family: var(--font-mono); font-variant-numeric: tabular-nums;
}
.input::-webkit-calendar-picker-indicator { cursor: pointer; opacity: 0.6; }
.input::-webkit-calendar-picker-indicator:hover { opacity: 1; }

/* ═══════════════════════════════════════════════════════════════════════
   v0.15.0 · component mints (Workstream E + D)
   Adopted from Lane 1's v0.15.0 round, cherry-picked onto current repo CSS
   (the sandbox base lagged the repo — additive only, never wholesale).
   All mints, zero new tokens, zero hardcoded hex.
   ═══════════════════════════════════════════════════════════════════════ */

/* ═══ Option-popover — .menu (actions) / .listbox (selection) ═══════════
   The ONE floating option list. Don't hand-roll a dropdown. Two roles share
   one look:
     .menu     = a command/action list   → role="menu",    children role="menuitem"
     .listbox  = a selectable list        → role="listbox", children role="option" + aria-selected
   Both use __option rows. Drop inside .popover (or .palette) for the floating
   surface. Keyboard + ARIA ride artificer-options.js (enhance = roving
   tabindex on a standalone list; combobox = aria-activedescendant for the
   palette recipe); open/close stays consumer-side. Composes into: command
   palette, enhanced select, combobox, multiselect, tag-input.
   v0.15.0 (Workstream E keystone). */
.menu, .listbox {
  list-style: none; margin: 0; padding: var(--s-xs);
  display: flex; flex-direction: column; gap: 2px;
  font-family: var(--font-sans); font-size: var(--t-label-md-size);
  max-height: 320px; overflow-y: auto;
}
.menu__option, .listbox__option {
  display: flex; align-items: center; gap: var(--s-sm);
  padding: var(--s-sm) 10px; border-radius: var(--radius-sm);
  color: var(--fg); cursor: pointer; white-space: nowrap;
  border: none; background: transparent; width: 100%; text-align: left;
  font: inherit;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
/* hover AND the roving keyboard cursor are one visual — .is-active is the cursor */
.menu__option:hover, .listbox__option:hover,
.menu__option.is-active, .listbox__option.is-active { background: var(--bg-raised); }
.menu__option:focus-visible, .listbox__option:focus-visible {
  outline: var(--focus-width) solid var(--accent); outline-offset: var(--focus-offset-inset);
}
.listbox__option[aria-selected="true"] { color: var(--accent); }
.listbox__option[aria-selected="true"]::after { content: "✓"; margin-left: auto; color: var(--accent); }
.menu__option[aria-disabled="true"], .listbox__option[aria-disabled="true"] { color: var(--fg-disabled); cursor: not-allowed; }
.menu__option[aria-disabled="true"]:hover, .listbox__option[aria-disabled="true"]:hover { background: transparent; }
.menu__option--danger, .listbox__option--danger { color: var(--urgent); }
.menu__option--danger:hover, .menu__option--danger.is-active,
.listbox__option--danger:hover, .listbox__option--danger.is-active { background: color-mix(in oklch, var(--urgent) 14%, transparent); }
.menu__option > .icon, .listbox__option > .icon { color: var(--fg-secondary); flex: none; }
.menu__hint, .listbox__hint { margin-left: auto; color: var(--fg-secondary); font-family: var(--font-mono); font-size: var(--t-label-xs-size); }
.menu__label, .listbox__label {
  padding: 6px 10px 2px; font-size: var(--t-label-xs-size); letter-spacing: .08em; text-transform: uppercase;
  color: var(--fg-secondary); font-weight: 500; font-family: var(--font-sans);
}
.menu__sep, .listbox__sep { height: 1px; background: var(--border); margin: var(--s-xs) 0; border: none; }

/* ═══ Command palette — the signature tool surface (recipe) ═════════════
   A centered overlay = .palette__search header + a .listbox body, focus-trapped
   (ArtificerFocus.trap, Esc to close) on a .scrim. Opened on ⌘K. Surface is
   --bg-overlay (the token reserved for "modals, command palette"). The body
   is just the .listbox above — palette is composition, not a new primitive.
   Why .listbox, not .menu: the search input is a combobox, and ARIA permits a
   combobox popup of listbox / tree / grid / dialog — never menu (#174). */
.palette {
  position: fixed; top: 12vh; left: 50%; transform: translateX(-50%);
  width: min(560px, 92vw); z-index: var(--z-modal);
  background: var(--bg-overlay); border: 1px solid var(--border);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-overlay);
  overflow: hidden; animation: palette-in var(--dur-fast) var(--ease);
}
@keyframes palette-in { from { opacity: 0; transform: translate(-50%, 8px); } to { opacity: 1; transform: translate(-50%, 0); } }
@media (prefers-reduced-motion: reduce) { .palette { animation: none; } }
.palette__search { display: flex; align-items: center; gap: var(--s-sm); padding: 2px 14px; border-bottom: 1px solid var(--border); }
.palette__search > .icon { color: var(--fg-secondary); flex: none; }
/* compensating focus ring (#175): the row's existing 1px bottom border flips to
   accent — the .input border-color-flip model, sized to the composed control */
.palette__search:focus-within { border-bottom-color: var(--accent); }
.palette__search > input {
  flex: 1; background: transparent; border: none; outline: none; padding: 13px 0;
  color: var(--fg); font-family: var(--font-mono); font-size: var(--t-body-lg-size);
}
.palette__search > input::placeholder { color: var(--fg-disabled); }
/* The bare input outline is suppressed AND compensated (#175): the
   .palette__search:focus-within border-flip above carries the visible focus
   signal for the composed control (out-specifies input:focus-visible). */
.palette__search > input:focus-visible { outline: none; }
.palette .menu, .palette .listbox { max-height: 52vh; padding: var(--s-sm); }
.palette__empty { padding: var(--s-lg); color: var(--fg-secondary); font-family: var(--font-mono); font-size: var(--t-body-md-size); }

/* ═══ Toast region — transient .notif placement layer ══════════════════
   The fixed, corner-anchored, stacking host where .notif cards appear
   TRANSIENTLY. Consumes --z-toast (the reserved top rung). The .notif is
   the atom; this is only WHERE it appears. ARIA is the CONSUMER'S job at
   insert time — CSS cannot set roles: urgent → role="alert" (assertive),
   attention/info → role="status" (polite), background → NO role (silent
   badge, never announced — it never enters the region). Lifecycle is
   consumer-owned JS too: info auto-dismisses (5s, pause on hover/focus);
   urgent + attention persist until acted on. Cap: 3 visible — overflow
   collapses to .toast-region__more ("+N more"), which routes to
   notification history; info is the only evictable tier.
   Spec: the Lane 1 toast-region ruling (workshop). Live demo with
   the correct role wiring: notifications.html. v0.15.0, contract issue 177. */
.toast-region {
  position: fixed; z-index: var(--z-toast);
  display: flex; flex-direction: column; gap: var(--s-sm);
  width: min(380px, calc(100vw - 2 * var(--s-lg)));
  pointer-events: none;                       /* gaps don't eat clicks… */
  /* default: bottom-right, clearing the notch (responsive doctrine) */
  inset: auto max(var(--s-lg), env(safe-area-inset-right))
         max(var(--s-lg), env(safe-area-inset-bottom)) auto;
}
.toast-region > * { pointer-events: auto; }   /* …the notifs do */
.toast-region > .notif { box-shadow: var(--shadow-popover); }

.toast-region--top-right {
  inset: max(var(--s-lg), env(safe-area-inset-top))
         max(var(--s-lg), env(safe-area-inset-right)) auto auto;
}
.toast-region--top-left {
  inset: max(var(--s-lg), env(safe-area-inset-top)) auto auto
         max(var(--s-lg), env(safe-area-inset-left));
}
.toast-region--bottom-left {
  inset: auto auto max(var(--s-lg), env(safe-area-inset-bottom))
         max(var(--s-lg), env(safe-area-inset-left));
}
.toast-region--top-center,
.toast-region--bottom-center { left: 50%; transform: translateX(-50%); }
.toast-region--top-center    { top: max(var(--s-lg), env(safe-area-inset-top)); bottom: auto; }
.toast-region--bottom-center { bottom: max(var(--s-lg), env(safe-area-inset-bottom)); top: auto; }

/* entry = modal-entry motion (pattern #05): slide 8px + fade, ≤160ms */
.toast-region > .notif { animation: toast-in var(--dur-fast) var(--ease); }
@keyframes toast-in { from { opacity: 0; transform: translateY(8px) } to { opacity: 1; transform: none } }

/* overflow affordance — "+N more" routes to notification history */
.toast-region__more {
  pointer-events: auto; align-self: center;
  font-family: var(--font-sans); font-size: var(--t-label-sm-size); font-weight: 500;
  color: var(--fg-secondary); background: var(--bg-raised);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: var(--s-xs) 10px; cursor: pointer;
  transition: color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.toast-region__more:hover { color: var(--fg); border-color: var(--accent); }

@media (prefers-reduced-motion: reduce) {
  .toast-region > .notif { animation: none; }
}

/* ═══ Pagination — jump within a counted range ═════════════════════════
   For ranges you can count and jump; disable at the ends. For unbounded sets
   prefer "load more" or cursor paging. v0.15.0. */
.pagination { display: flex; align-items: center; gap: var(--s-xs); font-family: var(--font-mono); }
.pagination button, .pagination a {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 32px; height: 32px; padding: 0 var(--s-sm);
  background: transparent; border: 1px solid transparent; border-radius: var(--radius-sm);
  color: var(--fg-secondary); font: inherit; font-size: var(--t-label-md-size);
  text-decoration: none; cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.pagination button:hover, .pagination a:hover { color: var(--fg); background: var(--bg-raised); }
.pagination button:focus-visible, .pagination a:focus-visible { outline: var(--focus-width) solid var(--accent); outline-offset: var(--focus-offset); }
.pagination [aria-current="page"] { color: var(--accent); border-color: var(--accent); }
.pagination button[disabled] { color: var(--fg-disabled); cursor: not-allowed; background: transparent; }
.pagination__gap { color: var(--fg-disabled); padding: 0 var(--s-xs); user-select: none; }   /* ellipsis between ranges */
/* Pagination is nav (a11y #8): on a coarse pointer the cells take the 44px touch
   floor. Declared here (after the 32px base) so it out-orders the generic button
   escalation, which the page-earlier `.pagination button` base would otherwise win. */
@media (pointer: coarse) {
  .pagination button, .pagination a { min-width: 44px; min-height: 44px; }
}

/* ═══ Banner — persistent, page-level inline notice ═════════════════════
   A full-width band pinned in the LAYOUT (top of a page/section), NOT the
   transient toast-tier .notif. Use for standing conditions: "Read-only —
   viewing a snapshot", "2 nodes degraded", a maintenance window. Persists
   until the condition clears (or the user dismisses, if dismissible). Tiers
   reuse the status quartet — COLOR encodes the tier; texture never does.
   DELIBERATE divergence from the toast tier (#189): banner--info is STEEL
   where notif--info is accent — a persistent structural state should read
   calmer than a transient toast; "--info" does not imply accent here.
   v0.15.0. */
.banner {
  display: flex; align-items: center; gap: var(--s-md);
  padding: 10px var(--s-lg);
  background: var(--bg-raised); border: 1px solid var(--border); border-left-width: 2px;
  font-family: var(--font-mono); font-size: var(--t-label-md-size); color: var(--fg); line-height: 1.5;
}
.banner__body { flex: 1; min-width: 0; }
.banner__actions { display: flex; align-items: center; gap: var(--s-sm); flex: none; }
.banner--info      { border-left-color: var(--steel);     background: color-mix(in oklch, var(--steel) 8%, var(--bg-raised)); }
.banner--attention { border-left-color: var(--attention); background: color-mix(in oklch, var(--attention) 8%, var(--bg-raised)); }
.banner--urgent    { border-left-color: var(--urgent);    background: color-mix(in oklch, var(--urgent) 8%, var(--bg-raised)); }
.banner--success   { border-left-color: var(--success);   background: color-mix(in oklch, var(--success) 8%, var(--bg-raised)); }

/* ═══ Tree view — nested disclosure (file explorer / nested nav) ════════
   For nested hierarchy that accordion (flat) and sidenav (one level) don't
   cover. role="tree" > role="treeitem"; nested children in role="group".
   Indent compounds via nested .tree__group. Twisty mirrors .accordion's
   chevron. artificer-tree.js ships expand/collapse + arrow-key roving focus
   (data-tree / ArtificerTree.enhance()). Leaves (.tree__leaf) hide the
   twisty but keep alignment. v0.15.0. */
.tree { list-style: none; margin: 0; padding: 0; font-family: var(--font-mono); font-size: var(--t-body-md-size); }
.tree__group { list-style: none; margin: 0; padding-left: var(--s-md); }   /* one indent step per level */
.tree__row {
  display: flex; align-items: center; gap: var(--s-sm);
  min-height: 28px; padding: 2px var(--s-sm); border-radius: var(--radius-sm);
  color: var(--fg-secondary); cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.tree__row:hover { background: var(--bg-raised); color: var(--fg); }
/* aria-expanded / aria-selected live on the parent [role="treeitem"] (APG tree
   pattern), so reach the row through it — keying off .tree__row[aria-*] matches
   nothing (the row is a roleless div, the attrs sit on the <li>). */
[role="treeitem"][aria-selected="true"] > .tree__row { background: var(--bg-raised); color: var(--accent); }
/* Focus rides the roving-tabindex HOST — the [role="treeitem"] li (APG tree) —
   not the roleless row div, which is never focused. Suppress the host's own
   ring (its box spans the whole subtree, children included) and draw it on the
   item's first row instead. Tie-break: the global [tabindex]:focus-visible
   opt-in earlier in this file is the same specificity (0,2,0) — this block
   wins only by source order. Do not reorder. (#173) */
[role="treeitem"]:focus-visible { outline: none; }
[role="treeitem"]:focus-visible > .tree__row {
  outline: var(--focus-width) solid var(--accent);
  outline-offset: var(--focus-offset-inset);
}
.tree__twisty {
  flex: none; width: 14px; height: 14px; display: inline-flex; align-items: center; justify-content: center;
  color: var(--fg-secondary); transition: transform var(--dur-fast) var(--ease);
}
.tree__twisty::before { content: ""; width: 6px; height: 6px; border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor; transform: rotate(-45deg); }
[role="treeitem"][aria-expanded="true"] > .tree__row > .tree__twisty { transform: rotate(90deg); }
.tree__row > .icon { flex: none; color: var(--fg-secondary); }
.tree__leaf > .tree__twisty { visibility: hidden; }   /* files have no twisty; keep alignment */

/* ═══ Stat card — a single KPI: label + big number + optional delta ═════
   The cell of a .kpi-strip. PROMOTED to core v0.15.0 (was demo-local). Delta
   keeps the .stat__delta(.down) naming; the change-direction × polarity rework
   (.delta--better/--worse) remains the separate staged value-polarity PR. */
.stat { display: flex; flex-direction: column; gap: var(--s-xs); padding: var(--s-md); background: var(--bg-raised); border-radius: var(--radius-md); border: 1px solid var(--border); }
.stat__label { font-family: var(--font-mono); font-size: var(--t-label-xs-size); letter-spacing: .08em; text-transform: uppercase; color: var(--fg-secondary); }
.stat__value { font-family: var(--font-mono); font-weight: 700; font-size: var(--t-headline-md-size); color: var(--fg); font-variant-numeric: tabular-nums; line-height: 1.1; }
.stat__row { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s-sm); }
.stat__delta { font-family: var(--font-mono); font-size: var(--t-label-xs-size); color: var(--success); }
.stat__delta.down { color: var(--urgent); }

/* ═══ Indeterminate-but-long progress rung (D2) ════════════════════════
   Deploy / slow query — long wait, nothing to count. NOT a bare indeterminate
   bar (that's a horizontal spinner; the voice rule refuses content-free
   "Loading…"). INSEPARABLE from concrete copy ("Deploying to us-east-1…",
   never "Deploying…"): always pair with a label. ARIA: role="progressbar" +
   aria-label carrying that concrete copy, and NO aria-valuenow/min/max —
   the missing value is what marks it indeterminate (demo: states.html).
   A contained burnished sweep (continuous translation, motion pattern
   #02 → --ease-linear). Zero new tokens. v0.15.0. */
.progress--indeterminate { position: relative; }
.progress--indeterminate .progress__bar {
  width: 35%; transition: none;
  animation: progress-sweep 1.6s var(--ease-linear) infinite;
}
@keyframes progress-sweep {
  from { transform: translateX(-120%); }
  to   { transform: translateX(320%); }
}
@media (prefers-reduced-motion: reduce) {
  /* motion suppressed → static low-opacity full track; the copy carries meaning */
  .progress--indeterminate .progress__bar { animation: none; width: 100%; opacity: .4; }
}
