/* ═══════════════════════════════════════════════════════════════════════════
   ARTIFICER · Design tokens · v0.24.2 (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 S / V — the two roles split by surface (see FONTS.md):
   S is interface chrome (labels, controls, tabular UI numerals), V is
   body prose. No bare 'iA Writer Quattro' face ships — it was a duplicate
   of S/V's own glyph set with no role of its own.
   All SIL OFL 1.1. Files live in src/assets/fonts/. See FONTS.md for the
   rationale and the Windows faux-bold disposition. */
@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;
}

/* iA Writer Quattro S — tabular-numeral variant. Interface chrome: labels,
   controls, badges, dense tabular UI numerals. 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).
   Body prose face. Only 400 weight ships from iA — a bold body run
   (headlines set on --font-body) synthesizes faux-bold in the browser,
   same as any face missing a weight. */
@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 */
  --urgent-text:     #e6a8a2;   /* lifted error TEXT (.field>.error) — bg 6.98 AA · v0.19.0 #122-B */
  --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 V — display/body variant. Body prose face: document
     surfaces, .meta, headline/body type utilities. Falls back to serif →
     system-ui. */
  --font-body:      'iA Writer Quattro V', 'Iowan Old Style', 'Charter',
                    'Source Sans 3', system-ui, -apple-system, sans-serif;
  /* iA Writer Quattro S — tabular-numeral variant. Interface chrome face:
     labels, controls, badges, form fields, nav, tabular UI numerals. */
  --font-interface: 'iA Writer Quattro S', 'Iowan Old Style', 'Charter',
                    'Source Sans 3', system-ui, -apple-system, sans-serif;
  /* --font-sans is a legacy alias kept resolvable for public-API back-compat —
     new rules use --font-body or --font-interface directly (see FONTS.md). */
  --font-sans:  var(--font-body);

  /* 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.24.2";

  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 */
  --urgent-text:     #8a2418;   /* = urgent · ivory 7.49 AAA · v0.19.0 #122-B */
  --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:       #20203e;   /* ink on gold-fill button — ivory→ink v0.19.0 #122-A · 5.65 AA */
  --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;
    --urgent-text:     #8a2418;
    --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:       #20203e;   /* ivory→ink v0.19.0 #122-A */
    --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-body);
  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 interface-faced, per the rule */
  font-family: var(--font-interface);
}

/* Document island — inverse of .surface-tool. Marks a prose region inside a
   tool surface that opts its body face back to --font-body (colophons, legal
   disclaimers, help prose). Sits after .surface-tool, so it wins at equal
   specificity on source order; class-based components (.stat, .table cells)
   self-declare mono and win there too. Bare-tag mono declarers (code, kbd)
   sit at lower specificity than the universal-descendant rule above and lose
   to it, so they're re-declared explicitly below. */
.surface-document, .surface-document * { font-family: var(--font-body); }
/* code/kbd are bare-tag selectors ((0,0,1)) and lose to .surface-document *
   ((0,1,0)) on specificity, not source order — re-declared here to hold mono. */
.surface-document code, .surface-document kbd { font-family: var(--font-mono); }

/* ═══ Type utilities ════════════════════════════════════════════════════
   Headlines and body run on --font-body (Quattro V) by default — document
   mode; label utilities run on --font-interface (Quattro S).
   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-body); 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-body); 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-body); font-weight: 400; font-size: var(--t-body-lg-size); line-height: var(--t-body-lg-lh); }
.t-body-md     { font-family: var(--font-body); font-weight: 400; font-size: var(--t-body-md-size); line-height: var(--t-body-md-lh); }
.t-label-md    { font-family: var(--font-interface); font-weight: 500; font-size: var(--t-label-md-size); line-height: var(--t-label-md-lh); }
.t-label-sm    { font-family: var(--font-interface); font-weight: 400; font-size: var(--t-label-sm-size); line-height: var(--t-label-sm-lh); }
.t-label-xs    { font-family: var(--font-interface); 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-body); 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);
  /* #122-E — document surface (default): prose links carry a persistent underline so
     a link in a text block is distinguished by more than color (WCAG 1.4.1). Chrome
     and components opt out below / via their own text-decoration:none. */
  text-decoration: underline;
  text-underline-offset: 2px;
  transition: color var(--dur-fast) var(--ease);
}
a:hover, a:focus-visible { color: var(--accent-bright); text-decoration: underline; text-underline-offset: 2px; }
/* Tool surfaces read links by position — underline only on hover/focus (the rule above). */
.surface-tool a { text-decoration: none; }
/* .btn--link follows `a` here rather than joining its selector: widening that
   one to :is(a, .btn--link) would lift it from (0,1,1) to (0,2,0) and could
   out-order a rule that deliberately re-underlines an anchor. */
.surface-tool .btn--link { text-decoration: none; }
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-interface); 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; text-decoration: none;
  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); }

/* An action that READS as text. <button> is the correct element when a control
   acts rather than navigates — an SPA switching views, a disclosure toggle,
   "show more" — and this renders it as the link it reads as, instead of
   dressing an <a> in a click handler and lying to the accessibility tree.
   Parity with <a> is by CONSTRUCTION where it matters: this class joins `a` in
   the two context rules (the tool-surface underline strip, the colophon's sans
   escape), so a later retune of either carries here instead of drifting.
   Sits after the .btn variants deliberately — it is (0,1,0) like .btn, so it
   wins on source order alone. v0.22.1. */
.btn--link {
  padding: 0; border: 0; border-radius: 0; background: none;
  font: inherit;                       /* undo .btn's face, size and weight */
  color: var(--accent);
  text-decoration: underline; text-underline-offset: 2px;
  white-space: normal;                 /* .btn's nowrap is wrong for inline text */
}
.btn--link:hover { color: var(--accent-bright); }

/* 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; }
/* A LONE primary icon control — mobile nav trigger, modal close — has no
   row of neighbors to borrow prominence from, so the ghost resting color
   (--fg-secondary) under-serves it (#114, two consumers independently).
   Rests at full --fg; in a cluster of controls, stay on the ghost default. */
.btn--icon-prominent { color: var(--fg); }

.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-interface); font-size: var(--t-label-md-size); font-weight: 500; color: var(--fg); }
.field > .hint  { font-family: var(--font-interface); font-size: var(--t-label-sm-size); color: var(--fg-secondary); }
.field > .error { font-family: var(--font-interface); font-size: var(--t-label-sm-size); color: var(--urgent-text); }

/* ═══ Badges ════════════════════════════════════════════════════════════ */
.badge {
  display: inline-flex; align-items: center; gap: var(--s-xs);
  padding: 2px var(--s-sm);
  font-family: var(--font-interface); 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); }

/* ═══ Cell glyphs ════════════════════════════════════════════════════════ */
/*
 * Themeable status marks for dense data cells — the anti-emoji. Emoji are
 * OS-rendered: they ignore the theme and read loud in a grid. A .glyph is real
 * text the author types (✓ ✗ ~ –), tinted by a semantic token so it tracks the
 * theme. These are GRAPHICAL status objects (WCAG 2.2 SC 1.4.11, 3:1 floor) —
 * each MUST carry an accessible name so meaning never rests on the mark alone:
 *   <span class="glyph glyph--success" role="img" aria-label="Yes">✓</span>
 * Reach for glyphs in dense comparison grids; keep .badge + .dot for sparse,
 * labeled status. (✗ uses --fg-muted, not --fg-disabled, which fails 3:1 dark.)
 */
.glyph { display: inline-block; min-width: 1.25ch; text-align: center; font-style: normal; line-height: 1; }
.glyph--success   { color: var(--success); }       /* ✓ */
.glyph--muted     { color: var(--fg-muted); }       /* ✗ — recedes, clears 3:1 */
.glyph--attention { color: var(--attention); }      /* ~ */
.glyph--na        { color: var(--fg-secondary); }   /* – */

/* ═══ 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-interface); 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); }
/* Canonical icon form — artificer-theme.js injects the half-circle glyph
   into an empty [data-theme-toggle] button and adds this class. A pinned
   mode (dark/light) renders the glyph in accent; auto stays quiet — the
   gold is the "you overrode the OS" signal. */
.theme-toggle--icon { padding: 6px; }
.theme-toggle--icon .icon { display: block; }
.theme-toggle--icon:not([data-theme-mode="auto"]) { color: var(--accent); }
.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); }

/* Colophon — page-foot document seam (#97). The band at a page's bottom for
   attribution, links, and fine print — the one place a tool surface routinely
   speaks in sentences, so its prose opts back into --font-body even inside
   .surface-tool (this rule sits after .surface-tool, winning on source order).
   Columns compose from .grid-auto / .stack; wrap inner content in a .container. */
.colophon {
  border-top: 1px solid var(--border);
  padding: var(--s-xl) 0;
  color: var(--fg-secondary);
  font-family: var(--font-body);
  font-size: var(--t-label-sm-size);
  line-height: 1.6;
}
.colophon :where(p, ul, ol, li, a, small, dd, dt, .btn--link) { font-family: var(--font-body); }
.colophon__label {
  display: block; margin-bottom: var(--s-xs);
  font-family: var(--font-mono); font-size: var(--t-label-xs-size);
  font-weight: 500; letter-spacing: .06em; text-transform: uppercase;
  color: var(--fg-secondary);
}
.colophon__fine {
  margin-top: var(--s-lg); padding-top: var(--s-md);
  border-top: 1px solid var(--border);
  color: var(--fg-secondary);            /* NOT --fg-muted — 3.05:1, below AA floor */
  font-size: var(--t-label-xs-size); line-height: 1.5;
}
/* The spine — the colophon's one row: identity · sign-off · links. Three slots,
   and the middle one is centred ON THE PAGE, not in whatever space the outer
   two leave over. That is why this is a grid and not `space-between`: equal
   `1fr` side columns mean a long copyright can't shove the sign-off sideways.
   Slots are positional, not classed — first child starts, last child ends. Any
   slot may be empty; leave the element in place to hold its column.
   (Minting provenance — two consumers derived this independently — in
   CHANGELOG v0.22.0. The touch floor lives with the other nav targets in
   § Touch escalation, not here.) */
.colophon__spine {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--gap, var(--s-md));       /* --gap: the same knob .stack/.cluster expose */
}
/* Above the stack breakpoint only: below it the container centres every slot,
   and an `end` here would have to be countermanded. Declaring it in min-width
   keeps the alignment rule in one place instead of two. */
@media (min-width: 641px) {
  .colophon__spine > :last-child { justify-self: end; }
}
/* Zone spacing: the spine only needs a gap above it when it follows sibling
   colophon content (a .grid-auto of labeled sections). A spine-only colophon
   gets its space from .colophon's own padding. */
.colophon__spine:not(:first-child) { margin-top: var(--s-lg); }
@media (max-width: 640px) {
  /* Stack, and centre every slot — a left-aligned copyright above a centred
     sign-off reads as a mistake at this width. text-align carries the wrapped
     second line, which justify-items alone does not. */
  .colophon__spine {
    grid-template-columns: 1fr;
    justify-items: center;
    text-align: center;
    gap: var(--gap, var(--s-sm));
  }
}

/* ═══ 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-interface); 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-interface); font-size: var(--t-label-xs-size); line-height: 1.3;
  pointer-events: none; white-space: nowrap;
  box-shadow: var(--shadow-popover);
}
/* Reveal wrapper (#248/#262) — makes .tooltip extractable WITHOUT breaking the
   bare always-visible box used inline elsewhere. Wrap a trigger + .tooltip in
   .has-tooltip; the tooltip stays hidden until hover OR keyboard focus lands on
   the trigger (WCAG 1.4.13 — hoverable, focusable, blur-dismissable). A bare
   .tooltip with no .has-tooltip ancestor is unchanged (still a static box). */
.has-tooltip { position: relative; display: inline-flex; }
.has-tooltip .tooltip {
  opacity: 0; visibility: hidden;
  transition: opacity var(--dur-fast) var(--ease), visibility var(--dur-fast) var(--ease);
}
.has-tooltip:hover .tooltip,
.has-tooltip:focus-within .tooltip {
  opacity: 1; visibility: visible; pointer-events: auto;
}
/* Position modifiers — replace per-instance inline styles. Arrow via ::after,
   a border-triangle in --fg so it matches the bubble across both themes. */
.tooltip--top,
.tooltip--bottom { left: 50%; transform: translateX(-50%); }
.tooltip--top    { bottom: calc(100% + 6px); }
.tooltip--bottom { top: calc(100% + 6px); }
.tooltip--left,
.tooltip--right  { top: 50%; transform: translateY(-50%); }
.tooltip--left   { right: calc(100% + 6px); }
.tooltip--right  { left: calc(100% + 6px); }
.tooltip--top::after,
.tooltip--bottom::after,
.tooltip--left::after,
.tooltip--right::after { content: ""; position: absolute; border: 4px solid transparent; }
.tooltip--top::after    { top: 100%;  left: 50%; transform: translateX(-50%); border-top-color: var(--fg); }
.tooltip--bottom::after { bottom: 100%; left: 50%; transform: translateX(-50%); border-bottom-color: var(--fg); }
.tooltip--left::after   { left: 100%; top: 50%; transform: translateY(-50%); border-left-color: var(--fg); }
.tooltip--right::after  { right: 100%; top: 50%; transform: translateY(-50%); border-right-color: var(--fg); }

/* ═══ 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-interface); 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-interface); 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-interface); 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-interface); 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); }

/* ─── Bar — horizontal meter (row + track + fill) ──────────────────────
   The liftable horizontal bar the gallery lacked. BEM'd to sidestep the
   SVG-scoped `.gauge .track`/`.fill`. `.bar__fill` bakes in `display:block` +
   `height:100%` so a bare fill never silently collapses to zero width (#248);
   width is driven inline, background from ANY semantic token (`--accent-fill`
   like `.progress__bar`, or `--series-N` / `--diverge-*` for categorical or
   valenced data — the consumer picks). `.bar` is a flex row so an optional
   label sits inline before the track. */
.bar { display: flex; align-items: center; gap: var(--s-sm); }
.bar__track {
  display: block; flex: 1;
  height: 8px; background: var(--bg-raised);
  border-radius: var(--radius-pill); overflow: hidden;
}
.bar__fill {
  display: block; height: 100%;
  background: var(--accent-fill);
  border-radius: inherit;
  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-interface); 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-interface); 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; } }

/* Workbench — a row of equal-width editor/text panes, side by side; a
   work surface, not a sixth dash recipe (it's a valid .dash BODY, same
   tier as .split-pane / .table). Collapses to a stack below --bp-tablet
   (800px), same threshold as .split-pane. */
.workbench {
  display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr);
  gap: var(--s-md); min-height: 0;
}
@media (max-width: 800px) { .workbench { grid-auto-flow: row; } }

.workbench__pane {
  background: var(--bg-raised); border: 1px solid var(--border);
  border-radius: var(--radius-md);
  display: grid; grid-template-rows: auto 1fr; min-height: 0;
  overflow: hidden;
}
.workbench__pane:focus-within { border-color: var(--accent); }

.workbench__header {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s-md);
  padding: var(--s-sm) var(--s-md);
  border-bottom: 1px solid var(--border);
  font-family: var(--font-mono); font-size: var(--t-label-sm-size);
  color: var(--fg-secondary);
}

.workbench__editor {
  height: 100%; resize: none;
  border: none; border-radius: 0; background: transparent;
  padding: var(--s-md); line-height: 1.6;
}

/* ── 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-interface); 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-interface); 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-interface); 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-interface); 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-interface); font-size: var(--t-label-xs-size);
  letter-spacing: .08em; text-transform: uppercase; font-weight: 600;
}

/* ─── HTML diagram node — the CSS twin of the SVG .dia-node ─────────────
   A div card reading the same --dia-* tokens, for HTML/CSS diagrams
   (React Flow, hand-built flows) where inline SVG is overkill. Mirrors the
   .rf-artificer .react-flow__node snippet documented in diagrams.html. */
.dia-box {
  display: inline-block;
  background: var(--dia-node-bg);
  color: var(--dia-node-fg);
  border: 1px solid var(--dia-node-border);
  border-radius: var(--radius-sm);
  font-family: var(--font-mono);
  font-size: var(--t-label-sm-size);
  padding: var(--s-sm) var(--s-md);
}
.dia-box--accent { border-color: var(--accent); }
.dia-box--ghost  { background: transparent; border-style: dashed; }
/* Colored type-tab down the left edge; family tint driven by a series token:
   <div class="dia-box dia-box--tab" style="--dia-tab: var(--series-3)"> */
.dia-box--tab { border-left: 3px solid var(--dia-tab, var(--series-1)); }

/* ─── Flow / pipeline — horizontal step-chains with connectors ──────────
   Left-to-right chain of steps joined by drawn connectors, with optional
   dashed phase-group frames. A .flow__step IS a .dia-box; the connector
   mirrors the SVG .dia-edge doctrine (solid line + arrowhead; --dashed
   flips border-style for async/return). Distinct from the vertical,
   numbered, grouped .spine timeline. */
.flow {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-md);
}
.flow__step { position: relative; }
.flow__link {
  flex: 0 0 auto;
  align-self: center;
  width: var(--s-lg);
  border-top: 1.25px solid var(--dia-edge);
  position: relative;
}
.flow__link::after {
  content: "";
  position: absolute;
  right: -1px; top: 50%;
  width: 5px; height: 5px;
  border-top: 1.25px solid var(--dia-edge);
  border-right: 1.25px solid var(--dia-edge);
  transform: translateY(-50%) rotate(45deg);
}
.flow__link--dashed { border-top-style: dashed; }
.flow-frame {
  border: 1px dashed var(--border);
  border-radius: var(--radius-md);
  padding: var(--s-md);
}
.flow-frame__label {
  font-family: var(--font-interface); font-size: var(--t-label-xs-size);
  letter-spacing: .08em; text-transform: uppercase; font-weight: 600;
  color: var(--fg-secondary); margin-bottom: var(--s-sm);
}

/* ─── Phased timeline — .spine / .phase ─────────────────────────────────
   A vertical, NUMBERED timeline where each phase holds a GROUP of nodes —
   "what happens in sequence, where each step is a set." The rail runs down
   the marker gutter; each .phase is a numbered station whose .phase__body
   holds a .stack of .dia-box nodes (composes the HTML node above). Distinct
   from the horizontal single-node .flow chain. */
.spine {
  --spine-gutter: 32px;
  display: flex; flex-direction: column; gap: var(--s-lg);
}
.phase {
  position: relative;
  display: grid;
  grid-template-columns: var(--spine-gutter) 1fr;
  gap: var(--s-md);
  align-items: start;
}
/* the connecting rail: a segment per station, from this marker's center down
   into the gap to the next marker; suppressed on the last station. */
.phase::before {
  content: "";
  position: absolute;
  left: calc(var(--spine-gutter) / 2);
  top: calc(var(--spine-gutter) / 2);
  bottom: calc(-1 * var(--s-lg));
  width: 1px; margin-left: -0.5px;
  background: var(--dia-rail);
}
.phase:last-child::before { display: none; }
.phase__marker {
  position: relative; z-index: var(--z-raised);
  width: var(--spine-gutter); height: var(--spine-gutter);
  display: grid; place-items: center;
  border-radius: var(--radius-pill);
  background: var(--bg-raised); border: 1px solid var(--border);
  color: var(--fg-secondary);
  font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  font-size: var(--t-label-sm-size); font-weight: 600;
}
.phase__body { min-width: 0; }
/* the one phase that breaks off the rail — accent marker + ring, one per timeline */
.phase--accent .phase__marker {
  border-color: var(--accent); color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in oklch, var(--accent) 20%, transparent);
}


/* ═══════════════════════════════════════════════════════════════════════
   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-interface); 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-interface); 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-interface);
  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);
}
/* Hover only where hover exists — a touch tap latches :hover on the tapped
   row until the next tap lands elsewhere, leaving a stuck highlight that
   reads as a phantom active state. */
@media (hover: hover) {
  .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 .sidenav__footer { display: none; }   /* text row can't collapse to icon-only */
.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;
}

/* Collapsible section — drawer-scale density (issue: 20+ rows read as a wall).
   A <details class="sidenav__section"> wraps a group; its <summary> carries the
   .sidenav__group label treatment plus a twisty. Mechanism borrowed from
   .tree--static / .accordion (list-style + details-marker suppression) with
   sidenav rows kept intact — rail, counts, aria-current all still apply.
   Deliberate trade-offs: inside <details> the sticky group label no longer
   pins (position:sticky scopes to the details box), and a collapsed section
   hides its rows — so the section holding [aria-current="page"] MUST be
   rendered open. `open` is an HTML attribute; the consumer's router/JS owns
   it (default-open the active section; there is no CSS for this).
   Drawer/full-width sidenavs only — NOT rail-compatible: .sidenav--rail is
   icon-only and has no affordance for a summary toggle, so don't compose
   them (the footer is force-hidden in rail below; sections are on the
   consumer to avoid). */
.sidenav__section { border: none; }
.sidenav__section > summary {
  display: flex; align-items: center; gap: var(--s-sm);
  list-style: none; cursor: pointer;
  min-height: 44px;   /* interactive nav row — a11y #8 touch floor */
  font-family: var(--font-interface); font-size: var(--t-label-sm-size);
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--fg-secondary);
  padding: var(--s-xs) var(--s-sm);
  border-radius: var(--radius-sm);
  transition: color var(--dur-fast) var(--ease);
}
.sidenav__section > summary::-webkit-details-marker { display: none; }
.sidenav__section > summary::after {
  content: ""; width: 6px; height: 6px; margin-left: auto; flex: none;
  border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg);   /* points right when closed */
  transition: transform var(--dur-fast) var(--ease);
}
.sidenav__section[open] > summary::after { transform: rotate(45deg); }
@media (hover: hover) {
  .sidenav__section > summary:hover { color: var(--fg); }
}
.sidenav__section > summary:focus-visible {
  outline: var(--focus-width) solid var(--accent);
  outline-offset: var(--focus-offset-inset);
}
/* Divider between sections — mirrors .sidenav__group:not(:first-child). */
.sidenav__section + .sidenav__section {
  margin-top: var(--s-sm); padding-top: var(--s-sm);
  border-top: 1px solid var(--border);
}

/* Footer row — bottom-anchored settings-class home (the theme toggle's
   drawer seat, #17's pattern). The drawer's .sidenav is a min-height:100%
   flex column, so margin-top:auto pins this to the bottom, above the
   safe-area padding. Compose: <span>Theme</span> + a canonical
   .theme-toggle--inline (empty [data-theme-toggle] button). This also
   resolves the z-index collision where the open drawer (--z-modal + 1)
   covers a fixed .theme-toggle (--z-overlay). */
.sidenav__footer {
  margin-top: auto;
  display: flex; align-items: center; justify-content: space-between; gap: var(--s-sm);
  min-height: 44px; padding: var(--s-sm) var(--s-sm) 0;
  border-top: 1px solid var(--border);
  font-family: var(--font-interface); font-size: var(--t-label-md-size);
  color: var(--fg-secondary);
}
/* The nav-row reset (.sidenav button: width 100%, background none, border 0,
   font inherit) hits ANY button in the sidenav — including this footer's
   toggle, which it strips of its pill and stretches full-width. Restore the
   toggle's own presentation at higher specificity. */
.sidenav__footer .theme-toggle {
  width: auto; flex: none;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  padding: 6px 10px;
  font-family: var(--font-interface); font-size: var(--t-label-xs-size); font-weight: 500;
}

/* 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 { min-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 1 auto; min-width: 0; /* content basis, shrink allowed: basis 0 gave
    the brand only its grow-share (the spacer claims the rest), ellipsizing a
    child .wordmark carrier even with free room; min-width:0 still lets it
    shrink below min-content on narrow viewports (#127) */
  overflow: hidden; /* clip any brand text that exceeds the shrunk container */
  white-space: nowrap; /* a brand never wraps; pairs with the ellipsis carriers below */
  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;
}
/* Ellipsis carriers — text-overflow is inert on a flex container, so the
   truncation lives on a block-level carrier: the brand element itself when it
   IS the wordmark (one element, two classes — the extra specificity also beats
   the coarse-pointer `display: inline-flex` escalation below), or a .wordmark
   child inside a composed brand (icon + mark). Either way the mark yields its
   trailing content — including the ::after stop — to the ellipsis when space
   runs out; an intact mark always keeps its stop. */
.appbar__brand.wordmark,
.appbar__brand > .wordmark {
  display: block; width: auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis;
  align-content: center; /* keeps the mark vertically centred when the
    coarse-pointer 44px min-height applies to a block carrier */
}
.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-interface); 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));
}
/* The drawer scrolls, so the bottom inset must live INSIDE the scroll content
   (on the full-height inner .sidenav) — padding-bottom on the scroll container
   itself sits outside the 100%-tall child, pushing the last row under the home
   indicator and collapsing at scroll-end in some engines. */
.nav-drawer { padding-top: env(safe-area-inset-top); }
.nav-drawer > .sidenav { padding-bottom: calc(var(--s-md) + env(safe-area-inset-bottom, 0px)); }

/* 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; }
  /* The ellipsis carriers set min-width: 0 at higher specificity so the mark
     can shrink to truncation — re-floor them here so the tap target never
     drops below 44px on a finger (the ellipsis engages above the floor). */
  .appbar__brand.wordmark, .appbar__brand > .wordmark { min-width: 44px; }
  .crumb a { min-height: 44px; min-width: 44px; display: inline-flex; align-items: center; }
  /* Colophon spine links. Scoped to the spine, NOT to a <nav> inside it: the
     spine's contract is that slots are positional and their content is the
     consumer's choice, so a links slot written as <div class="cluster">, a
     <nav>, bare <a>s, or an SPA's view-switching <button>s are all conforming
     — and a floor that only fired for some of them would advertise a
     guarantee it doesn't keep. Prose links
     in .colophon__fine are deliberately NOT covered: those are inline text
     links, exempt under WCAG 2.5.8.
     `justify-content` (not needed by the two rules above, whose content fills
     the box) centres a short label like "RSS" inside its square minimum. */
  .colophon__spine a { min-height: 44px; min-width: 44px; display: inline-flex; align-items: center; justify-content: center; }
  /* .btn--link strips .btn's padding, so it needs the floor in its own right
     — anywhere it is used, not only in a colophon. */
  .btn--link { min-height: 44px; min-width: 44px; display: inline-flex; align-items: center; justify-content: 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-interface); 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];
  column-gap: var(--shell-gap, 0px); /* rail↔content breathing room; 0 keeps the
    classic flush shell — consumers set --shell-gap instead of redefining the grid */
}
.app-shell > .appbar  { grid-row: bar;  grid-column: 1 / -1; }
.app-shell > .sidenav { grid-row: body; grid-column: rail; }
/* Sticky rail — the sidenav pins while the content scrolls. align-self: start
   is load-bearing: a default-stretch grid child is as tall as the body row and
   has nowhere to stick. --sidenav-sticky-top clears any sticky chrome above
   (e.g. calc(56px + var(--s-md)) under a sticky .appbar). Replaces the
   consumer-invented sticky wrapper aside. */
.sidenav--sticky {
  position: sticky;
  top: var(--sidenav-sticky-top, var(--s-md));
  align-self: start;
}
.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-interface); 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-interface); 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-interface);
}
.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-interface); 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. (ADR 0017) */
.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 */

/* ═══ Static tree — native <details> disclosure, no-JS contexts ═════════
   Deliberately NOT role="tree" — nested <details><summary> IS the honest
   a11y model here (browser-native disclosure semantics), not an APG tree
   emulation. ArtificerTree.enhance() is inert on this variant by design.
   Reuse .tree__row on the <summary>; twisty rotation keys off details[open]
   instead of aria-expanded. Mechanism lifted from .accordion (list-style
   + ::-webkit-details-marker suppression). Issue 317. */
.tree--static details { border: none; }
.tree--static summary { list-style: none; cursor: pointer; }
.tree--static summary::-webkit-details-marker { display: none; }
.tree--static details[open] > .tree__row > .tree__twisty { transform: rotate(90deg); }
.tree--static .tree__row:focus-visible {
  outline: var(--focus-width) solid var(--accent);
  outline-offset: var(--focus-offset-inset);
}
@media (pointer: coarse) {
  .tree--static .tree__row { min-height: 44px; }
}

/* ═══ 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; }
}
