/* Brand font — Figtree (LI.FI 1.0, the default brand). Since the typography
   axis landed (CLAUDE.md → "Typography axis"), this @import + the --font-sans
   token below are the FIRST-PAINT MIRROR of the default preset's `font` field
   (presets.js → lifi-1 `font: 'figtree'`) — the same hand-maintained CSS↔JS
   mirror as the :root colour seeds (check-theme-tokens.py TC7). When the
   default brand's font changes, flip all three together: this @import, the
   --font-sans token, and the preset's `font` field (+ revision bump).
   The JetBrains Mono @import below is the same first-paint mirror for the
   default MONO family (presets.js → lifi-1 `mono: 'jetbrains-mono'` + the
   --font-mono token; TC7 checks both). The mono typography sub-axis lazy-
   loads any OTHER mono family (e.g. LI.FI 2.0 → IBM Plex Mono) on apply. */
@import url('https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400..900&display=swap');

/* ═══════════════════════════════════════════════════════════════
   LI.FI Design System — styles.css
   ───────────────────────────────────────────────────────────────
   Single source of truth for all shared visual styles.
   Edit here → changes propagate to every page automatically.

   ┌─────────────────────────────────────────────────────────────┐
   │  COLOR SYSTEM: OKLCH                                        │
   │                                                             │
   │  oklch(L  C  H)                                             │
   │         │  │  └── Hue  0–360°  (color wheel angle)         │
   │         │  └────── Chroma 0–0.4+  (colorfulness)           │
   │         └───────── Lightness  0–100%  (perceptual)         │
   │                                                             │
   │  Why OKLCH over hex/rgba:                                   │
   │  • Perceptually uniform — L=50% looks equally bright        │
   │    across every hue, so palette steps are visually even     │
   │  • Contrast-predictable — adjusting L guarantees legibility │
   │  • One token flip (dark ↔ light) is all that's needed;      │
   │    no more scattered rgba(255,255,255,X) overrides          │
   │                                                             │
   │  LI.FI hue anchor: 264° (blue)                              │
   │  Brand hues: 264° blue · 183° teal · 308° pink             │
   └─────────────────────────────────────────────────────────────┘

   Contents:
   1. Reset & Design Tokens (incl. Page Ambient Background)
   3. Navbar & Mega Dropdown
   4. Mobile Menu
   5. Buttons
   6. Section Primitives
   7. Footer
   8. Theme Toggle
   9. Light Theme Overrides (structural only — tokens handle the rest)
   10. Shared Responsive Breakpoints
   (Section 2, Page Glow Background, was removed when the 5-orb legacy
    system was replaced by the body::before/body::after ambient.)
   ═══════════════════════════════════════════════════════════════ */


/* ══════════════════════════════════════════
   1. RESET & DESIGN TOKENS
   ══════════════════════════════════════════ */
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }

:root {
  /* ──────────────────────────────────────────
     Font Family Tokens
     ──────────────────────────────────────────
     Two families, two roles:
       --font-sans   Figtree         — content + UI (default, set on `html`)
       --font-mono   JetBrains Mono  — code, addresses, numerics, eyebrows
     Documented at #typography → "Font families" card. The `<link>` tag
     in each HTML page loads Figtree (300–900) + JetBrains Mono (400–700)
     from Google Fonts; these tokens just name the families so every
     font-family rule in this file references a token, never a literal.
     Bug history (May 2026): these declarations lived in the retired
     `lifi-design-system.css` and got dropped when that file was killed,
     silently breaking every `font-family: var(--font-mono)` usage in
     the catalog (mono fell back to inherited Figtree). Restored here so
     the cascade source for fonts matches the rest of the system —
     styles.css declares once, every consumer reads via the token. */
  /* First-paint mirror of the default preset's `font` field (presets.js →
     lifi-1 'figtree'; see the @import comment at the top of this file).
     Per-theme fonts are the Theme Composer's typography axis (THEME_FONTS,
     theme-composer-core.js) — a theme apply overrides this token on its
     scope; this literal is only what paints before/without a pick. */
  --font-sans: 'Geist', system-ui, -apple-system, 'Segoe UI', Arial, sans-serif;
  --font-mono: 'Geist Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
  /* Jumper sibling-brand sans (jumper.xyz). The type SCALE, weights ladder,
     and --font-mono are shared across both brands — only the sans family
     differs. Consumers that render Jumper-brand chrome (lockup labels,
     future Jumper surfaces) reach for this token; pages that use it must
     load Urbanist themselves (Google Fonts) — unloaded pages fall back to
     the same system stack as --font-sans. Spec: design-system/foundations.html#typography. */
  --font-jumper: 'Urbanist', system-ui, -apple-system, 'Segoe UI', Arial, sans-serif;

  /* ──────────────────────────────────────────
     OKLCH Brand Primitives
     (hue-locked to 268° — the LI.FI 1.0 blue axis)
     ────────────────────────────────────────── */
  /* ── Brand Accent Tiers ─────────────────────────────────── */
  /* Primary:   Accent 1 — CTAs, links, key actions           */
  /* Secondary: Accent 2 — secondary CTAs, labels, badges    */
  /* Tertiary:  Accent 3 — highlights, tags, decorative      */
  --lifi-sapphire:       oklch(52% 0.18 269);    /* Primary accent — Sapphire #405CCF, native in BOTH modes. June 2026: the dark lift (was L0.66) was dropped so the filled-CTA label auto-flips to white AND passes AA on the dark page (white-on-L0.66 was 2.86:1, fails; on native L0.52 it's 5.14:1). Brand-seed token, named for the palette (renamed from --lifi-blue June 2026). */
  --lifi-sapphire-mid:   oklch(62% 0.1584 269);  /* Primary mid — accent TEXT / focus / link. First-paint literal kept in lockstep with the runtime: deriveTokens recomputes --accent-primary-mid as seed+10L (= L0.62 now the seed is L0.52), so this mirrors it to avoid a cold-load FOUC. On the dark page L0.62 gives accent text 5.16:1 (AA); identical to the light mid now that both seeds are L0.52. */
  --lifi-sapphire-light: oklch(96% 0.0234 269);  /* Primary light — tinted bg surface (soft brand wash) */
  --lifi-pink:     oklch(88% 0.1 322);      /* Secondary accent — Pink #F7C2FF (renamed from --lifi-teal June 2026). */
  --lifi-ink:     oklch(66% 0.063 276);    /* Tertiary accent — Ink, text-capable, lifted on dark (renamed from --lifi-pink June 2026). */
  --lifi-dark:     oklch(18.5% 0.063 276);  /* Foundation — Ink #0C0E2E             */

  /* ── Official brand ramps — 2.0 Brand Book (Figma 409:75) ─────────
     The three brand colours as hand-tuned 9-step scales (1 = lightest tint,
     9 = darkest shade). LI.FI 1.0 CONSTANTS — static literals (the exact book
     hexes → OKLCH), NOT Theme-Composer-derived. Anchors coincide with the
     seeds above: sapphire-6 = --lifi-sapphire (light) #405CCF · signal-2 = --lifi-pink
     #F7C2FF · midnight-9 = --lifi-dark / --surface-page (dark) #0C0E2E.
     Naming avoids the neutral --ink-0..900 alpha ladder (§ further down). */
  --midnight-1: oklch(96.5% 0.0164 275);  /* #F0F3FF */
  --midnight-2: oklch(89.9% 0.0180 276);  /* #DADDEA */
  --midnight-3: oklch(82.0% 0.0313 275);  /* #BEC3D9 */
  --midnight-4: oklch(72.0% 0.0457 275);  /* #9CA3C2 */
  --midnight-5: oklch(59.9% 0.0633 276);  /* #757DA6 */
  --midnight-6: oklch(48.0% 0.0622 275);  /* #535B81 */
  --midnight-7: oklch(37.1% 0.0626 275);  /* #363D61 */
  --midnight-8: oklch(26.9% 0.0576 276);  /* #1F2342 */
  --midnight-9: oklch(18.5% 0.0631 276);  /* #0C0E2E — Ink */
  --sapphire-1: oklch(95.7% 0.0220 244);  /* #E5F3FF */
  --sapphire-2: oklch(89.6% 0.0503 264);  /* #CCDDFF */
  --sapphire-3: oklch(82.1% 0.0795 269);  /* #AFC3F9 */
  --sapphire-4: oklch(72.0% 0.1001 269);  /* #8BA2E4 */
  --sapphire-5: oklch(59.9% 0.1242 269);  /* #627BCA */
  --sapphire-6: oklch(52.0% 0.1803 269);  /* #405CCF — Sapphire */
  --sapphire-7: oklch(37.0% 0.1690 269);  /* #1F2F97 */
  --sapphire-8: oklch(27.1% 0.1795 269);  /* #0F007C */
  --sapphire-9: oklch(22.2% 0.1438 272);  /* #0D005C */
  --signal-1:   oklch(96.6% 0.0275 322);  /* #FDEEFF */
  --signal-2:   oklch(88.0% 0.0997 322);  /* #F7C2FF — Pink */
  --signal-3:   oklch(82.0% 0.0502 322);  /* #D4BAD8 */
  --signal-4:   oklch(72.0% 0.0720 322);  /* #BA96C0 */
  --signal-5:   oklch(60.1% 0.0992 322);  /* #9C6CA4 */
  --signal-6:   oklch(47.9% 0.0998 322);  /* #77497F */
  --signal-7:   oklch(36.9% 0.0987 322);  /* #572B5E */
  --signal-8:   oklch(27.0% 0.0906 322);  /* #3A1340 */
  --signal-9:   oklch(17.6% 0.0840 321);  /* #200026 */

  /* ── Jumper sibling-brand primitives (jumper.xyz) ─────────────────
     Brand constants for rendering the Jumper logo + lockups on any
     surface — NOT Theme Composer accents (those live in presets.js →
     jumper-1 / jumper-2 and are user-selectable). Mode-paired to
     Jumper's own published tokens: accent1-main #31007A (light) ↔
     #653CA2 (dark), accent2-main #8700B8 (light) ↔ #B49BDA (dark).
     The logo SVGs reference these via style="fill: var(--jumper-…,
     #lightFallback)" so inline marks flip with the mode while
     standalone <img>/downloaded copies render the light-mode brand
     literals. Assets: logos/jumper/. Spec: design/components/imagery.md
     → Brand lockup. */
  --jumper-indigo: oklch(46% 0.158 298);    /* logo body + wordmark — renders #653CA2 in dark   */
  --jumper-purple: oklch(73% 0.093 302);    /* logo accent piece — renders #B49BDA in dark      */

  /* Semantic accent aliases — use these everywhere instead of --lifi-* */
  --accent-primary:       var(--lifi-sapphire);
  --accent-primary-mid:   var(--lifi-sapphire-mid);
  --accent-primary-light: var(--lifi-sapphire-light);
  --accent-secondary:     var(--lifi-pink);
  --accent-tertiary:      var(--lifi-ink);

  /* ── Semantic state tokens — formula-derived from --brand-anchor ──
     The four roles (success / danger / warn / info) are not hand-typed
     oklch literals anymore — they are LIVE FUNCTIONS of the brand
     anchor (--brand-anchor → var(--lifi-sapphire) by default). When the
     Theme Composer swaps --lifi-sapphire (LI.FI 1.0 → 2.0 → custom),
     every semantic and every bg/border companion auto-rebalances via
     pure CSS cascade. No JS wiring; no manual retune.

     The doctrine:
       H = role canonical hue                  (locked per role)
       L = anchor_L + role_L_offset            (theme-aware)
       C = max(anchor_C, 0.12) × role_C_mult   (theme-aware; floored)

     Each OKLCH channel has a different owner:
       H carries role meaning  (green = success, red = danger, etc.)
       L carries system weight (semantic legibility band)
       C carries brand voice   (saturation matches brand signature;
                                the 0.12 input floor keeps a muted /
                                monochrome anchor from collapsing the
                                states to grey — multiplicative chroma
                                can't recover from 0. Mirrors
                                theme-composer-core.js → SEM_C_FLOOR;
                                keep the pair in lockstep)

     The role_L_offset and role_C_multiplier are TUNABLE — bound to
     --sem-{role}-L-offset and --sem-{role}-C-mult so the Theme
     Composer's "Semantic states" section can mutate them live, PER
     THEME (June 2026 — the Semantic Composer merge; a theme record's
     optional `semantics` field carries per-mode lo/cm + role hues,
     painted by theme-composer-core.js → applyPalette: coefficient vars
     here at :root, resolved literals on scoped canvases). The defaults
     below MIRROR theme-composer-core.js → SEM_DEFAULTS — keep the two
     in lockstep when retuning. Token docs:
     design-system/foundations.html#semantic-composer.

     Bg / border companion tones derive from the role anchor itself
     (preserving hue, swapping in theme-aware L+C constants from
     --sem-bg-L / --sem-bg-C / --sem-border-L / --sem-border-C).

     Browser support: OKLCH relative-color syntax — Chrome 119+,
     Safari 16.4+, Firefox 128+. Evergreen-safe by 2026. ────────── */

  --brand-anchor: var(--lifi-sapphire);

  /* Per-role formula coefficients — DARK-mode defaults.
     Anchor = --brand-anchor = --lifi-sapphire (Sapphire), dark L=0.52 (unified
     with light — June 2026). Output L = anchor.L + offset:
       success: 0.52+0.180=0.70, C=0.18*0.770=0.139, H=160
       danger:  0.52+0.100=0.62, C=0.18*0.950=0.171, H= 24
       warn:    0.52+0.240=0.76, C=0.18*0.730=0.131, H= 74
       info:    0.52+0.180=0.70, C=0.18*0.730=0.131, H=269
     L-offsets bumped +0.14 (June 2026) when the dark Sapphire anchor dropped
     0.66→0.52 for the white-CTA-label unification — the semantics derive
     their lightness from the anchor, so the bump keeps the dark states at
     their original, AA-legible output (danger 4.75:1, info 6.93, success
     7.46, warn 8.55 on the dark page). C-mult + H unchanged. Keep in lockstep
     with theme-composer-core.js SEM_DEFAULTS.dark. */
  --sem-success-L-offset:  0.180;
  --sem-success-C-mult:    0.770;
  --sem-success-H:          160;

  --sem-danger-L-offset:   0.100;
  --sem-danger-C-mult:     0.950;
  --sem-danger-H:            24;

  --sem-warn-L-offset:     0.240;
  --sem-warn-C-mult:       0.730;
  --sem-warn-H:              74;

  --sem-info-L-offset:     0.180;
  --sem-info-C-mult:       0.730;
  --sem-info-H:             269;

  /* Surface-tier recipe — bg / border use these constants (theme-aware,
     overridden in light mode). Role H feeds in via the role anchor. */
  --sem-bg-L:     0.20;
  --sem-bg-C:     0.05;
  --sem-border-L: 0.34;
  --sem-border-C: 0.10;

  /* The four semantic anchors — formula-derived via OKLCH relative-color
     syntax. Reading the anchor's L and C from --brand-anchor, applying
     the per-role offset/multiplier, locking the hue to the role's H. */
  --success: oklch(from var(--brand-anchor)
                   calc(l + var(--sem-success-L-offset))
                   calc(max(c, 0.12) * var(--sem-success-C-mult))
                   var(--sem-success-H));
  --danger:  oklch(from var(--brand-anchor)
                   calc(l + var(--sem-danger-L-offset))
                   calc(max(c, 0.12) * var(--sem-danger-C-mult))
                   var(--sem-danger-H));
  --warn:    oklch(from var(--brand-anchor)
                   calc(l + var(--sem-warn-L-offset))
                   calc(max(c, 0.12) * var(--sem-warn-C-mult))
                   var(--sem-warn-H));
  --info:    oklch(from var(--brand-anchor)
                   calc(l + var(--sem-info-L-offset))
                   calc(max(c, 0.12) * var(--sem-info-C-mult))
                   var(--sem-info-H));

  /* Companion bg / border tones — color-mix recipes derived from the
     role token itself. Same pattern buttons use for hover backgrounds
     (`--btn-accent-bg`). Every Semantic Composer slider (L / C / H)
     produces a visible change on alert + notification surfaces because
     the bg/border scale with the role's full color, not with system
     constants.

     Design history:
       v1 — hand-typed oklch literals per role, per theme.
       v2 — derived companion: `oklch(from var(--success) <L> <C> h)`.
            Hit a browser caching bug: when `--success` is itself a
            relative-color `oklch(from --brand-anchor calc(l + ...) ...)`,
            Chrome / Safari / Firefox cache the source's resolved L+C
            at compute time. Slider-driven L-offset / C-mult changes
            updated `--success` but did NOT propagate to the dependent.
            Only `h` cascaded cleanly.
       v3 — flat oklch(<L> <C> <H>) using --sem-bg-L / --sem-bg-C /
            --sem-<role>-H. Cascade worked correctly, but L-offset and
            C-mult sliders still didn't reach bg/border because the
            recipe locked L+C to system constants — system-uniform but
            visually inert against Composer tuning.
       v4 (current, May 2026) — color-mix from the role token. All
            three slider channels now flow through. Trade-off accepted:
            warn / info / danger / success bg's L+C now varies slightly
            per role (each scales with its own --success / --danger /
            etc.), losing the prior system-wide pastel uniformity. The
            Composer's job is visible feedback; buttons + progress
            have always behaved this way; alerts + notifications now
            match. The `--sem-bg-L / --sem-bg-C / --sem-border-L /
            --sem-border-C` constants are retained as light/dark
            anchors for any future tier that wants the system-uniform
            recipe (currently unused). */
  /* May 2026 — alert / notification bg palette calibrated to 14 %
     tone-over-transparent. 10 % originally read as muddy tan/beige in
     light mode (90 % white + 10 % vivid tone collapses the perceived
     hue); 18 % then over-corrected and read as loud across the four
     tones. 14 % is the middle ground — the tone reads as itself
     ("yellow", "green", "blue", "red") without dominating the surface.
     Border tokens kept (no consumer right now; .alert dropped its
     border in the same pass — see .alert rule) for future tier use. */
  --success-bg:     color-mix(in oklch, var(--success) 14%, transparent);
  --success-border: color-mix(in oklch, var(--success) 30%, transparent);
  --success-fg:     var(--success);

  --danger-bg:      color-mix(in oklch, var(--danger)  14%, transparent);
  --danger-border:  color-mix(in oklch, var(--danger)  30%, transparent);
  --danger-fg:      var(--danger);

  --warn-bg:        color-mix(in oklch, var(--warn)    14%, transparent);
  --warn-border:    color-mix(in oklch, var(--warn)    30%, transparent);
  --warn-fg:        var(--warn);

  --info-bg:        color-mix(in oklch, var(--info)    14%, transparent);
  --info-border:    color-mix(in oklch, var(--info)    30%, transparent);
  --info-fg:        var(--info);

  /* On-color tokens — the accessible text color to use when placing
     text on a full-saturation version of each tone. Uses the same
     luminance-threshold formula as .btn-primary's --btn-on-accent:
     pick near-black text for backgrounds with L > 0.62, near-white
     text for backgrounds with L < 0.62. Ride the Theme Composer
     automatically — swap --accent-primary to a dark hue and text
     flips to white without any component edit. */
  --on-accent-primary:   oklch(from var(--accent-primary)   clamp(0.08, (0.62 - l) * 100, 0.96) 0 0);
  --on-accent-secondary: oklch(from var(--accent-secondary) clamp(0.08, (0.62 - l) * 100, 0.96) 0 0);
  --on-accent-tertiary:  oklch(from var(--accent-tertiary)  clamp(0.08, (0.62 - l) * 100, 0.96) 0 0);
  --on-success:          oklch(from var(--success)          clamp(0.08, (0.62 - l) * 100, 0.96) 0 0);
  --on-danger:           oklch(from var(--danger)           clamp(0.08, (0.62 - l) * 100, 0.96) 0 0);
  --on-warn:             oklch(from var(--warn)             clamp(0.08, (0.62 - l) * 100, 0.96) 0 0);
  --on-info:             oklch(from var(--info)             clamp(0.08, (0.62 - l) * 100, 0.96) 0 0);

  /* ──────────────────────────────────────────
     Spectrum — extended brand palette (12 OKLCH hues at 30° spacing)
     ──────────────────────────────────────────
     The brand color system has two layers:

       Identity (3)  — --accent-primary / -secondary / -tertiary
                       Carries brand identity. Shifts with the Palette
                       Composer (LI.FI Blue / Across / Uniswap).
       Spectrum (12) — --spectral-1..12 (this block)
                       Extends the palette for content where the color
                       carries no semantic meaning — categorical data,
                       illustration fills, decorative blocks. Stays put
                       across preset switches so chart data reads the
                       same regardless of which brand is active.

     Sibling tiers (defined elsewhere):
       Semantic   → status:   --success / --warn / --error / --info
       Series     → chart-curated subset of Spectrum, defined below
       Neutrals   → --ink-*, --surface-*

     OKLCH parameters — DASHBOARD-DISCIPLINE DIRECTION (Apr 2026 v3).
     Apr 2026 v2 pushed chroma toward the gamut ceiling (C 0.18–0.30,
     L 0.58–0.82) for "vibrant" decorative use, but that wide band
     created a "muddy" read on dashboards — magenta at C 0.30 screamed
     next to teal at C 0.18, lightness drifted 24 points across indices
     so adjacent series had different visual weights, and the single
     universal set forced cool hues to fail AA contrast on light
     backgrounds (failed 4.5:1 against #FFFFFF for L > 0.65).

     v3 keeps the 12-hue / 30°-spacing architecture but applies five
     disciplines from the Omni Analytics theme work — see design.md §02
     "Palette grammar" for the full rules:

       1. Tight L band per mode  Dark L 0.66–0.84 (yellows the only
          outlier — they need higher L to read as yellow, not olive),
          most colors in 0.70–0.78 (8-pt). Light L 0.45–0.66 (yellows
          again the high outlier).
       2. Tight C band  C 0.18–0.24 — magenta caps at 0.24 (was 0.30),
          teal floors at 0.18 in dark / 0.13 in light (gamut limit).
       3. Dual-mode tokens  --spectral-N-dark and --spectral-N-light
          are CONSTANTS, always available. --spectral-N is the
          theme-aware alias. Every cool hue clears 3:1 contrast against
          its mode-appropriate tile bg (WCAG 1.4.11 for graphical
          objects); blues/magentas clear 4.5:1.
       4. Brand anchor preserved  --spectral-9 (blue) light-mode value
          is exactly LI.FI Blue (#3F49E1 / oklch 0.44 0.23 268). The
          dashboard's primary series reads as the brand by default.
       5. Hue array unchanged  20°, 50°, 80°, 110°, 140°, 175°, 205°,
          235°, 265°, 295°, 325°, 355° — preserves the index→named-color
          contract (--spectral-1 === --spectral-coral, etc.).

     Continuity — every var(--spectral-N) consumer in the codebase
     keeps working unchanged but gets a more disciplined value AND
     auto-adapts to light mode. No call-site changes required.

     Naming — numeric for iteration (--spectral-1..12), named aliases
     (--spectral-blue) for prose. Both forms first-class.

     Sibling palettes added in v3:
       Sequential — --seq-1..9 (single-hue blue ramp, dual-mode)
       Diverging  — --div-1..9 (Pink ↔ Slate ↔ Green, dual-mode,
                    mode-agnostic neutral)
     ────────────────────────────────────────── */

  /* Dark-mode primitives — always available, used as default alias source. */
  --spectral-1-dark:  oklch(0.72 0.20  20);   /* coral    · #FF8C7A */
  --spectral-2-dark:  oklch(0.74 0.20  50);   /* orange   · #FF9358 */
  --spectral-3-dark:  oklch(0.82 0.18  80);   /* amber    · #FFB454 */
  --spectral-4-dark:  oklch(0.84 0.20 110);   /* lime     · #C5E866 */
  --spectral-5-dark:  oklch(0.78 0.22 140);   /* green    · #5EEAA0 */
  --spectral-6-dark:  oklch(0.76 0.18 175);   /* teal     · #3DD9D9 */
  --spectral-7-dark:  oklch(0.78 0.18 205);   /* cyan     · #42E2F5 */
  --spectral-8-dark:  oklch(0.74 0.20 235);   /* sky      · #67BAFF */
  --spectral-9-dark:  oklch(0.70 0.22 265);   /* blue     · #7B86FF — brand-anchor in dark mode */
  --spectral-10-dark: oklch(0.66 0.22 295);   /* violet   · #9B5DE5 */
  --spectral-11-dark: oklch(0.72 0.24 325);   /* magenta  · #D472FA */
  --spectral-12-dark: oklch(0.76 0.22 355);   /* rose     · #FF8FCB */

  /* Light-mode primitives — always available; activated via --spectral-N alias in [data-theme="light"]. */
  --spectral-1-light:  oklch(0.55 0.21  20);   /* coral    · #DC4A4A */
  --spectral-2-light:  oklch(0.58 0.20  50);   /* orange   · #C9722E */
  --spectral-3-light:  oklch(0.66 0.16  80);   /* amber    · #B58A1A */
  --spectral-4-light:  oklch(0.62 0.20 110);   /* lime     · #6EA214 */
  --spectral-5-light:  oklch(0.55 0.20 140);   /* green    · #0E9D5C */
  --spectral-6-light:  oklch(0.55 0.13 175);   /* teal     · #008483 */
  --spectral-7-light:  oklch(0.55 0.16 205);   /* cyan     · #007D9B */
  --spectral-8-light:  oklch(0.50 0.20 235);   /* sky      · #1862C8 */
  --spectral-9-light:  oklch(0.44 0.23 268);   /* blue     · #3F49E1 — exact LI.FI Blue */
  --spectral-10-light: oklch(0.45 0.22 295);   /* violet   · #7C3AED */
  --spectral-11-light: oklch(0.50 0.24 325);   /* magenta  · #C042E0 — brand-darker pink */
  --spectral-12-light: oklch(0.55 0.22 355);   /* rose     · #E1538F */

  /* Theme-aware aliases — default to dark; [data-theme="light"] re-points. */
  --spectral-1:  var(--spectral-1-dark);
  --spectral-2:  var(--spectral-2-dark);
  --spectral-3:  var(--spectral-3-dark);
  --spectral-4:  var(--spectral-4-dark);
  --spectral-5:  var(--spectral-5-dark);
  --spectral-6:  var(--spectral-6-dark);
  --spectral-7:  var(--spectral-7-dark);
  --spectral-8:  var(--spectral-8-dark);
  --spectral-9:  var(--spectral-9-dark);
  --spectral-10: var(--spectral-10-dark);
  --spectral-11: var(--spectral-11-dark);
  --spectral-12: var(--spectral-12-dark);

  /* Named aliases — same values, prose-friendly. */
  --spectral-coral:   var(--spectral-1);
  --spectral-orange:  var(--spectral-2);
  --spectral-amber:   var(--spectral-3);
  --spectral-lime:    var(--spectral-4);
  --spectral-green:   var(--spectral-5);
  --spectral-teal:    var(--spectral-6);
  --spectral-cyan:    var(--spectral-7);
  --spectral-sky:     var(--spectral-8);
  --spectral-blue:    var(--spectral-9);
  --spectral-violet:  var(--spectral-10);
  --spectral-magenta: var(--spectral-11);
  --spectral-pink:    var(--spectral-12);

  /* ──────────────────────────────────────────
     Sequential Palette — single-hue Blue ramp (Apr 2026 v3, NEW)
     ──────────────────────────────────────────
     For dashboards: heatmaps, choropleths, density plots, any
     "more is more" metric. 9 stops with a perceptually uniform
     lightness ramp anchored on H 265 (brand axis).

     Direction by mode:
       Dark mode  — low → high goes deep navy (L 0.20) → near-white-blue
                    (L 0.94). High data value reads as "brighter" against
                    the dark tile.
       Light mode — low → high goes near-white (L 0.96) → deep navy
                    (L 0.16). High data value reads as "darker" against
                    the white tile.

     The semantic --seq-1..9 is "low → high" data; the visual reading
     flips per mode. Brand-anchor lands at stop 6 in both modes.
     ────────────────────────────────────────── */

  /* Sequential — dark-mode primitives. */
  --seq-1-dark: oklch(0.20 0.10 265);   /* low  · #1F2147 deep navy */
  --seq-2-dark: oklch(0.28 0.13 265);   /*      · #2A2D6B */
  --seq-3-dark: oklch(0.36 0.16 265);   /*      · #363B92 */
  --seq-4-dark: oklch(0.44 0.20 265);   /*      · #424ABB */
  --seq-5-dark: oklch(0.54 0.22 265);   /*      · #5159E0 */
  --seq-6-dark: oklch(0.66 0.22 265);   /*      · #7B86FF brand-anchor */
  --seq-7-dark: oklch(0.76 0.20 265);   /*      · #A0AAFF */
  --seq-8-dark: oklch(0.86 0.16 265);   /*      · #C5CCFF */
  --seq-9-dark: oklch(0.94 0.08 265);   /* high · #E2E5FF near-white-blue */

  /* Sequential — light-mode primitives. */
  --seq-1-light: oklch(0.96 0.04 265);   /* low  · #EEF0FF */
  --seq-2-light: oklch(0.90 0.08 265);   /*      · #D8DCFF */
  --seq-3-light: oklch(0.82 0.12 265);   /*      · #B8BFFF */
  --seq-4-light: oklch(0.72 0.18 265);   /*      · #959DFF */
  --seq-5-light: oklch(0.60 0.22 265);   /*      · #7079FF */
  --seq-6-light: oklch(0.44 0.23 268);   /*      · #3F49E1 brand-anchor (LI.FI Blue) */
  --seq-7-light: oklch(0.36 0.20 265);   /*      · #4148E0 */
  --seq-8-light: oklch(0.26 0.16 265);   /*      · #2A30AB */
  --seq-9-light: oklch(0.16 0.10 265);   /* high · #171B6B deep navy */

  /* Theme-aware aliases. */
  --seq-1: var(--seq-1-dark);  --seq-2: var(--seq-2-dark);  --seq-3: var(--seq-3-dark);
  --seq-4: var(--seq-4-dark);  --seq-5: var(--seq-5-dark);  --seq-6: var(--seq-6-dark);
  --seq-7: var(--seq-7-dark);  --seq-8: var(--seq-8-dark);  --seq-9: var(--seq-9-dark);

  /* ──────────────────────────────────────────
     Diverging Palette — Pink ↔ Slate ↔ Green (Apr 2026 v3, NEW)
     ──────────────────────────────────────────
     For benchmark deltas — negative ↔ neutral ↔ positive. 9 stops,
     perceptually balanced. Pink/Green axis (not Red/Green) keeps it
     LI.FI-aligned AND CVD-safe — under deuteranopia the Pink end
     becomes muted lavender and Green stays distinguishable.

     Stop semantics:
       --div-1..4  negative side (worst → least-bad)
       --div-5     neutral / zero delta (mode-agnostic slate)
       --div-6..9  positive side (least-good → best)

     The neutral midpoint is identical across modes (oklch 0.66 0.02
     250 / #94A3B8) — a "no change" cell reads as the same color in
     dark and light dashboards.
     ────────────────────────────────────────── */

  /* Diverging — dark-mode primitives. */
  --div-1-dark: oklch(0.65 0.22 350);   /* −1   · #FF5C8E sat pink   */
  --div-2-dark: oklch(0.72 0.18 350);   /* −.75 · #FF85AB           */
  --div-3-dark: oklch(0.80 0.14 350);   /* −.5  · #FFAEC7           */
  --div-4-dark: oklch(0.88 0.08 350);   /* −.25 · #FFD7E3           */
  --div-5-dark: oklch(0.66 0.02 250);   /*  0   · #94A3B8 slate     */
  --div-6-dark: oklch(0.85 0.10 160);   /* +.25 · #A8E5C7           */
  --div-7-dark: oklch(0.78 0.16 160);   /* +.5  · #7AD8A6           */
  --div-8-dark: oklch(0.70 0.20 160);   /* +.75 · #4FCB85           */
  --div-9-dark: oklch(0.62 0.22 160);   /* +1   · #1FB364 sat green */

  /* Diverging — light-mode primitives. */
  --div-1-light: oklch(0.50 0.24 350);   /* −1   · #C4248A           */
  --div-2-light: oklch(0.58 0.22 350);   /* −.75 · #D85AA1           */
  --div-3-light: oklch(0.70 0.18 350);   /* −.5  · #E78AB8           */
  --div-4-light: oklch(0.82 0.10 350);   /* −.25 · #F0BBD0           */
  --div-5-light: oklch(0.66 0.02 250);   /*  0   · #94A3B8 slate (same as dark) */
  --div-6-light: oklch(0.80 0.12 160);   /* +.25 · #AED9C5           */
  --div-7-light: oklch(0.62 0.18 160);   /* +.5  · #7AC1A1           */
  --div-8-light: oklch(0.50 0.20 160);   /* +.75 · #3FA77E           */
  --div-9-light: oklch(0.42 0.20 160);   /* +1   · #0F8A5E           */

  /* Theme-aware aliases. */
  --div-1: var(--div-1-dark);  --div-2: var(--div-2-dark);  --div-3: var(--div-3-dark);
  --div-4: var(--div-4-dark);  --div-5: var(--div-5-dark);  --div-6: var(--div-6-dark);
  --div-7: var(--div-7-dark);  --div-8: var(--div-8-dark);  --div-9: var(--div-9-dark);

  /* ──────────────────────────────────────────
     Series Palette — curated 8-color subset of Spectrum for charts
     ──────────────────────────────────────────
     Apr 2026 v3 — same indices as v2 (blue/orange/green/magenta is
     the canonical primary-quartet for the most common 4-series case),
     but the underlying spectral values are now mode-aware (see above)
     so series colors auto-adapt between dark and light dashboards
     without a separate token swap. Use these for data viz only —
     never for UI chrome.
     ────────────────────────────────────────── */
  /* THE CHARTS AXIS CAN OVERRIDE THIS TIER (June 2026): a theme record's
     `charts: 'brand'` makes the Theme Composer paint --series-1..12 inline
     (on :root or a scoped canvas) as a generated 12-hue ramp anchored on the
     theme's primary hue, L/C sampled from the spectral discipline curve
     (theme-composer-core.js → CHART_DISCIPLINE — keep in lockstep with the
     spectral primitives above). 'spectral' (the default) leaves these
     aliases standing. --spectral-* is NEVER overridden — avatar tones +
     decorative consumers stay fixed. See CLAUDE.md → "Charts axis". */
  --series-1: var(--spectral-9);    /* blue    */
  --series-2: var(--spectral-2);    /* orange  */
  --series-3: var(--spectral-5);    /* green   */
  --series-4: var(--spectral-11);   /* magenta */
  --series-5: var(--spectral-3);    /* amber   */
  --series-6: var(--spectral-7);    /* cyan    */
  --series-7: var(--spectral-10);   /* violet  */
  --series-8: var(--spectral-1);    /* coral   */

  /* Extended series — 9–12 fill the remaining spectral hues not
     consumed by 1–8 (4 lime, 6 teal, 8 sky, 12 rose) so a 12-category
     chart spans the full hue circle with no adjacent-bar collisions.
     Reach for these only when 1–8 aren't enough; the canonical
     primary-quartet (1–4) still leads in dashboards. */
  --series-9:  var(--spectral-4);   /* lime    */
  --series-10: var(--spectral-6);   /* teal    */
  --series-11: var(--spectral-8);   /* sky     */
  --series-12: var(--spectral-12);  /* rose    */

  /* ──────────────────────────────────────────
     Semantic Surface Tokens  (Dark mode defaults)
     Lightness steps: 5 → 8 → 16 → 24 %  (sunk → page → raised → card)
     May 2026 rebalance (v2) — bumped raised 14→16 and card 19→24 for a
     more confident lift on every dark-mode panel and card. Step sizes
     are now uniform (8 % between page→raised and raised→card) for
     consistent visual rhythm. Light values unchanged.
     History: pre-rebalance was 8 → 10 → 15 (2 % page→raised, too tight);
     v1 went 8 → 14 → 19 (closer to Material 3 container tier); v2 is
     the current 8 → 16 → 24 step.
     ────────────────────────────────────────── */
  --surface-sunk:     oklch(17% 0.063 276);    /* recessed canvas / well — recess below Ink page so wells read recessed */
  --surface-page:          oklch(18.5% 0.063 276);  /* page background — Ink #0C0E2E */
  --surface-raised:   oklch(23.2% 0.0577 275); /* raised surface — Ink #161A38 (2.0 Brand Book scale; pinned for lifi-1 via Manual mode, mirrored here for first-paint) */

  /* Card-tier surface fill — what cards, tiles, and paper-material
     consumers reach for to lift above the canvas. Dark: L31 (#262C55, the
     2.0 Brand Book card tier), above the L23.2 raised (#161A38) and the L18.5
     Midnight-Ink page — keeps cards clearly lifted against deep-ink themes
     (e.g. Jumper 1.0's deep-indigo page). Light: pivots to
     --surface-page (100% white) so cards lift via shadow alone — see
     [data-theme="light"] override below. Materials definitions and the
     Theme Composer both write through this token, so the theme pivot
     lives in ONE place. */
  --surface-card:     oklch(31% 0.0735 275);   /* card tier — Ink #262C55 (2.0 Brand Book scale; pinned for lifi-1 via Manual mode, mirrored here for first-paint) */

  /* ── Elevation shadow tokens — DARK MODE ───────────────────
     System-wide rule: prefer elevation over hard borders. A card or
     row reads as "lifted off the page" via three coordinated mechanics
     — a fill one step above the page surface (--surface-raised /
     --surface-card), a soft drop shadow, and a 1 px inset top highlight
     that catches the rim of the card like ambient light. The inset
     highlight is the dark-mode trick: shadows on dark backgrounds
     barely register, but a subtle white-at-low-alpha rim sells the lift.

     Three tiers map to physical depth:
       --elev-1   resting cards & rows (setting-row, feature-card)
       --elev-2   hover state, active sticky cards, tooltips
       --elev-3   floating surfaces — modals, popovers, dropdowns
     Authors should reach for the tier, not hand-author shadows. */
  --elev-1: 0 1px 2px rgba(0,0,0,0.35),
            inset 0 1px 0 rgba(255,255,255,0.04);
  --elev-2: 0 4px 14px -2px rgba(0,0,0,0.45),
            0 2px 4px -1px rgba(0,0,0,0.25),
            inset 0 1px 0 rgba(255,255,255,0.05);
  --elev-3: 0 24px 56px -8px rgba(0,0,0,0.55),
            0 8px 16px -4px rgba(0,0,0,0.30),
            inset 0 1px 0 rgba(255,255,255,0.06);

  /* Filter-form of the elevation tokens — MASKED elements ONLY (box-shadow is
     the canonical method; box-shadow is clipped by mask, drop-shadow paints
     AFTER the mask so it wraps the cutout). VALUE-MATCHED to the --elev-N twin
     so a masked surface matches its box-shadow neighbours. Dark --elev-1 has a
     single OUTSET layer (its second layer is the inset rim, which drop-shadow
     can't replicate), so the dark filter is a single drop-shadow that matches
     that outset exactly. See CLAUDE.md → "mask-image clips box-shadow". */
  --elev-1-filter: drop-shadow(0 1px 2px rgba(0,0,0,0.35));
  --elev-2-filter: drop-shadow(0 4px 14px rgba(0,0,0,0.45))
                   drop-shadow(0 2px 4px rgba(0,0,0,0.25));
  --elev-3-filter: drop-shadow(0 24px 56px rgba(0,0,0,0.55))
                   drop-shadow(0 8px 16px rgba(0,0,0,0.30));
  /* Glass panel base — one elevation step LIGHTER than --surface-page in
     both themes. Dark: surface-page + 8% white tint → visibly lighter.
     Light: surface-page + 50% white tint → pushes toward pure white.
     Every glass surface (navbar, FAB panel, sub-header) mixes this
     with transparent to get its glass fill. */
  --glass-base: color-mix(in oklch, var(--surface-page), white 8%);

  /* ── Glass-panel surface recipe — single source of truth ──────────
     The shared surface for EVERY floating glass panel and menu —
     `.navbar::before`, `.cmd-palette__panel`, `.dropdown__panel`,
     `.ds-doc-menu-panel`, `.playground-rail`, and any future glass
     surface. They all read as the same surface (untinted, neutral,
     brand-tinted edge) because they all consume these three tokens.
     If you find yourself writing a panel background + shadow + blur
     by hand, reach for these tokens instead — never fork the recipe.

     Recipe lifted to navbar parity in May 2026 (was a one-tier-lower
     panel glass before then). The +12% white mix on the bg + accent
     halo on the shadow are what make the surface feel "premium" — that
     quality now ships on every glass panel, not just the navbar. The
     navbar's hand-rolled override at `.navbar:hover::before` is
     retired; both navbar and panels share this recipe.

     Light-mode shadow values are overridden in [data-theme="light"]
     below; the surface fill auto-flips via --glass-base.                  */
  --surface-panel-glass: color-mix(in oklch,
    color-mix(in oklch, var(--glass-base), white 12%) 78%,
    transparent);
  --shadow-panel-glass:
    inset 0 1px 0 rgba(255,255,255,0.12),
    0 12px 40px -12px color-mix(in oklch, #000 55%, transparent),
    0 2px 8px -4px color-mix(in oklch, var(--accent-primary) 10%, transparent);
  --backdrop-panel-glass: blur(40px) saturate(140%);

  /* ── Materials — surface character bundles ────────────────────────
     A material is the visible character of a surface — fill + border
     + shadow + backdrop-filter + edge highlight, bundled and named.
     Components consume the resolved layer (--mat-*) and stay
     material-agnostic; the active material is chosen via
     `data-material="…"` on the element, or via a :where(<class>)
     default for components that always wear one material.

     Three layers:
       1. DEFINITIONS  --material-<name>-{bg,border,shadow,backdrop,highlight}
                       The full character of each named material. Light/dark
                       flips happen at the upstream tokens this layer composes
                       (--surface-*, --elev-*, --shadow-panel-glass), so the
                       Materials block needs NO [data-theme="light"] override.
       2. RESOLVED API --mat-{bg,border,shadow,backdrop,highlight}
                       What components actually consume. Defaults to PAPER at
                       :root; [data-material] selectors below override per-element.
       3. CONSUMERS    .card { background: var(--mat-bg); ... }
                       Reference --mat-*, never reference the definitions
                       directly inside a component rule. That keeps cards
                       material-agnostic and lets [data-material] swap them.

     OKLCH at the definition site only. Materials compose --surface-*,
     --lifi-* and accent tokens; never bake hex/oklch literals here
     (the LI.FI 1.0 FOUC bug pattern — see CLAUDE.md → "LI.FI 1.0 is
     the only default" and "feedback_lifi-2-fouc-derivative-tokens").

     Catalog: #materials in design-system/index.html.
     Spec:    design.md §02 → Materials. */

  /* Paper — opaque solid + soft drop + 1px hairline. The default for
     stat-cards, feature-cards, action-cards, action containers.
     References --surface-card (theme-aware) so it lifts above the
     canvas in both modes — dark = 24% lightness, light = pivots to
     --surface-page so cards lift via shadow alone. */
  --material-paper-bg:           var(--surface-card);
  --material-paper-border:       1px solid color-mix(in oklch, var(--text-primary) 6%, transparent);
  --material-paper-shadow:       var(--elev-1);
  --material-paper-shadow-hover: var(--elev-2);
  --material-paper-backdrop:     none;
  --material-paper-highlight:    0 0 0 0 transparent;

  /* Glass — translucent + backdrop-blur + neutral drop + edge highlight.
     Aliases the canonical Panel Surfaces 3-token recipe so the visual
     diff is zero for existing glass consumers (.ds-doc-menu-panel,
     .dropdown__panel, .playground-rail). The inset edge highlight is
     baked INTO --shadow-panel-glass already, so --material-glass-highlight
     stays empty (don't double-stack it).

     May 2026 — `frosted` retired and merged into glass. Marketing tiles
     (.tile / .feature-card / .enterprise-card) now wear glass by default. */
  --material-glass-bg:           var(--surface-panel-glass);
  --material-glass-border:       0 solid transparent;
  --material-glass-shadow:       var(--shadow-panel-glass);
  --material-glass-shadow-hover: var(--shadow-panel-glass);   /* glass is already a floating surface; hover keeps the shadow */
  --material-glass-backdrop:     var(--backdrop-panel-glass);
  --material-glass-highlight:    0 0 0 0 transparent;

  /* `--accent-tint` parametric base — consumed by halo and gradient
     backgrounds, plus any per-component accent wash. Override per
     element via inline style or per-component CSS. */
  --accent-tint:                  var(--lifi-ink);

  /* Gradient — brand gradient fill + minimal chrome. Hero KPIs, marketing
     accent surfaces, cover tiles. Composes --lifi-sapphire + --lifi-ink so
     the gradient flips through any palette switch via the brand tokens. */
  --material-gradient-bg:           linear-gradient(135deg, var(--lifi-sapphire), var(--lifi-ink));
  --material-gradient-border:       1px solid color-mix(in oklch, white 12%, transparent);
  --material-gradient-shadow:       var(--elev-2);
  --material-gradient-shadow-hover: var(--elev-3);
  --material-gradient-backdrop:     none;
  --material-gradient-highlight:    inset 0 1px 0 rgba(255,255,255,0.18);

  /* Halo — soft accent-tinted card with a radial glow anchored to the
     top-right corner. Promoted from the .brand-card / .tile.brand-card
     visual signature (May 2026). The recipe is two layers stacked into
     one `background:` value:
       1. Top: radial-gradient ellipse at top-right → 12% accent fading
          to transparent at 70% reach. The "halo."
       2. Bottom: 3% accent over --surface-card. The base wash that
          gives the card its faint brand identity.
     Pairs with paper-style hairline border + --elev-1 drop. Use for
     brand-identity cards, premium feature tiles, "about this product"
     showcases — anywhere a card should feel quietly brand-forward
     without going full gradient. Parametric via --accent-tint. */
  --material-halo-bg:           radial-gradient(ellipse 85% 65% at 100% 0%,
                                  color-mix(in oklch, var(--accent-tint) 12%, transparent) 0%,
                                  transparent 70%),
                                color-mix(in oklch, var(--accent-tint) 3%, var(--surface-card));
  --material-halo-border:       1px solid color-mix(in oklch, var(--text-primary) 6%, transparent);
  --material-halo-shadow:       var(--elev-1);
  --material-halo-shadow-hover: var(--elev-2);
  --material-halo-backdrop:     none;
  --material-halo-highlight:    0 0 0 0 transparent;

  /* ── Resolved API — what components consume. PAPER is the default.
     Override per-element via [data-material="…"] (defined after the
     [data-theme="light"] block below) or per-component via a
     :where(<class>) default. Never reference --material-*-* inside a
     component rule; always read --mat-*. */
  --mat-bg:           var(--material-paper-bg);
  --mat-border:       var(--material-paper-border);
  --mat-shadow:       var(--material-paper-shadow);
  --mat-shadow-hover: var(--material-paper-shadow-hover);
  --mat-backdrop:     var(--material-paper-backdrop);
  --mat-highlight:    var(--material-paper-highlight);

  /* Navbar-scoped surface fills — used by the marketing navbar's
     mega-dropdown panel and mobile menu. Distinct from the surface
     ladder (these are translucent, scoped to nav chrome only). */
  --nav-panel-bg:   oklch(9.5% 0.019 268 / 0.95);   /* mega-dropdown */
  --nav-mobile-bg:  oklch(8%   0.019 268 / 0.97);   /* mobile menu   */

  /* ──────────────────────────────────────────
     Text Hierarchy  (Dark mode defaults)
     WCAG contrast on --surface-page background:
       --text-primary ≈ 18 : 1  ✓✓
       --text-secondary ≈ 9 : 1  ✓✓
       --text-muted     ≈ 5 : 1  ✓
       --text-faint     ≈ 3 : 1  (decorative only)
     ────────────────────────────────────────── */
  /* Text — neutral white ALPHA ladder (the LI.FI 1.0 default; the default
     preset carries no `text` record, so the per-theme text-tint axis stays
     dormant until a tinted theme is applied — check-theme-tokens.py TC5
     verifies this mirror in both directions). */
  --text-primary:   oklch(98% 0.0066 280);       /* primary text — near-white on ink (text-tint axis) */
  --text-secondary: oklch(76% 0.066 280);        /* secondary — support tone #A9AEDC                  */
  --text-muted:     oklch(56% 0.0759 280);       /* muted — metadata #6A70A0                           */
  --text-faint:     oklch(44% 0.066 280);        /* faint/decorative                                   */

  /* ──────────────────────────────────────────
     Icon colour family (May 2026) — semantic tokens for icon-only chrome
     (borderless icon buttons, dropdown carets, list-row affordances).
     Today these consume the text tokens so icons read at the same weight
     as the labels they sit next to. Defined as their own tokens so they
     can diverge from text colour later (e.g., chrome icons darker than
     body type, or a brand-tinted icon set on a hero surface) by editing
     ONE line each, without touching every consumer's rule. Always
     reference `--icon-*` (not `--text-*`) from icon-bearing components.
     ────────────────────────────────────────── */
  --icon-primary:   var(--text-primary);   /* matches text today; overridable per-theme below */
  --icon-muted:     var(--text-muted);     /* matches text today; overridable per-theme below */

  /* ──────────────────────────────────────────
     Deprecated aliases  (backwards compat — migrate to new names)
     ────────────────────────────────────────── */
  --navy:  var(--surface-page);        /* deprecated — use --surface-page */
  --white: var(--text-primary);   /* deprecated — use --text-primary */
  --text-2: var(--text-secondary); /* deprecated — use --text-secondary */
  --text-3: var(--text-muted);    /* deprecated — use --text-muted */
  --text-4: var(--text-faint);    /* deprecated — use --text-faint */
  --gray:   var(--text-muted);    /* deprecated — use --text-muted */
  --deep-blue: var(--surface-raised);   /* deprecated */
  --brand-blue: var(--accent-primary);    /* deprecated — use --accent-primary */
  --brand-pink: var(--accent-tertiary);   /* deprecated — use --accent-tertiary */

  /* ──────────────────────────────────────────
     Backwards-compat aliases — for tokens that used to live in
     lifi-design-system.css (the LI.FI 2.0 reference stylesheet).
     That file was unloaded from every DS page in Apr 2026 v4 to
     fix a flash-of-wrong-palette bug; this block re-exposes the
     same token names so any consumer (page CSS, inline styles,
     `style="…"` attributes, downstream HTML) continues to render
     without modification.

     Resolution strategy — wherever possible, alias to a canonical
     `--accent-*` / `--surface-page` / `--text-*` token via `color-mix`,
     so the value tracks the active Theme Composer preset AND
     auto-flips with `[data-theme="light"]`. The few hard-coded
     OKLCH values below sit in the neutral ramp (very low chroma)
     where hue parity matters less.

     New code should NOT reach for these — use the canonical name
     listed on each line. The old-name aliases stay so legacy
     surfaces don't break; nothing forces a rename.
     ────────────────────────────────────────── */

  /* Spacing — short numeric scale; 1:1 with --space-* (4px base unit). */
  --sp-1:  var(--space-4);    /* 4px  — micro gap                          */
  --sp-2:  var(--space-8);    /* 8px  — base                               */
  --sp-3:  var(--space-12);   /* 12px                                      */
  --sp-4:  var(--space-16);   /* 16px                                      */
  --sp-5:  var(--space-20);   /* 20px                                      */
  --sp-6:  var(--space-24);   /* 24px — default component padding          */
  --sp-8:  var(--space-32);   /* 32px — card / panel chrome                */
  --sp-10: var(--space-40);   /* 40px                                      */
  --sp-12: var(--space-48);   /* 48px                                      */
  --sp-16: var(--space-64);   /* 64px — section gutter (tight)             */
  --sp-20: var(--space-80);   /* 80px — section (default)                  */
  --sp-24: var(--space-96);   /* 96px — section (loose)                    */

  /* Border radius — these are the system's only --r-* tokens; future
     work might rename to --radius-*, but for now `--r-*` IS canonical. */
  --r-sm:   4px;
  --r-md:   8px;
  --r-lg:   12px;
  --r-xl:   16px;
  --r-full: 9999px;

  /* Ink scale — neutral surface↔text ramp.
     Each step is a `color-mix` between page background and primary text,
     which auto-flips between dark and light themes (no override needed).
     Conventional reading: 0 = page bg, 900 = primary text — same orientation
     in both themes, the underlying values just swap. */
  --ink-0:   var(--surface-page);
  --ink-50:  color-mix(in oklch, var(--surface-page)      92%, var(--text-primary)  8%);
  --ink-100: color-mix(in oklch, var(--surface-page)      85%, var(--text-primary) 15%);
  --ink-200: color-mix(in oklch, var(--surface-page)      75%, var(--text-primary) 25%);
  --ink-300: color-mix(in oklch, var(--surface-page)      60%, var(--text-primary) 40%);
  --ink-400: color-mix(in oklch, var(--surface-page)      45%, var(--text-primary) 55%);
  --ink-500: color-mix(in oklch, var(--surface-page)      30%, var(--text-primary) 70%);
  --ink-600: color-mix(in oklch, var(--surface-page)      22%, var(--text-primary) 78%);
  --ink-700: color-mix(in oklch, var(--surface-page)      14%, var(--text-primary) 86%);
  --ink-900: var(--text-primary);

  /* `--lifi-*-light` — pale background variants of the brand accents.
     Track the active palette automatically; readable on either theme
     because they're mixed against `--surface-page`. */
  --lifi-sapphire-accessible: var(--accent-primary);   /* AA-tuned variant; the active palette is already AA */
  --lifi-pink-light:      color-mix(in oklch, var(--accent-secondary) 14%, var(--surface-page));
  --lifi-ink-light:      color-mix(in oklch, var(--accent-tertiary)  14%, var(--surface-page));

  /* ──────────────────────────────────────────
     Alpha Surface Scale  (Dark mode defaults)
     Role-stable tokens for depth layering.
     Dark mode: white overlays lighten to elevate.
     Light mode: black overlays darken to recess,
                 white overlays lighten to elevate.
     Components use these instead of raw color-mix()
     so zero [data-theme] overrides are needed.
     ────────────────────────────────────────── */
  --alpha-dim:      oklch(100% 0 0 / 0.04);  /* wells, dividers, subtle bg   */
  --alpha-subtle:   oklch(100% 0 0 / 0.08);  /* tracks, input bgs, cards     */
  --alpha-moderate: oklch(100% 0 0 / 0.12);  /* hover on subtle, containers  */
  --alpha-raised:   oklch(100% 0 0 / 0.18);  /* active tabs, elevated items  */
  --alpha-overlay:  oklch(100% 0 0 / 0.24);  /* hover on raised, popovers    */

  /* On-surface text — alpha foreground for layered UIs */
  --on-alpha-faint:   oklch(100% 0 0 / 0.35);  /* disabled, decorative       */
  --on-alpha-muted:   oklch(100% 0 0 / 0.55);  /* secondary, inactive tabs   */
  --on-alpha-default: oklch(100% 0 0 / 0.80);  /* primary readable text      */
  --on-alpha-strong:  oklch(100% 0 0 / 1.00);  /* emphasis, active labels    */

  /* Alpha border scale */
  --alpha-border-dim:    oklch(100% 0 0 / 0.05);
  --alpha-border-subtle: oklch(100% 0 0 / 0.10);
  --alpha-border-strong: oklch(100% 0 0 / 0.18);

  /* ──────────────────────────────────────────
     Adaptive Overlays
     (flip automatically via [data-theme] block)
     ────────────────────────────────────────── */
  --overlay-hover:  oklch(100% 0 0 / 0.04);   /* subtle mouse-over tint  */
  --overlay-press:  oklch(100% 0 0 / 0.09);   /* icon bg / pressed state */
  /* Brand-tinted overlay derives from --lifi-sapphire so first paint matches
     the active palette (LI.FI 1.0 by default). The composer's runtime
     override at shared.js → buildTokens replaces this when palettes change. */
  --overlay-brand:  color-mix(in oklch, var(--lifi-sapphire) 10%, transparent);

  /* State overlay recipe (sticky-active) lives at .tile / .feature-card /
     .enterprise-card — see comment above that ruleset. Defining it at
     :root would pre-resolve var(--tile-accent) here (unset → fallback
     to --accent-primary), baking brand-blue into the inherited value
     before consumers can override. Scoped to the tile family so
     var(--tile-accent) resolves per-element at compute time. */

  /* ──────────────────────────────────────────
     Adaptive Borders / Dividers
     ────────────────────────────────────────── */
  --border-subtle: oklch(100% 0 0 / 0.06);
  --border-brand:  color-mix(in oklch, var(--lifi-sapphire) 15%, transparent);
  /* Content-aware swatch edge — the canonical swatch hairline (June 2026).
     A % of the INK (which flips per mode via --text-primary), NOT a fixed
     white border: a faint dark line in light mode, a faint light line in
     dark mode, and invisible on fills that already contrast the page
     (Sapphire, Pink). Only "shows" where the fill is near the page tone —
     Ink on dark, Paper on light — exactly where an edge is needed. Defined
     on :root (not .swatch) so EVERY swatch template resolves it — the card
     tile, --label-inside, AND the standalone .swatch-ramp__step (which is
     not a .swatch descendant). Tunable. */
  --swatch-edge: color-mix(in oklch, var(--text-primary) 12%, transparent);

  /* ──────────────────────────────────────────
     Field Tokens — semantic input chrome
     Neutral surface elevation (white overlays on dark),
     theme-independent contrast. Inputs sit alongside
     buttons / segmented controls and inherit this chrome.
     ────────────────────────────────────────── */
  /* Inputs read as a LIGHTER, raised field on the dark canvas (June 2026).
     Neutral overlays via --text-primary (pure white in dark) — palette-token,
     composer-safe (not brand-derived). */
  --field-bg:            color-mix(in oklch, var(--text-primary) 12%, transparent);
  --field-bg-hover:      color-mix(in oklch, var(--text-primary) 16%, transparent);
  --field-bg-focus:      color-mix(in oklch, var(--text-primary) 20%, transparent);
  --field-border:        color-mix(in oklch, var(--text-primary) 12%, transparent);
  --field-border-hover:  color-mix(in oklch, var(--text-primary) 22%, transparent);
  --field-border-focus:  var(--accent-primary-mid);
  --field-ring:          color-mix(in oklch, var(--accent-primary) 22%, transparent);
  --field-placeholder:   var(--text-faint);

  /* ──────────────────────────────────────────
     Spacing System — 8px grid (4px half-step)
     ──────────────────────────────────────────
     Two layers:
       1. CORE SCALE — raw px values, indexed by value. Use when a
          bespoke number is needed. Always prefer the semantic layer.
       2. SEMANTIC TOKENS — named by intent (pad-chrome, stack-md,
          pad-section-y). Responsive: they remap to smaller core
          values at ≤900px and ≤600px in the breakpoints block.
     Rules:
       • Every px value in component CSS SHOULD be a --space-* token.
       • Canonical scale: 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96,
         120, 160. 4 is the only half-step (micro gaps: badges,
         chip clusters, icon-to-label spacing).
       • Do NOT introduce 10, 12, 14, 18, 20, 22, 26, 28, 44, 52 —
         snap to the nearest grid value.
       • Line-height, font-size, border-radius, and border-width
         have their own scales and are exempt.
     ────────────────────────────────────────── */
  /* Core scale — index = px.
     Half-grid steps (12, 20) live between the main 8px multiples.
     Use sparingly — prefer the main grid (8/16/24/32) whenever the
     component allows. 12 fills the gap between "tight" and
     "comfortable"; 20 between "comfortable" and "default padding". */
  /* Geometry rule (Apr 2026 sweep): every component dimension —
     width, height, padding, margin, gap, border-radius — must
     resolve to a --space-* token below. Anything off the 4px grid
     is a bug (the only allowed exceptions are 1px / 2px hairlines
     for borders and dividers). When sizing a new component, snap
     to the nearest token; if no existing token fits, audit whether
     the size is really necessary before adding a new one. */
  --space-0:    0;
  --space-2:    2px;     /* hairline / divider — borders, focus rings, tight inset wrappers (.seg track offset, etc.) */
  --space-4:    4px;     /* half-step — micro gaps only       */
  --space-8:    8px;     /* 1× — base unit, tight gaps        */
  --space-12:   12px;    /* 1.5× — tight comfortable gap      */
  --space-16:   16px;    /* 2× — comfortable gaps             */
  --space-20:   20px;    /* 2.5× — card inner gutter          */
  --space-24:   24px;    /* 3× — default component padding    */
  --space-28:   28px;    /* 3.5× — fills 24↔32 (icon-only xs+) */
  --space-32:   32px;    /* 4× — card / panel chrome          */
  --space-36:   36px;    /* 4.5× — fills 32↔40 (icon-only sm) */
  --space-40:   40px;    /* 5× — generous chrome              */
  --space-44:   44px;    /* 5.5× — fills 40↔48 (touch target) */
  --space-48:   48px;    /* 6× — hero form card               */
  --space-52:   52px;    /* 6.5× — fills 48↔56 (icon-only lg) */
  --space-56:   56px;    /* 7× — reserved                     */
  --space-64:   64px;    /* 8× — section gutter (tight)       */
  --space-72:   72px;    /* 9× — fills 64↔80 (token row / dense list-item--lg) */
  --space-80:   80px;    /* 10× — section (default)           */
  --space-96:   96px;    /* 12× — section (loose)             */
  --space-120:  120px;   /* 15× — hero section                */
  --space-160:  160px;   /* 20× — giant hero                  */

  /* ──────────────────────────────────────────
     Surface widths — `--w-*` (17-tier ordinal ladder, +32px step)
     ──────────────────────────────────────────
     Standard widths for floating containers (popovers, menus, panels,
     modals, command palette, side rails, drawers, sheets). Ordinal
     numeric naming — `--w-N+1` is always the next step up. Pixel
     value can be derived as `192 + N × 32` (so `--w-1` = 224, `--w-12`
     = 576), but the step is the contract; the value is implementation
     detail. The same way `--space-12` reads as "12px of space",
     `--w-7` reads as "tier 7 of the surface ladder".

     Names are deliberately abstract — they don't bake-in a surface
     type — so future components (popovers, drawers, pickers, sheets)
     can claim a tier without forking the convention. Pick the nearest
     tier; "step up" = N+1, "step down" = N-1, no skips, no exceptions.
     If a real surface needs a width outside the ladder, inline the
     `max-width` on the element with a comment — don't fork the ladder. */
  --w-1:  224px;  /* tightest tooltip / pill popover                           */
  --w-2:  256px;  /* compact info popover, mini-menu                           */
  --w-3:  288px;  /* dropdown menus, single-column pickers, FAB sub-panel      */
  --w-4:  320px;  /* narrow popover, empty-state text cap                      */
  --w-5:  352px;  /* (reserved tier — claim for a new surface type)            */
  --w-6:  384px;  /* small modal (confirm) · compact rail · swap rail          */
  --w-7:  416px;  /* widget canvas, content panel, side rail, swap canvas      */
  --w-8:  448px;  /* (reserved tier — claim for a new surface type)            */
  --w-9:  480px;  /* default modal — settings, forms                           */
  --w-10: 512px;  /* command palette overlay, wide popover, mid-density modal  */
  --w-11: 544px;  /* (reserved tier — claim for a new surface type)            */
  --w-12: 576px;  /* large modal · workshop drawer · side sheet                */
  --w-13: 608px;  /* (reserved tier — claim for a new surface type)            */
  --w-14: 640px;  /* (reserved tier — claim for a wide panel)                  */
  --w-15: 672px;  /* (reserved tier — claim for a wide panel)                  */
  --w-16: 704px;  /* Advanced-tier Limit receive panel (chart + 7-col Orders)  */
  --w-17: 736px;  /* (reserved tier — claim for a wide panel)                  */

  /* ──────────────────────────────────────────
     Type Scale — grounded in the spacing grid
     ──────────────────────────────────────────
     Every font-size and line-height lands on a --space-* token
     when rendered at the default 16px root (1rem = 16px). Authored
     in rem/em so user zoom + OS text-size scaling stay intact, but
     the rendered pixel grid is anchored to the same 8px+4px ladder
     the rest of the system uses.

     Size rem → px → spacing step:
       5rem     = 80px   → --space-80
       3.5rem   = 56px   → --space-56
       3rem     = 48px   → --space-48
       2rem     = 32px   → --space-32
       1.5rem   = 24px   → --space-24
       1rem     = 16px   → --space-16
       0.75rem  = 12px   → 3× --space-4  (half-step)
       0.5rem   = 8px    → --space-8
     ────────────────────────────────────────── */
  /* Font sizes — authored in rem; every value resolves to a
     --space-* token at the default 16px root. Everything sits on the
     8px main grid EXCEPT --text-sm and --text-caption, which land on
     the 4px half-grid. --text-sm is the single documented exception
     for dense UI (buttons, inputs, table cells, mono labels). */
  /* Mega display scale — for presentation decks, poster-scale
     website statements, single-word monuments, and word-by-word
     animated reveals. NEVER use on paragraphs, lists, or any
     multi-line editorial copy. Line-heights are tight (0.94–0.96
     ratio) so the cap-height hugs the next line — the large-
     headline rule applied one step further. Pair with the
     matching `--lh-*` below; the fluid utility clamps size to
     the viewport so these never overflow on small screens. */
  --text-colossal:  16rem;     /* 256px — single-word monument    */
  --text-keynote:   12rem;     /* 192px — deck headline (16:9)    */
  --text-statement:  9rem;     /* 144px — bold website statement  */
  --text-poster:     7rem;     /* 112px — sub-hero / dense slide  */

  --text-hero:     5rem;       /* 80px → --space-80       */
  --text-display:  3.5rem;     /* 56px → --space-56       */
  --text-h1:       3rem;       /* 48px → --space-48       */
  --text-h2-lg:    2.25rem;    /* 36px → 9× --space-4 (half-grid) — value-display tier; fills the 32↔48 gap for large numeric values (amount-card, limit-price-card, chart price) */
  --text-h2:       2rem;       /* 32px → --space-32       */
  --text-h3:       1.5rem;     /* 24px → --space-24       */
  --text-body-xl:  1.25rem;    /* 20px → --space-20 (oversized lede — page-level subtitles, hero intros) */
  --text-body-lg:  1.125rem;   /* 18px → 4.5× --space-4 (half-grid) — large body, fills the 16↔20 gap; xl button labels, intro paragraphs */
  --text-body:     1rem;       /* 16px → --space-16       */
  --text-sm:       0.875rem;   /* 14px → 3.5× --space-4 (half-grid) */
  --text-caption:  0.75rem;    /* 12px → 3× --space-4  (half-grid) */
  --text-micro:    0.625rem;   /* 10px → 2.5× --space-4 (half-grid) — slider ticks, badge superscripts, dense overlay glyphs */

  /* Line-heights — authored in rem so the baseline lands on a
     spacing step. Ratios shrink as size grows (large-headline rule). */
  --lh-colossal:   15rem;      /* 240px → --space-240 (ratio ~0.9375) */
  --lh-keynote:    11.25rem;   /* 180px → --space-180 (ratio ~0.9375) */
  --lh-statement:   8.5rem;    /* 136px → --space-136 (ratio ~0.944)  */
  --lh-poster:      6.75rem;   /* 108px → --space-108 (ratio ~0.964)  */

  --lh-hero:       5rem;       /* 80px → --space-80   (ratio 1.0)    */
  --lh-display:    4rem;       /* 64px → --space-64   (ratio ~1.143) */
  --lh-h1:         3.5rem;     /* 56px → --space-56   (ratio ~1.167) */
  --lh-h2:         2.5rem;     /* 40px → --space-40   (ratio 1.25)   */
  --lh-h3:         2rem;       /* 32px → --space-32   (ratio 1.333)  */
  --lh-body-xl:    2rem;       /* 32px → --space-32   (ratio 1.6 — oversized lede) */
  --lh-body-lg:    1.75rem;    /* 28px → --space-28   (ratio ~1.56 — large body, intro paragraphs) */
  --lh-body:       1.5rem;     /* 24px → --space-24   (ratio 1.5)    */
  --lh-sm:         1.25rem;    /* 20px → 5× --space-4 (ratio ~1.43)  */
  --lh-caption:    1rem;       /* 16px → --space-16   (ratio ~1.333) */

  /* Tracking (em) — scales with font-size. */
  --tracking-crunch:  -0.05em;   /* colossal / keynote statements    */
  --tracking-tighter: -0.04em;   /* hero, statement, metric numbers  */
  --tracking-tight:   -0.03em;   /* display, h1, h2        */
  --tracking-firm:    -0.02em;   /* value-display tier — paired with --text-h2-lg; between snug and tight */
  --tracking-snug:    -0.015em;  /* h3                     */
  --tracking-normal:   0;        /* body copy              */
  --tracking-loose:    0.06em;   /* small uppercase labels */
  --tracking-wide:     0.1em;    /* caption uppercase      */
  --tracking-wider:    0.14em;   /* eyebrow uppercase      */

  /* Font weights — named tokens so weight becomes a controllable
     axis like size, line-height, and tracking. The CSS spec defines
     100–900; the ladder covers the seven weights the codebase
     actually uses. Pair with `--text-*` sizes; never write literal
     weight values in new code (see CLAUDE.md "Typography tokens").
       300 — light display, oversized hero numbers (rare; soft-spoken)
       400 — body, prose, captions, footers
       500 — emphasised body, ghost / placeholder display type
       600 — headings, button labels, chip labels, eyebrows
       700 — display, hero, statement, primary metric numbers
       800 — heavy display, page headers, marketing impact type
       900 — extreme display, single-keyword statements (very rare) */
  --fw-light:      300;
  --fw-regular:    400;
  --fw-medium:     500;
  --fw-semibold:   600;
  --fw-bold:       700;
  --fw-extrabold:  800;
  --fw-black:      900;

  /* ── Semantic — component chrome ──────────────────────────
     Inner padding of interactive panels. Tiered by surface
     density: sm for dense lists, md default, lg for form
     cards, xl for hero panels. */
  --pad-chrome-sm:  var(--space-16);
  --pad-chrome-md:  var(--space-24);
  --pad-chrome-lg:  var(--space-32);
  --pad-chrome-xl:  var(--space-48);

  /* ── Semantic — section rhythm ────────────────────────────
     Page-level vertical gutters. Horizontal gutter is the
     safe-area inset for container contents. */
  --pad-section-y-sm:  var(--space-64);
  --pad-section-y:     var(--space-96);
  --pad-section-y-lg:  var(--space-120);
  --pad-section-x:     var(--space-48);

  /* ── Semantic — form rhythm ───────────────────────────────
     Used by .form-group, .form-row, .form-card. */
  --gap-form-field:    var(--space-24);
  --gap-form-row:      var(--space-16);
  --gap-form-inline:   var(--space-8);

  /* ── Semantic — stack (vertical flow) ─────────────────────
     For gaps in vertical flex columns, list spacing,
     successive block margins. */
  --stack-xs:  var(--space-8);
  --stack-sm:  var(--space-16);
  --stack-md:  var(--space-24);
  --stack-lg:  var(--space-32);
  --stack-xl:  var(--space-48);
  --stack-2xl: var(--space-64);

  /* ── Semantic — cluster (horizontal flow) ─────────────────
     For gaps in horizontal flex rows, inline element groups,
     icon + label pairs. */
  --cluster-xs:  var(--space-4);
  --cluster-sm:  var(--space-8);
  --cluster-md:  var(--space-16);
  --cluster-lg:  var(--space-24);

  /* ── Semantic — card gap (card-to-card spacing) ───────────
     5-tier ladder for the gap between card-shaped siblings in a
     list or grid. Distinct from --stack-* (vertical content rhythm)
     and --cluster-* (inline element gaps). Token names describe
     POSITION on the ladder (xs/sm/md/lg/xl), NOT the use case —
     the use case is documentation, the tokens are the ladder. This
     decoupling is what makes the mapping swappable: a future "card
     gap composer" can override the five tier tokens at :root to
     install a Compact or Spacious preset, and every consumer
     updates via cascade — same mechanism the Theme Composer uses
     to swap brand colours.

     Consumption pattern — every card-list component publishes a
     local `--card-gap` CSS custom property that defaults to one
     of these tokens, then reads `gap: var(--card-gap)`. This
     gives a component-level default plus a per-instance escape
     hatch (override `--card-gap` inline or via context selector
     without touching component CSS).

     Authoring rule — consumers reach for the named tier
     (`var(--gap-card-md)`), never the raw `--space-*` underneath.
     Reaching past the tier to the raw value defeats the
     swappability. May 2026. */
  --gap-card-xs:     var(--space-4);   /* coupled pair — FAB at seam, joint surfaces */
  --gap-card-sm:     var(--space-8);   /* compact list — rail cards, dense menus */
  --gap-card-md:     var(--space-12);  /* default — quote cards, vertical card stacks */
  --gap-card-lg:     var(--space-16);  /* comfortable grid — multi-col, breathing room */
  --gap-card-xl:     var(--space-24);  /* spacious grid — hero-tier, marketing */

  /* Convenience alias — `--gap-card` (no modifier) resolves to md.
     Use `var(--gap-card-md)` in component CSS for explicitness;
     use `var(--gap-card)` for inline overrides where brevity helps. */
  --gap-card:        var(--gap-card-md);

  /* ──────────────────────────────────────────
     Canvas grid spacing — two-axis, composer-driven (sibling of the
     radius tokens above)
     ──────────────────────────────────────────
     The horizontal + vertical gaps of a content CANVAS (the Portal
     dashboard grid, the Playground widget stage). Two axes because the
     two rhythms differ by design: tight gutters BETWEEN side-by-side
     cards (x) vs. roomier breathing BETWEEN stacked sections (y).

       --grid-gap-x  →  grid column-gap (cards/panels in a row)
       --grid-gap-y  →  section-stack gap + grid row-gap (wrapped rows)

     Mode-independent (like --card-radius — no [data-theme] variant).
     Editable per-theme from the Theme Composer's "Spacing" section
     (theme-editor.js features.gaps), painted to the active canvas via
     window.lifiApplyGapsToScope. Consumed today by .portal-* canvas
     grids + .widget-stage; .playground-canvas overrides --grid-gap-x to
     24 (its historical gutter) — see swap.css. Full contract:
     CLAUDE.md → "Canvas grid-spacing system". */
  --grid-gap-x:      var(--space-16);
  --grid-gap-y:      var(--space-24);

  /* ──────────────────────────────────────────
     Card radius — single source of truth
     ──────────────────────────────────────────
     One variable wires every card-like surface (.card, .ui-*-card,
     .theme-card, .alert, .notification, .setting-row, modals,
     popovers …) to a single radius token. Default 16 px (canonical UI
     card recipe). Override at :root via `--card-radius` to flip every
     consumer in one motion — wired to the playground rail's "Card
     radius" customization drill (initRailCardRadius in swap.js).

     When authoring a new card-shaped primitive, consume var(--card-radius)
     instead of hand-picking from --space-*. Off-ladder radii (avatars
     at 50%, pills at --r-full, hairlines at 1-2px) keep their literal
     values — those aren't cards. */
  --card-radius:     var(--space-16);

  /* ──────────────────────────────────────────
     Panel radius — sibling token for container surfaces
     ──────────────────────────────────────────
     Independent slider from --card-radius. Panels are the "frames"
     (.panel — form panel, receive panel, playground rail) that HOLD
     cards. Decoupling lets a designer flip pillowy panels around sharp
     cards (or vice versa) without one ladder dragging the other.
     Default 16 px to match the canonical recipe; wired to the rail's
     "Panel radius" drill (initRailPanelRadius in swap.js). */
  --panel-radius:    var(--space-16);

  /* ──────────────────────────────────────────
     Panel padding — single source of truth for .panel internal padding
     ──────────────────────────────────────────
     Consumed by .panel below. Default --space-24 matches the historic
     hand-picked value. Per-instance overrides via the local custom
     property pattern: a consumer can set padding-block / padding-inline
     directly, or redefine --panel-padding in its scoped rule.

     Why the token exists: some panel contexts host content whose hover
     or active states need to render PAST the row's edge (e.g., the
     token picker's .list-item--interactive::before bleed). Reducing the
     panel's side padding makes room for that bleed without affecting
     the avatar/trail position (when paired with a compensating body
     padding inside the panel — see swap.css "Picker panel side-padding
     compensation" block). */
  --panel-padding:   var(--space-24);

  /* Panel gap — internal flex-column gap between a panel's slots/children.
     Was a hardcoded --space-12 literal inside .panel; promoted to a token so
     a roomier consumer (e.g. .ui-card at 16) tunes via override, not new CSS.
     Default --space-12 reproduces the prior .panel literal exactly. */
  --panel-gap:       var(--space-12);

  /* Panel surface (elevation axis) — the three tokens .panel CONSUMES for its
     fill + cast shadow + border. Defined on :root (NOT on .panel) so any
     ancestor scope — a content canvas — can flip them by inheritance, and the
     .panel--flat / .panel--elevated modifiers can override per-instance.
     Defaults = the elevated look: raised-FRAME fill + --elev-3 + no border.
     Flat flips to the CONTENT tier (--surface-card — white-capable in light,
     tracks the surface seed) + no shadow + hairline, because a flat panel is a
     content surface sitting on the canvas, not a raised frame.
     June 2026 — see CLAUDE.md → "Panel primitive". */
  --panel-surface:   var(--surface-raised);
  --panel-shadow:    var(--elev-3);
  --panel-border:    transparent;

  /* ──────────────────────────────────────────
     List hover bleed — magnitude of the row hover/active/selected pseudo
     that extends past each list row's L/R edges
     ──────────────────────────────────────────
     Single source of truth for "how far past the row does the hover
     surface extend." Consumed by four sites in lockstep (search
     `var(--list-hover-bleed)`):
       • .ui-token-list .list-item--interactive::before — pseudo inset
       • .ui-token-picker__head / __body — compensating L/R padding so
         the avatar/trail stay at the panel content edge
       • Picker panel side-padding override — reduces panel inline
         padding by exactly this magnitude to make room for the bleed
     Bumping this token shifts every consumer in one edit. Default
     --space-16 matches the canonical token picker layout (panel
     padding 24 - bleed 16 = 8 side inset; body padding 16 brings the
     avatar back to panel-outer + 24). */
  --list-hover-bleed: var(--space-16);

  /* ──────────────────────────────────────────
     Button radius — third sibling token for text buttons
     ──────────────────────────────────────────
     Anchors the stepped text-button radius ladder (re-introduced May 2026,
     reversing the brief "uniform 16" era): the token IS the lg tier, and
     the other text tiers derive via calc — sm = token−8 (8), md/default =
     token−4 (12), lg = token (16), xl = token+4 (20). Every .btn-icon
     tier (xs / sm / default / lg) consumes the bare token (uniform) so the
     square chrome buttons round in lockstep with the lg text button. Under
     the rail's "Button radius" customization, sliding the token scales the
     whole ladder while preserving the +4 steps. Default 16 px matches the
     canonical (lg-anchor) button shape. .btn-circle stays exempt (full
     radius pill — an explicit shape primitive). Wired to the rail's
     "Button radius" drill (initRailButtonRadius in swap.js). */
  --button-radius:   var(--space-16);

  /* ──────────────────────────────────────────
     Nested radius — concentric corners (a calc, not a token)
     ──────────────────────────────────────────
     When a rounded child sits inside a rounded, PADDED parent, the two
     corner curves only stay parallel — concentric, a constant gap all
     the way around the bend — when:

         outer radius  =  inner radius + padding
         inner radius  =  outer radius − padding    (the same equation)

     There is no --nest-radius token, because the value depends on two
     per-context inputs (the parent's radius + the gap to it). Express
     it inline with calc(). Each direction carries a clamp:

       • INWARD (deriving the child) — floor at 0 so a deeply-inset child
         goes square rather than negative (--space-N is the parent's pad):
           .child { border-radius: max(var(--card-radius) - var(--space-N), 0px); }
       • OUTWARD (deriving the frame/encompassing card) — ceiling at
         --space-32 so a large padding doesn't over-round the outer card
         (--space-N is the child's pad):
           .parent { border-radius: min(calc(var(--inner-radius) + var(--space-N)), var(--space-32)); }

     The two clamps are twins: inward floors at 0 (square), outward caps
     at 32 (calm). Concentric BETWEEN the clamps; flat past them.

     When padding ≥ the parent's radius the child's corners are sharp
     (0): the parent's straight edges have already started before the
     child's corner, so any rounding reads as crowded, not concentric.
     Mirror case outward: once seed + padding exceeds 32, the outer card
     stops tracking the child and reads as a calm rounded card instead of
     an over-bubbled one. .destination-card is the reference consumer.

     Scope: card/panel/button radius all default to a flat 16 (by
     design — see the Radius system note above). This calc is for
     DERIVING a nested element's radius when you want the curves to
     track; it is NOT auto-applied to existing nested cards. Full spec:
     design-system/index.html#nested-radius + CLAUDE.md → "Nested
     radius — concentric corners". */

  /* ──────────────────────────────────────────
     Gradients
     ──────────────────────────────────────────
     ALL brand gradients derive from the active --lifi-* tokens so first
     paint matches the active palette (LI.FI 1.0 by default — see the
     "LI.FI 1.0 is the only default" rule in CLAUDE.md).

     Earlier versions baked LI.FI 2.0 OKLCH values directly into these
     defaults, then relied on the Theme Composer's runtime override
     (shared.js → buildTokens) to swap them. That created a visible
     FOUC on every page load — hero painted 2.0 → JS ran → tokens
     rewritten → hero re-painted 1.0. By driving the defaults off the
     same --lifi-sapphire/pink/ink tokens that the composer mutates, both
     first paint AND runtime updates look right with no flash.

     The composer still overrides these tokens at runtime when applying
     non-default palettes — see shared.js:1153 for the override list. */
  --grad-brand:      linear-gradient(135deg, var(--lifi-sapphire) 0%, var(--lifi-pink) 100%);
  --grad-brand-text: linear-gradient(135deg, var(--lifi-sapphire) 0%, color-mix(in oklch, var(--lifi-sapphire), var(--lifi-pink)) 50%, var(--lifi-pink) 100%);
  --grad-brand-rich: linear-gradient(135deg, var(--lifi-sapphire) 0%, color-mix(in oklch, var(--lifi-sapphire) 75%, var(--lifi-pink)) 25%, color-mix(in oklch, var(--lifi-sapphire) 50%, var(--lifi-pink)) 50%, var(--lifi-pink) 100%);
  --grad-card-top:   linear-gradient(180deg, color-mix(in oklch, var(--lifi-sapphire) 22%, transparent) 0%, transparent 100%);

  /* Spectral radial — built from the 12-tone --spectral-* palette
     (not the brand pair). Aurora character: rose center fans through
     magenta and indigo to cyan at the edge, travelling roughly a
     half-turn of the hue wheel for a soft "northern lights" feel.
     Use as a plate behind transparent portraits when the surface
     itself should be the moment (hero photos, "select your archetype"
     tiles, marketing standout). Paired modifier:
     .avatar--plate-spectral-radial. */
  --grad-spectral-radial: radial-gradient(circle at 50% 35%,
    var(--spectral-12) 0%,
    var(--spectral-11) 33%,
    var(--spectral-9)  66%,
    var(--spectral-7)  100%);
}

/* ──────────────────────────────────────────────────────────────
   RESPONSIVE SEMANTIC SPACING REMAPS
   ──────────────────────────────────────────────────────────────
   Desktop values live in :root above. Here we step the named
   tokens down one rung on tablet, and again on phone. The raw
   --space-* scale is invariant; only *intent* tokens shift.
   Every consumer of --pad-chrome-lg, --pad-section-y, etc.
   adapts automatically — no per-component media queries needed.
   ────────────────────────────────────────────────────────────── */
@media (max-width: 900px) {
  :root {
    /* Chrome: tablet tightens one rung */
    --pad-chrome-xl:     var(--space-40);  /* 48 → 40 */
    --pad-chrome-lg:     var(--space-24);  /* 32 → 24 */
    --pad-chrome-md:     var(--space-16);  /* 24 → 16 */

    /* Section rhythm */
    --pad-section-y-lg:  var(--space-96);  /* 120 → 96 */
    --pad-section-y:     var(--space-80);  /* 96  → 80 */
    --pad-section-x:     var(--space-32);  /* 48  → 32 */

    /* Vertical stack */
    --stack-2xl:         var(--space-48);  /* 64 → 48 */
    --stack-xl:          var(--space-32);  /* 48 → 32 */
    --stack-lg:          var(--space-24);  /* 32 → 24 */
  }
}

@media (max-width: 600px) {
  :root {
    /* Chrome: phone is tightest */
    --pad-chrome-xl:     var(--space-32);  /* 48 → 32 */
    --pad-chrome-lg:     var(--space-16);  /* 32 → 16 */
    --pad-chrome-md:     var(--space-16);  /* 24 → 16 */
    --pad-chrome-sm:     var(--space-8);   /* 16 →  8 */

    /* Section rhythm */
    --pad-section-y-lg:  var(--space-80);  /* 120 → 80 */
    --pad-section-y:     var(--space-64);  /* 96  → 64 */
    --pad-section-y-sm:  var(--space-48);  /* 64  → 48 */
    --pad-section-x:     var(--space-24);  /* 48  → 24 */

    /* Form gaps tighten */
    --gap-form-field:    var(--space-16);  /* 24 → 16 */

    /* Stack tightens one more rung */
    --stack-2xl:         var(--space-40);  /* 64 → 40 */
    --stack-xl:          var(--space-24);  /* 48 → 24 */
    --stack-lg:          var(--space-16);  /* 32 → 16 */
    --stack-md:          var(--space-16);  /* 24 → 16 */
  }
}

/* ──────────────────────────────────────────────────────────────
   LIGHT MODE TOKEN OVERRIDES
   ──────────────────────────────────────────────────────────────
   Technique: mirror the dark lightness delta across the 50% axis.
   Dark bg L=8.5%  → Light bg L=97%   (8.5 + 97 ≈ 105.5, midpoint ≈ 52.75)
   Dark text L=98% → Light text L=14% (98 + 14 = 112, midpoint ≈ 56)
   This gives perceptually balanced contrast in both modes.
   ────────────────────────────────────────────────────────────── */
[data-theme="light"] {
  /* Surfaces — Paper page model (LI.FI 1.0; neutralHue 277, surfaceL 99).
     The page is Paper #FAFBFF (L99, cool hue 277) — NOT pure white; the
     fixed-white --surface-card tier (#FFFFFF, L100) lifts ABOVE it, so white
     cards separate from the page via a 1% tonal step + shadow. June 2026:
     Paper was lightened L98→L99 (#F7F8FD → #FAFBFF, designer call) — the
     page↔card step tightened 2%→1%, so the card now lifts on shadow + the
     1% step (verified cards still read). The whole light ladder shifts up 1
     (page surf, raised/sunk surf-3) except the fixed-white card. Mirrors the
     lifi-1 light surface seed { l:99, c:0.007, h:277 } (CSS can't read JS —
     keep the pair in lockstep). */
  --surface-page:          oklch(99%  0.007 277);   /* light page bg — Paper #FAFBFF (cool pale-blue) */
  --surface-raised:   oklch(96%   0.0076 277); /* light surface — below paper page (surf-3)     */
  --surface-sunk:     oklch(96%   0.007 277);  /* light recessed canvas — recess below Paper page (surf-3); level with --surface-raised (elevated panels separate via shadow), below the fixed-white --surface-card content tier the flat panels use */

  /* ── Elevation shadow tokens — LIGHT MODE ──────────────────
     Light-mode shadows are classic Material-style soft drops —
     no inset highlight needed (light surfaces show shadow well).
     Tier semantics match the dark-mode tokens; consumers don't
     branch on theme, they just reach for --elev-N. */
  --elev-1: 0 1px 2px rgba(0,0,0,0.06),
            0 1px 1px rgba(0,0,0,0.04);
  --elev-2: 0 4px 14px -2px rgba(0,0,0,0.10),
            0 2px 4px -1px rgba(0,0,0,0.05);
  --elev-3: 0 24px 56px -8px rgba(0,0,0,0.16),
            0 8px 16px -4px rgba(0,0,0,0.08);

  /* Filter-form of the elevation tokens — for MASKED elements ONLY. box-shadow
     is the canonical elevation method (see :root above + CLAUDE.md →
     "mask-image clips box-shadow"); this form exists only because mask-image
     clips box-shadow, so a masked surface must paint its lift via filter. Each
     token is VALUE-MATCHED to its --elev-N twin: --elev-1-filter mirrors
     --elev-1's two outset layers as a chained drop-shadow, so the lone masked
     surface (.ui-amount-pair) renders the same shadow as its box-shadow
     neighbours (e.g. .ui-destination-wallet-card right below it). Chained-
     compounding is negligible at these 1–2 px blurs; the smearing risk only
     bites at large radii. The inset rim-highlight layer has no drop-shadow
     equivalent and is dropped (light mode has no rim anyway). */
  --elev-1-filter: drop-shadow(0 1px 2px rgba(0,0,0,0.06))
                   drop-shadow(0 1px 1px rgba(0,0,0,0.04));
  --elev-2-filter: drop-shadow(0 4px 14px rgba(0,0,0,0.10))
                   drop-shadow(0 2px 4px rgba(0,0,0,0.05));
  --elev-3-filter: drop-shadow(0 24px 56px rgba(0,0,0,0.16))
                   drop-shadow(0 8px 16px rgba(0,0,0,0.08));
  /* Glass panel base — one elevation LIGHTER than surface-page. In light
     mode surface-page is already near-white, so we push to pure white for a
     visible lift. Kept in sync with the dark-mode intent (see :root). */
  --glass-base: color-mix(in oklch, var(--surface-page), white 70%);

  /* Light-mode override for the shared glass-panel shadow. --surface-
     panel-glass and --backdrop-panel-glass are unchanged (auto-flip
     via --glass-base). Light shadows are softer + less neutral-black so
     the panel doesn't read as dim against a near-white page. The 4th
     layer — a 1px accent ring — is light-mode-only; it gives the panel
     a crisp brand-tinted edge on near-white surfaces where neutral
     shadows alone wash out. Dark mode skips it (the inset highlight
     plus alpha contrast already define the edge there).                */
  --shadow-panel-glass:
    inset 0 1px 0 rgba(255,255,255,0.7),
    0 12px 40px -12px color-mix(in oklch, #000 20%, transparent),
    0 2px 8px -4px color-mix(in oklch, var(--accent-primary) 7%, transparent),
    0 0 0 1px color-mix(in oklch, var(--accent-primary) 6%, transparent);

  /* Materials — light-mode card tier.
     FIXED near-WHITE (June 2026 paper-and-card model): cards sit at true
     white ABOVE the light paper page — the symmetric counterpart of the
     dark side's fixed 28% tier. Replaces the May 2026 `var(--surface-page)`
     pivot. With the Paper page (#FAFBFF, L99 — LI.FI 1.0, the default) the
     white card (L100) lifts ABOVE the page via a 1% tonal step + shadow; white
     (100) is ≥ every light page, so a card can never recess. The Theme Composer
     mirrors this
     rule: deriveTokens writes `oklchStr(100, sc * 0.3, nh)` in light mode —
     for LI.FI 1.0 the surface chroma is a whisper (0.002), so the card is
     effectively pure white below.

     Shadows / borders / backdrop / glass-fill all flow through the
     upstream theme-flip without an override (--elev-*, --text-primary,
     --shadow-panel-glass, --glass-base already auto-flip).

     Spec: design.md §02 → Materials → Light-mode overrides. */
  --surface-card: oklch(100% 0.0021 277);


  /* Deprecated aliases */
  --navy:      var(--surface-page);
  --deep-blue: var(--surface-raised);

  /* Navbar surfaces flip to light-on-light */
  --nav-panel-bg:  oklch(97% 0 268 / 0.95);
  --nav-mobile-bg: oklch(97% 0 268 / 0.97);

  /* Text — neutral black ALPHA ladder (LI.FI 1.0 default; no `text` record
     on the default preset — TC5 mirror, see the dark block). */
  --text-primary:   oklch(10% 0.0078 274);     /* primary text — near-black ink on paper (text-tint axis) */
  --text-secondary: oklch(49% 0.026 274);      /* secondary — Slate #5C6070                                */
  --text-muted:     oklch(62% 0.026 274);      /* muted                                                    */
  --text-faint:     oklch(70% 0.0234 274);     /* faint/decorative                                         */

  /* Icon family — light-mode override slot. Today it consumes the text
     tokens, so icons match label weight on white surfaces. Override
     here when chrome icons should diverge from text in light mode
     (e.g., a slightly warmer black for icons on white). */
  --icon-primary:   var(--text-primary);
  --icon-muted:     var(--text-muted);

  /* Deprecated aliases */
  --white: var(--text-primary);
  --text-2: var(--text-secondary);
  --text-3: var(--text-muted);
  --text-4: var(--text-faint);
  --gray:   var(--text-muted);

  /* Light-mode spectral aliases (Apr 2026 v3)
     Re-point the theme-aware --spectral-N alias at the *-light primitives
     defined in :root. Same architecture used for accents, text, surfaces.
     See the dashboard-discipline comment in :root above for the why. */
  --spectral-1:  var(--spectral-1-light);
  --spectral-2:  var(--spectral-2-light);
  --spectral-3:  var(--spectral-3-light);
  --spectral-4:  var(--spectral-4-light);
  --spectral-5:  var(--spectral-5-light);
  --spectral-6:  var(--spectral-6-light);
  --spectral-7:  var(--spectral-7-light);
  --spectral-8:  var(--spectral-8-light);
  --spectral-9:  var(--spectral-9-light);
  --spectral-10: var(--spectral-10-light);
  --spectral-11: var(--spectral-11-light);
  --spectral-12: var(--spectral-12-light);

  /* Sequential aliases — light mode points at *-light primitives. */
  --seq-1: var(--seq-1-light);  --seq-2: var(--seq-2-light);  --seq-3: var(--seq-3-light);
  --seq-4: var(--seq-4-light);  --seq-5: var(--seq-5-light);  --seq-6: var(--seq-6-light);
  --seq-7: var(--seq-7-light);  --seq-8: var(--seq-8-light);  --seq-9: var(--seq-9-light);

  /* Diverging aliases — light mode points at *-light primitives.
     Note --div-5 (neutral) is identical across modes by design. */
  --div-1: var(--div-1-light);  --div-2: var(--div-2-light);  --div-3: var(--div-3-light);
  --div-4: var(--div-4-light);  --div-5: var(--div-5-light);  --div-6: var(--div-6-light);
  --div-7: var(--div-7-light);  --div-8: var(--div-8-light);  --div-9: var(--div-9-light);

  /* Light-mode accent overrides (LI.FI 1.0)
     Hue parity rule: light + dark MUST share the same hue value for
     each accent. Only lightness (and optionally chroma) differs across
     modes — that's how the same brand color reads as the "same color"
     in both themes. Drift here is the bug class that broke
     --accent-tertiary in light mode (hue 268 instead of 290 silently
     made tertiary indistinguishable from primary). */
  --lifi-sapphire:       oklch(52% 0.18 269);   /* primary   — Sapphire #405CCF (native; AA on paper 6.1:1) */
  --lifi-sapphire-mid:   oklch(62% 0.1584 269); /* primary   mid — hover states              */
  --lifi-sapphire-light: oklch(50% 0.054 269);  /* primary   light — tinted bg               */
  --lifi-pink:       oklch(88% 0.1 322);    /* secondary — Pink #F7C2FF (matches dark) */
  --lifi-ink:       oklch(24% 0.066 276);  /* tertiary  — Ink, deep band on paper (matches dark hue) */
  --lifi-dark:       oklch(9.5% 0.007 277); /* foundation — light derivation */
  --brand-pink: var(--lifi-ink);           /* deprecated alias */

  /* Jumper sibling-brand primitives — light pair (see :root block) */
  --jumper-indigo: oklch(30% 0.170 288);    /* renders #31007A — Indigo Purple */
  --jumper-purple: oklch(47% 0.237 312);    /* renders #8700B8 — Vivid Purple  */

  /* ── Semantic state tokens — light mode coefficient overrides ──
     The four --success / --danger / --warn / --info anchors are
     formula-derived in :root (see the doctrine block there). Light
     mode only needs to override the per-role coefficients (semantics
     shift darker for WCAG contrast on light surfaces) and the
     surface-tier constants (bg/border lift to high-L, low-C tints).

     Tuned via the Semantic Composer (May 2026 export) when
     anchor = oklch(44% 0.230 268):
       success: L=0.44+0.095=0.535, C=0.230*1.000=0.230, H=165
       danger:  L=0.44+0.140=0.580, C=0.230*1.000=0.230, H= 24
       warn:    L=0.44+0.280=0.720, C=0.230*1.300=0.299, H= 98
       info:    L=0.44+0.190=0.630, C=0.230*0.820=0.189, H=264

     H values flow through from :root via the cascade — only L+C
     overrides live here. */
  --sem-success-L-offset: 0.095;
  --sem-success-C-mult:   1.000;

  --sem-danger-L-offset:  0.140;
  --sem-danger-C-mult:    1.000;

  --sem-warn-L-offset:    0.280;
  --sem-warn-C-mult:      1.300;

  --sem-info-L-offset:    0.190;
  --sem-info-C-mult:      0.820;

  /* Surface-tier — light mode inverts: high-L pale tints + mid-L borders. */
  --sem-bg-L:     0.95;
  --sem-bg-C:     0.04;
  --sem-border-L: 0.80;
  --sem-border-C: 0.12;

  /* Spectrum Palette — light mode resolves via the --spectral-N → *-light
     alias block above (Apr 2026 v3 dashboard-discipline). The retired v2
     "vibrant" literal overrides (higher-chroma, near-gamut-ceiling) lived
     here and silently shadowed that alias, so the live light palette was
     v2 while design.md §Spectrum and personas.html were on v3. Removed so
     the canonical *-light primitives are the single light-mode source —
     matching the dark side's primitives+alias shape (no literal override).

     Series palette inherits via the var() chain in :root — no light-mode
     override needed here, since each --series-N points at a --spectral-N
     which is re-pointed to its *-light primitive in the alias block above. */

  /* Alpha Surface Scale — light mode
     Recess = black overlay (darken), Elevate = white overlay (lighten).
     --alpha-raised flips to white because the active surface should
     "float" above the darker track, matching the dark mode logic. */
  --alpha-dim:      oklch(0% 0 0 / 0.03);
  --alpha-subtle:   oklch(0% 0 0 / 0.06);
  --alpha-moderate: oklch(0% 0 0 / 0.09);
  --alpha-raised:   oklch(100% 0 0 / 0.88);   /* white — elevates above track */
  --alpha-overlay:  oklch(100% 0 0 / 0.95);   /* brighter hover on raised     */

  --on-alpha-faint:   oklch(0% 0 0 / 0.25);
  --on-alpha-muted:   oklch(0% 0 0 / 0.45);
  --on-alpha-default: oklch(0% 0 0 / 0.65);
  --on-alpha-strong:  oklch(0% 0 0 / 0.85);

  --alpha-border-dim:    oklch(0% 0 0 / 0.04);
  --alpha-border-subtle: oklch(0% 0 0 / 0.08);
  --alpha-border-strong: oklch(0% 0 0 / 0.14);

  /* Overlays flip: white tint → dark tint */
  --overlay-hover:  oklch(0%   0.015 268 / 0.05);
  --overlay-press:  oklch(0%   0.015 268 / 0.09);
  --overlay-brand:  color-mix(in oklch, var(--lifi-sapphire) 8%, transparent);

  /* Borders flip */
  --border-subtle: oklch(0%    0.015 268 / 0.08);
  --border-brand:  color-mix(in oklch, var(--lifi-sapphire) 20%, transparent);

  /* Field Tokens — light mode: black overlays darken / recess.
     Focus background flips to pure white so the field reads as
     "opened" rather than merely tinted. */
  /* Light mode: the field is the WHITE content surface (lighter than the
     off-white panels it sits on) — not the old grey-black well. Border is a
     neutral --text-primary overlay (pure black in light). */
  --field-bg:            var(--surface-card);
  --field-bg-hover:      var(--surface-card);
  --field-bg-focus:      var(--surface-card);
  --field-border:        color-mix(in oklch, var(--text-primary) 12%, transparent);
  --field-border-hover:  color-mix(in oklch, var(--text-primary) 22%, transparent);
  --field-border-focus:  var(--accent-primary-mid);
  --field-ring:          color-mix(in oklch, var(--accent-primary) 18%, transparent);
  --field-placeholder:   var(--text-faint);

  color-scheme: light;
}

/* ──────────────────────────────────────────────────────────────
   MATERIALS — [data-material] attribute selectors
   ──────────────────────────────────────────────────────────────
   Each selector wires one material's definitions onto the
   resolved --mat-* API. Specificity 0,1,0 — overrides the :root
   default (paper) and any :where(<class>) per-component default.

   Authoring rule: components read from --mat-*, never from
   --material-*-*. This block is the ONLY place selectors map
   the two layers; never duplicate the mapping inside a component
   rule. Light/dark flips happen at the upstream tokens, not here
   — there is intentionally NO [data-theme="light"] override for
   the resolved layer.

   Spec: design.md §02 → Materials.
   Catalog: #materials in design-system/index.html.
   ────────────────────────────────────────────────────────────── */
[data-material="paper"] {
  --mat-bg:           var(--material-paper-bg);
  --mat-border:       var(--material-paper-border);
  --mat-shadow:       var(--material-paper-shadow);
  --mat-shadow-hover: var(--material-paper-shadow-hover);
  --mat-backdrop:     var(--material-paper-backdrop);
  --mat-highlight:    var(--material-paper-highlight);
}
[data-material="glass"] {
  --mat-bg:           var(--material-glass-bg);
  --mat-border:       var(--material-glass-border);
  --mat-shadow:       var(--material-glass-shadow);
  --mat-shadow-hover: var(--material-glass-shadow-hover);
  --mat-backdrop:     var(--material-glass-backdrop);
  --mat-highlight:    var(--material-glass-highlight);
}
[data-material="gradient"] {
  --mat-bg:           var(--material-gradient-bg);
  --mat-border:       var(--material-gradient-border);
  --mat-shadow:       var(--material-gradient-shadow);
  --mat-shadow-hover: var(--material-gradient-shadow-hover);
  --mat-backdrop:     var(--material-gradient-backdrop);
  --mat-highlight:    var(--material-gradient-highlight);
}
[data-material="halo"] {
  --mat-bg:           var(--material-halo-bg);
  --mat-border:       var(--material-halo-border);
  --mat-shadow:       var(--material-halo-shadow);
  --mat-shadow-hover: var(--material-halo-shadow-hover);
  --mat-backdrop:     var(--material-halo-backdrop);
  --mat-highlight:    var(--material-halo-highlight);
}
/* ── Per-component material defaults — :where() = specificity 0
   so [data-material="…"] on the element overrides cleanly.

   Two component families have a "default material by class membership":
   the glass family (floating panels) and the paper family (content
   cards). Other components (utility primitives, form controls) don't
   set --mat-* and inherit the :root default (paper) only if their
   layout consumes those tokens. */

/* Glass-by-class — floating panels reserved for navigation chrome and
   workspace tools that sit over arbitrary content. The siblings below
   stay glass because the underlying content is visually load-bearing
   (the navbar reveals the page behind it; the cmd-palette overlays a
   working canvas; the FAB menu floats over a busy page).

   Migration history:
   - Apr 2026 — .panel switched to an opaque raised surface + --elev-3
     and removed from this list.
   - May 2026 — .playground-rail removed (composes .panel for its
     surface chrome, so the glass registration was dead weight).
   - May 2026 — .dropdown__panel moved to the paper registry below.
     Dropdowns now use a solid card surface; the glass blur read as
     "weird background blur" inside form rows and dense settings UIs
     where the panel's anchored to a specific trigger, not floating
     over the page. Glass remains reserved for navigation surfaces. */
:where(.ds-doc-menu-panel, .cmd-palette__panel) {
  --mat-bg:           var(--material-glass-bg);
  --mat-border:       var(--material-glass-border);
  --mat-shadow:       var(--material-glass-shadow);
  --mat-shadow-hover: var(--material-glass-shadow-hover);
  --mat-backdrop:     var(--material-glass-backdrop);
  --mat-highlight:    var(--material-glass-highlight);
}

/* Paper-by-class — UI-tier card primitives, the search-bar form
   control, and the dropdown menu panel. The :root default is already
   paper, but this rule is explicit so a card sitting inside a glass-
   tinted ancestor doesn't inherit glass. Override per-card with
   data-material on the element itself. */
:where(.stat-card, .card, .action-card, .search, .notification, .content-tile, .dropdown__panel) {
  --mat-bg:           var(--material-paper-bg);
  --mat-border:       var(--material-paper-border);
  --mat-shadow:       var(--material-paper-shadow);
  --mat-shadow-hover: var(--material-paper-shadow-hover);
  --mat-backdrop:     var(--material-paper-backdrop);
  --mat-highlight:    var(--material-paper-highlight);
}

/* Glass-by-class — marketing tile primitives. Promoted from frosted
   to glass when frosted was retired (May 2026); marketing tiles now
   wear the same translucent + backdrop-blur recipe as floating panels,
   giving them an "airy lifted" feel that fully-opaque paper can't
   match. Override per-card with data-material="paper" if a tile
   specifically needs an opaque surface (e.g. dense data grids where
   blur cost is undesirable). */
:where(.tile, .feature-card, .enterprise-card) {
  --mat-bg:           var(--material-glass-bg);
  --mat-border:       var(--material-glass-border);
  --mat-shadow:       var(--material-glass-shadow);
  --mat-shadow-hover: var(--material-glass-shadow-hover);
  --mat-backdrop:     var(--material-glass-backdrop);
  --mat-highlight:    var(--material-glass-highlight);
}

/* Halo-by-class — brand-identity cards. Source order matters: this
   rule comes AFTER the glass :where() above so .brand-card (which
   typically composes .tile.brand-card) wears halo, not glass.

   --mat-bg is inlined with var(--brand-c) directly instead of routing
   through --material-halo-bg. Reason: --material-halo-bg's value
   contains var(--accent-tint), which is substituted at :root scope
   when --material-halo-bg is computed there, locking it to :root's
   default --accent-tint (--lifi-ink). Per-element overrides of
   --accent-tint never propagate back. Inlining the gradient with
   var(--brand-c) here defers substitution to the consuming element
   where --brand-c resolves to the card's brand colour. */
:where(.brand-card) {
  --mat-bg:           radial-gradient(ellipse 85% 65% at 100% 0%,
                        color-mix(in oklch, var(--brand-c) 12%, transparent) 0%,
                        transparent 70%),
                      color-mix(in oklch, var(--brand-c) 3%, var(--surface-card));
  --mat-border:       var(--material-halo-border);
  --mat-shadow:       var(--material-halo-shadow);
  --mat-shadow-hover: var(--material-halo-shadow-hover);
  --mat-backdrop:     var(--material-halo-backdrop);
  --mat-highlight:    var(--material-halo-highlight);
}

html { scroll-behavior: smooth; font-family: var(--font-sans); background: var(--surface-page); }
body { background: transparent; color: var(--text-primary); overflow-x: hidden; isolation: isolate; }
a    { text-decoration: none; color: inherit; }

/* ══════════════════════════════════════════
   PAGE AMBIENT BACKGROUND — default on every page
   ══════════════════════════════════════════
   Two fixed pseudo-element layers behind all page content:
     ::before — top-left primary + top-right secondary (always on)
     ::after  — bottom-right primary + bottom-left secondary,
                mirrored from the top pair. Fades out on scroll so
                the bottom glows only show while the hero is in view.
   Both layers derive from the active palette via --accent-primary /
   --accent-secondary, so they follow every theme and preset automatically.
   Intensity is tuned per theme — heavier in dark, dialed back in light.
   A slow, very subtle drift animation keeps the layers feeling alive
   without drawing attention; disabled for prefers-reduced-motion users.
   The bottom-layer opacity is driven by --page-bg-bottom-opacity, set
   by initPageAmbientBackground() in shared.js (rAF-throttled; fades
   from 1 → 0 across the first viewport height of scroll).
   ══════════════════════════════════════════ */
body::before,
body::after {
  content: none; /* TEMP — page ambient glow paused; restore with content: '' */
  position: fixed;
  /* -6% inset gives a 112vw × 112vh box so drift translates up to ±3%
     never expose the viewport edge. Gradient positions are still
     interpreted relative to this (slightly larger) element box. */
  inset: -6%;
  pointer-events: none;
  z-index: -1;
  will-change: transform;
}
body::before {
  background:
    radial-gradient(ellipse 80% 55% at 30% -10%,
      color-mix(in oklch, var(--accent-primary) 26%, transparent) 0%,
      transparent 55%),
    radial-gradient(ellipse 45% 65% at 100% 25%,
      color-mix(in oklch, var(--accent-secondary) 20%, transparent) 0%,
      transparent 55%);
  animation: page-ambient-drift-a 22s ease-in-out infinite;
}
body::after {
  background:
    radial-gradient(ellipse 80% 55% at 70% 110%,
      color-mix(in oklch, var(--accent-primary) 26%, transparent) 0%,
      transparent 55%),
    radial-gradient(ellipse 45% 65% at 0% 75%,
      color-mix(in oklch, var(--accent-secondary) 20%, transparent) 0%,
      transparent 55%);
  opacity: var(--page-bg-bottom-opacity, 1);
  transition: opacity 0.3s ease-out;
  animation: page-ambient-drift-b 28s ease-in-out infinite;
}
[data-theme="light"] body::before {
  background:
    radial-gradient(ellipse 80% 55% at 30% -10%,
      color-mix(in oklch, var(--accent-primary) 15%, transparent) 0%,
      transparent 55%),
    radial-gradient(ellipse 45% 65% at 100% 25%,
      color-mix(in oklch, var(--accent-secondary) 12%, transparent) 0%,
      transparent 55%);
}
[data-theme="light"] body::after {
  background:
    radial-gradient(ellipse 80% 55% at 70% 110%,
      color-mix(in oklch, var(--accent-primary) 15%, transparent) 0%,
      transparent 55%),
    radial-gradient(ellipse 45% 65% at 0% 75%,
      color-mix(in oklch, var(--accent-secondary) 12%, transparent) 0%,
      transparent 55%);
}

/* Drift — small translate, fast enough to read as gentle motion
   without distracting. The two pseudos use different periods so
   they drift out of phase and never line up. */
@keyframes page-ambient-drift-a {
  0%, 100% { transform: translate3d(0, 0, 0); }
  33%      { transform: translate3d(-3%, 1.8%, 0); }
  66%      { transform: translate3d(2.2%, -1.4%, 0); }
}
@keyframes page-ambient-drift-b {
  0%, 100% { transform: translate3d(0, 0, 0); }
  40%      { transform: translate3d(2.6%, -2%, 0); }
  70%      { transform: translate3d(-1.8%, 1.6%, 0); }
}
@media (prefers-reduced-motion: reduce) {
  body::before, body::after {
    animation: none;
    transition: none;
  }
}


/* ══════════════════════════════════════════
   3. NAVBAR & MEGA DROPDOWN
   The whole navigation — logo, links, buttons — is wrapped inside a
   single floating pill spanning the viewport with a 20 px gutter.
   The glass surface is rendered on ONE pseudo-element (#navbar::before)
   that expands downward when a dropdown opens. The dropdown panel
   itself carries no background — it's just content laid on top of the
   extended pseudo. That keeps a single backdrop-filter pass across
   both the nav row and the submenu area (two separate backdrop-filter
   surfaces composite slightly differently at the seam, which reads as
   a visible secondary gradient — not what we want).
   ══════════════════════════════════════════ */
/* Navbar family — `.navbar-<surface>`. Shared chrome (pill shape, sticky
   position, glass-on-scroll, dropdown expansion) lives on a combined
   selector; only `.nav-*` children differ + a few surface-specific
   extras (the logo slot diverges: `.navbar-site__logo` vs the docs
   navbar's `.brand-lockup--back`).
   May 2026 v3 — reverted the v2 visual split (.navbar-docs as flush
   doc bar). The pill is the family default again; future product
   navbars on OUR surfaces (.navbar-scan, etc.) clone the shared
   chrome and only diverge where their content / surface-specific
   children require. The hero-pill metaphor is what makes the family
   feel like one component family rather than separate primitives.
   June 2026 — recreating a HOST/partner site's bar (jumper.xyz,
   lifi.fi-as-host) is NOT this family: that's the in-flow .host-nav
   below (flat, transparent, no glass pill / fixed positioning). */
.navbar-site,
.navbar-docs {
  position: fixed; top: 20px; left: 50%;
  transform: translateX(-50%);
  width: calc(100% - 40px); max-width: 1400px;
  height: auto; min-height: 72px;
  padding: 0 32px;
  z-index: 1000;
  background: transparent;
  backdrop-filter: none; -webkit-backdrop-filter: none;
  box-shadow: none;
  border: none;
  display: flex; align-items: center; justify-content: flex-start; gap: 0;
}
/* Visual pill on ::before so it can extend below the navbar bounds
   when a dropdown opens. --nav-dropdown-height is set by JS to each
   active dropdown's measured height; fallback keeps the effect usable
   without JS. */
.navbar-site::before,
.navbar-docs::before {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  border-radius: 28px;
  background: transparent;
  backdrop-filter: none; -webkit-backdrop-filter: none;
  box-shadow: none;
  pointer-events: none;
  z-index: -1;
  transition:
    background 0.25s cubic-bezier(0.4,0,0.2,1),
    backdrop-filter 0.25s cubic-bezier(0.4,0,0.2,1),
    -webkit-backdrop-filter 0.25s cubic-bezier(0.4,0,0.2,1),
    box-shadow 0.25s cubic-bezier(0.4,0,0.2,1),
    bottom 0.28s cubic-bezier(0.4,0,0.2,1);
}
.navbar-site:hover::before,
.navbar-site.scrolled::before,
.navbar-site.nav-dropdown-active::before,
.navbar-docs:hover::before,
.navbar-docs.scrolled::before,
.navbar-docs.nav-dropdown-active::before {
  /* Glass surface — consumes the canonical panel-glass recipe. */
  background: var(--surface-panel-glass);
  backdrop-filter: var(--backdrop-panel-glass);
  -webkit-backdrop-filter: var(--backdrop-panel-glass);
  box-shadow: var(--shadow-panel-glass);
}
.navbar-site.nav-dropdown-active::before,
.navbar-docs.nav-dropdown-active::before {
  bottom: calc(-1 * var(--nav-dropdown-height, 520px));
}

/* `.navbar-docs--preview` — overrides for the catalog's own Docs navbar
   card. ds-nav.js mounts this modifier when rendering into the
   #dsNavPreview placeholder so the same navbar markup that ships on
   live DS pages can also render inline in a catalog viewport pane.
   Strips fixed positioning + interactivity; keeps all visual chrome.
   Force-paints the glass pill (the live navbar only paints glass when
   :hover / .scrolled / .nav-dropdown-active — none of which fire on
   the inert preview, so the pill would otherwise read as transparent). */
.navbar-docs--preview {
  position: relative;
  top: auto; left: auto; right: auto;
  width: 100%; max-width: none;
  transform: none;
  pointer-events: none;
}
.navbar-docs--preview::before {
  background: var(--surface-panel-glass);
  backdrop-filter: var(--backdrop-panel-glass);
  -webkit-backdrop-filter: var(--backdrop-panel-glass);
  box-shadow: var(--shadow-panel-glass);
}

/* ── .host-nav — in-flow host-site navigation (embed preview) ─────────
   Recreation of a HOST site's top navigation, rendered in normal flow
   above an embedded widget — the playground's "Site preview" feature
   uses it to demo the swap widget sitting under jumper.xyz / lifi.fi
   chrome (body[data-nav-preview], swap.js → initRailNavPreview).

   Sibling of the .navbar-<surface> family but deliberately NOT a clone
   of its chrome: host navs are flat, transparent, in-flow bars (the
   host page's own background shows through) — no fixed positioning,
   no glass pill, no dropdown machinery. Reach for .navbar-site /
   .navbar-docs when building OUR site chrome; reach for .host-nav when
   recreating a partner/host site's bar as an embed-context preview.

   Anatomy (composable slots — omit what the brand doesn't use):
     __logo     brand lockup, inline SVG. In this EMBED PREVIEW the mark
                FOLLOWS THE ACTIVE THEME (so the mockup re-brands with the
                canvas + flips light/dark): LI.FI = currentColor; Jumper =
                two-tone — body on currentColor (--text-primary), spark on
                --accent-primary. Deliberate preview choice — the real logo
                assets (logos/jumper/*.svg), .brand-lockup, and downloads
                keep the --jumper-* brand constants per the Jumper
                sibling-brand rule (shipping logos never wire to an accent).
     __nav      tab cluster — the .seg.seg--borderless.seg--raised variant
                (NOT a bespoke link); .is-active rides the raised pill.
     __actions  trailing cluster (margin-left: auto) — compose REAL
                buttons (.btn-primary Connect, .btn-neutral.btn-icon.btn-circle …)

   Theming: EVERYTHING routes through tokens — the seg tabs, the .btn-*
   actions, the preview logo (--text-primary + --accent-primary), AND the
   font (the tabs read --font-sans) — so a scoped Theme Composer pick
   re-brands the whole bar end to end. Apply jumper-1 to the canvas and the
   Connect CTA goes pill-indigo, the logo re-tints, and the tabs become
   Urbanist (the jumper-1/-2 presets carry font: 'urbanist', which paints
   --font-sans + lazy-loads the family). Nothing pins a brand font on the
   preview — under a non-Jumper theme the tabs read that theme's sans, the
   honest inherit-the-theme behaviour. */
.host-nav {
  display: flex; align-items: center;
  gap: var(--space-40);
  min-height: var(--space-72);
  font-family: var(--font-sans);
}
.host-nav__logo {
  display: inline-flex; align-items: center;
  color: var(--text-primary);
  text-decoration: none;
  flex-shrink: 0;
}
/* overflow: visible — the LI.FI mark's corner curves bulge past the
   viewBox edge (see the lifi-mark rule above). */
.host-nav__logo svg { height: var(--space-24); width: auto; overflow: visible; }
.host-nav--lifi .host-nav__logo svg { height: var(--space-28); }
/* Jumper preview mark is two-tone, theme-following: the spark always takes
   Accent 2 (inline fill: var(--accent-secondary) on its path); the body +
   wordmark (currentColor here) take Accent 1 in light mode and Accent 3 in
   dark (a deliberate dark-only swap). LI.FI stays currentColor
   (--text-primary). Canonical brand assets (logos/jumper/*.svg) keep --jumper-*. */
.host-nav--jumper .host-nav__logo { color: var(--accent-primary); }
[data-theme="dark"] .host-nav--jumper .host-nav__logo { color: var(--accent-tertiary); }
/* Tab cluster — the bespoke .host-nav__links / .host-nav__link primitive
   retired June 2026 → the .seg.seg--borderless.seg--raised tab-bar variant.
   The raised card-pill active state (--surface-card + --elev-1, --r-full)
   and the neutral 4% hover that lived here moved verbatim into .seg--raised.
   The <nav> tab cluster now wears the seg classes + a thin .host-nav__nav
   positioning marker; the active tab is .seg-item.is-active. INCUBATING —
   see styles.css → .seg--raised + design-system/lab.html#incubator. */
.host-nav__actions {
  display: flex; align-items: center;
  gap: var(--space-12);
  margin-left: auto;
}
/* JS-rendered wallet slot — display:contents so the Connect button / account
   pill inside it participates directly in the actions flex row (correct gap
   to the kebab), without the slot wrapper adding a layout level. */
.host-nav__wallet { display: contents; }
/* LI.FI placeholder composition — the tab cluster sits RIGHT, beside the
   Connect CTA (lifi.fi shape), instead of trailing the logo (jumper.xyz
   shape). The seg cluster (.host-nav__nav) takes the auto margin; actions
   follows on the gap. */
.host-nav--lifi .host-nav__nav { margin-left: auto; }
.host-nav--lifi .host-nav__actions { margin-left: 0; }
/* Trailing icon button — compose a standard DS stack
   (.btn-neutral.btn-icon.btn-circle). The bespoke .host-nav__more
   card-surface circle was retired June 2026: it composed onto a bare
   .btn-icon (which doesn't reset UA <button> chrome) so the host page's
   default button border leaked as a hard ring. The neutral icon button
   composes the shared button base → no leak, proper hover/active states. */
/* .host-nav--jumper carries no font of its own — the bar follows the active
   theme's --font-sans like its colours + logo (June 2026). A Jumper THEME
   paints Urbanist via its own Typography axis (the jumper-1/-2 presets'
   font: 'urbanist'); the preview never force-pins it. The retired
   `.host-nav--jumper { font-family: var(--font-jumper) }` pin was inert
   anyway once the tabs became seg-items (which read --font-sans). The class
   stays purely as the data-nav-preview display hook. */

/* Logo — marketing-surface only (DS navbar uses .brand-lockup--back).
   BEM-scoped to .navbar-site since this is the one navbar child that
   genuinely diverges between surfaces. */
.navbar-site__logo {
  display: inline-flex; align-items: center; gap: var(--cluster-sm);
  font-size: var(--text-h3); font-weight: var(--fw-extrabold); letter-spacing: var(--tracking-snug);
  color: var(--text-primary); text-decoration: none;
}
.navbar-site__logo .lifi-logo-svg { height: 28px; width: auto; flex-shrink: 0; }

/* ── LI.FI mark / lockup — overflow: visible defense ──────────────────
   The LI.FI mark's cubic-bezier corner curves bulge to x≈38.628 in
   viewBox units — past the path endpoints at x=32 and past common crops
   at x=36. The canonical mark viewBox is `0 0 40 48`; the canonical
   lockup viewBox is `0 0 132 48` (wide enough on its own).

   Even with the correct viewBox, Chrome's UA stylesheet defaults inline
   <svg> to `overflow: hidden`, which silently overrides the SVG
   `overflow="visible"` *attribute* via the CSS cascade. So we set
   `overflow: visible` on every selector that hosts the mark.

   ⚠ Do not narrow this rule. Future designers will crop the lockup down
   to a mark — without this, they'll hit the corner-clip bug.
   Spec: design/components/foundations.md → "LI.FI mark — viewBox rule".
*/
.lifi-logo-svg,
.lifi-mark,
.brand-lockup__glyph-idle,
.brand-lockup__glyph-hover { overflow: visible; }

/* ── .brand-lockup — co-branded LI.FI sub-product header lockup ──────
   Pairs the LI.FI brand mark + wordmark with an optional sub-product
   name (Playground, Design System, Research, Scan, Composer, Checkout,
   etc.). The bare `.brand-lockup` is identity-only; the back-link
   hover swap (chevron → back-arrow) is OPT-IN via the `--back`
   modifier and only fires in real navigation contexts. Catalog
   previews intentionally omit `--back` so the lockup is purely a
   visual demonstration, not interactive.

   Anatomy (slot-based — consumer composes):
     .brand-lockup                       — wrapper (identity-only by default)
     .brand-lockup--back                 — opt-in: enables hover swap
                                           (apply only to real nav
                                           consumers — DS nav, product
                                           rails, future back-links)
       .brand-lockup__glyph              — mark slot
         .brand-lockup__glyph-idle       — LI.FI chevron mark
         .brand-lockup__glyph-hover      — back-arrow (only visible on
                                           --back surfaces, on hover)
       .brand-lockup__wordmark           — "LI.FI" SVG wordmark
       .brand-lockup__label              — sub-product name (optional)

   Tier: universal (no prefix). Used on product surfaces (Playground
   rail), DS catalog chrome (DS nav), and any future LI.FI sub-product
   that needs canonical co-branded chrome.
*/
.brand-lockup {
  display: inline-flex;
  align-items: center;
  gap: var(--space-4);
  color: var(--text-primary);
  text-decoration: none;
}
.brand-lockup__glyph {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--space-24);
  height: var(--space-24);
  flex-shrink: 0;
  color: var(--text-primary);
}
.brand-lockup__glyph-idle,
.brand-lockup__glyph-hover {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: var(--space-24);
  height: var(--space-24);
  color: var(--text-primary);
  transition: opacity 0.18s ease;
}
.brand-lockup__glyph-idle  { opacity: 1; }
.brand-lockup__glyph-hover { opacity: 0; }
.brand-lockup--back:hover .brand-lockup__glyph-idle  { opacity: 0; }
.brand-lockup--back:hover .brand-lockup__glyph-hover { opacity: 1; }
.brand-lockup__wordmark {
  height: var(--space-24);
  width: auto;
  flex-shrink: 0;
  color: var(--text-primary);
}
.brand-lockup__label {
  /* Sub-product name — Figtree uppercase, muted ink, weight 300 (Light)
     so it reads quietly next to the bold LI.FI wordmark. 16 px
     (--text-body) → ~11.5 px cap-height, visually flush with the
     wordmark's 12 px letters. line-height: 1 collapses leading so the
     label centers in the 24 px row. Inherits Figtree from <html>.
     `transform: translateY(0.5px)` is an OPTICAL correction, tuned at
     6× zoom (June 2026): at translateY(1px) the label's typographic
     baseline matched the LI.FI wordmark's EXACTLY (probe-measured
     delta 0.00) yet still READ ~1px low — Light-weight antialiasing
     paints soft pixels below the baseline and round caps (O/C/G/S)
     overshoot it by design, while the bold wordmark is a crisp vector
     path ending dead on the line. Lifting 0.5px centers the label's
     optical mass on the wordmark's. The --jumper variant carries its
     own value (below) because the JUMPER wordmark's baseline sits at a
     different ratio of its box (18/25 vs LI.FI's 36/48). */
  font-size: var(--text-body);
  line-height: 1;
  font-weight: var(--fw-light);
  letter-spacing: var(--tracking-normal);
  text-transform: uppercase;
  color: var(--text-muted);
  margin-left: var(--space-2);
  white-space: nowrap;
  transform: translateY(0.5px);
  transition: color 0.18s ease;
}
.brand-lockup--back:hover .brand-lockup__label { color: var(--text-secondary); }
/* ── .brand-lockup--jumper — Jumper sibling-brand variant ──
   Same slot anatomy, same geometry, same hover-swap contract; the
   glyph/wordmark SVGs carry the Jumper brand tokens (--jumper-indigo /
   --jumper-purple) in their own fills. The modifier owns exactly three
   label deltas: brand typography (Figtree → Urbanist via --font-jumper),
   brand colour (the sub-product name reads in accent 2 — --jumper-purple
   — instead of muted ink), and its own optical baseline nudge (the
   JUMPER wordmark's baseline sits at 18/25 of its box vs LI.FI's 36/48,
   a 0.72px geometric delta at the 24px render; −0.72 + the base rule's
   −0.5 optical lift, from the old +1 anchor, nets −0.25px — tuned at 6×
   zoom, June 2026). Don't fork further — the shared primitive is the
   contract. */
.brand-lockup--jumper .brand-lockup__label {
  font-family: var(--font-jumper);
  color: var(--jumper-purple);
  transform: translateY(-0.25px);
}
/* Keep the accent label through the --back hover — the base hover rule
   (muted → secondary grey) wins on specificity otherwise, and a purple →
   grey flicker reads as a bug against the brand colour. */
.brand-lockup--jumper.brand-lockup--back:hover .brand-lockup__label {
  color: var(--jumper-purple);
}
@media (max-width: 720px) {
  .brand-lockup__label { display: none; }
}

/* Nav links — right-aligned, larger text.
   No `position: relative` on .nav-links or its <li>s — .nav-dropdown
   (absolute-positioned) needs to fall back to .navbar (which is `fixed`,
   a valid positioning context) so the panel spans the full pill width
   instead of just the narrow trigger. */
.nav-links { display: flex; gap: var(--space-4); list-style: none; align-items: center; margin-left: auto; }

/* Full-width Stripe-style backdrop — retired. The dropdown itself now
   carries its own scoped glass surface (see .nav-dropdown below), so a
   400px full-width gradient is no longer needed. Rule kept as a stub
   in case we want to restore it behind a feature flag. */
#navLinks::after { display: none; }
/* ── Nav alpha transparency tokens ─────────────────────────────────────── */
/* Reusable alpha-based text colors for navigation elements.
   Derived from --white so they flip automatically with theme and palette. */
.nav-alpha         { color: color-mix(in oklch, var(--text-primary) 92%, transparent); }  /* primary */
.nav-alpha-hover    { color: var(--text-primary); }                                        /* hover   */
.nav-alpha-muted    { color: color-mix(in oklch, var(--text-primary) 55%, transparent); }  /* desc    */
.nav-alpha-subtle   { color: color-mix(in oklch, var(--text-primary) 35%, transparent); }  /* labels  */

[data-theme="light"] .nav-alpha        { color: color-mix(in oklch, var(--text-primary) 85%, transparent); }
[data-theme="light"] .nav-alpha-hover   { color: var(--text-primary); }
[data-theme="light"] .nav-alpha-muted   { color: color-mix(in oklch, var(--text-primary) 50%, transparent); }
[data-theme="light"] .nav-alpha-subtle  { color: color-mix(in oklch, var(--text-primary) 30%, transparent); }

/* Individual nav links — no per-item pill. Subtle text-color hover only;
   the whole-nav pill (see .navbar rules above) carries the glass surface. */
.nav-link-item {
  color: color-mix(in oklch, var(--text-primary) 92%, transparent); text-decoration: none;
  font-size: var(--text-sm); font-weight: var(--fw-medium); letter-spacing: var(--tracking-normal);
  /* Apr 2026: vertical padding 10px → 14px → 18px tracking the
     underline drop (now -14px below the label). The underline sits
     ~4 px above the link's bottom edge — comfortably inside the
     link's hit area. */
  padding: var(--space-20) var(--space-16); border-radius: var(--space-8);
  display: inline-flex; align-items: center; gap: var(--space-8);
  cursor: pointer; position: relative;
  background: transparent;
  /* Labels stay on a single line — multi-word categories like "Brand
     Guide" or "Use cases" must never wrap mid-word inside the navbar
     pill, no matter how cramped horizontally things get. If the
     navbar runs out of room the hamburger breakpoint takes over. */
  white-space: nowrap;
  transition: color 0.2s cubic-bezier(0.4,0,0.2,1);
}
/* Bottom indicator line — pinned to the .nav-link-label span (the
   text container) so the underline never extends under the chevron.
   Same recipe as .tab-nav-list's active underline (text-width only),
   plus a subtle box-shadow glow on active. */
.nav-link-item .nav-link-label {
  position: relative;
  display: inline-block;
}
.nav-link-item .nav-link-label::after {
  content: '';
  position: absolute;
  left: 0; right: 0;
  /* Drop the underline 14 px below the label baseline so it reads as
     a separate horizontal anchor, not tucked against the descenders.
     Drop progression: -6 → -10 → -14. Combined with the link's 18 px
     bottom padding, the underline still sits ~4 px above the link's
     bottom edge — comfortably inside the link's hit area. */
  bottom: -16px;
  height: 2px; /* hairline rule — border-style functional thickness */
  border-radius: 1px;
  background: var(--accent-primary);
  transform: scaleX(0);
  transform-origin: center;
  transition: transform 0.25s cubic-bezier(0.4,0,0.2,1);
}
.nav-link-item:hover,
.nav-links > li:hover   > .nav-link-item,
.nav-links > li.nav-open > .nav-link-item {
  color: var(--text-primary);
}
.nav-link-item:hover .nav-link-label::after,
.nav-link-item.active .nav-link-label::after,
.nav-links > li:hover   > .nav-link-item .nav-link-label::after,
.nav-links > li.nav-open > .nav-link-item .nav-link-label::after {
  transform: scaleX(1);
}
/* No glow on the active underline. Apr 2026 v4: dropped the stacked
   box-shadow bloom — the underline alone is a strong enough active
   signal, and the halo read as decorative chrome that pulled focus
   away from the page content beneath. Mirrored on .tab-nav-list
   a.is-active in the in-page tab nav. */
.nav-link-item .chevron {
  width: var(--space-12); height: var(--space-12); opacity: 0.4;
  transition: transform 0.3s, opacity 0.3s;
}
.nav-links > li:hover   > .nav-link-item .chevron,
.nav-links > li.nav-open > .nav-link-item .chevron {
  transform: rotate(180deg); opacity: 0.8;
}
[data-theme="light"] .nav-link-item { color: color-mix(in oklch, var(--text-primary) 85%, transparent); }
[data-theme="light"] .nav-link-item:hover,
[data-theme="light"] .nav-links > li:hover > .nav-link-item,
[data-theme="light"] .nav-links > li.nav-open > .nav-link-item {
  color: var(--text-primary);
}
/* EXPERIMENT (Apr 2026 v4): active label inherits the accent so the
   text and the underline read as one continuous signal instead of
   "dark text + blue underline." Placed AFTER the light-mode block so
   it wins source order in both themes (equal specificity otherwise).
   Reversible — drop this rule and the label falls back to base text. */
.nav-link-item.active {
  color: var(--accent-primary);
}

/* ── "More" overflow control — priority+ nav (ds-nav.js → initPriorityNav)
   The trailing control in .nav-links that collects tabs which don't fit the
   current width. The desktop row used to spill past the navbar between
   ~860px (hamburger breakpoint) and ~1330px (where 10 tabs stop fitting);
   priority+ closes that dead zone by collapsing the overflow here.

   ONE menu paradigm (June 2026): More is a standard `.nav-item` carrying
   the SAME `.nav-dropdown` mega panel as every other tab — hover machinery,
   glass-pill extension, the single `.nav-open` gate. (The earlier compact
   `.dropdown` + `.menu--sm` popover was retired so the navbar ships one
   expansion style, not two.) NO `position: relative` on the <li> — the
   mega `.nav-dropdown` must anchor to the navbar (its fixed containing
   block), not this <li>; a positioned <li> would shrink the full-width
   panel to the trigger's width.

   The trigger reuses the .nav-link-item look but is a <button>, so it needs
   the UA chrome reset the <a> tabs don't — and font-family must be re-asserted
   (the <button> UA resets it to Arial) WITHOUT a `font:` shorthand, which
   would clobber .nav-link-item's size/weight. Hover color / chevron rotate /
   underline all come free from the generic `.nav-links > li:hover / li.nav-open
   > .nav-link-item` rules above. */
.nav-links > li[hidden],
.nav-more[hidden] { display: none; }   /* [hidden] guard — flex items override
   the UA `[hidden]{display:none}` otherwise (CLAUDE.md recurring-bug rule). */
.nav-more__trigger {
  appearance: none; -webkit-appearance: none;
  border: 0; background: transparent;
  font-family: var(--font-sans);
}
/* Current-page marking for an overflowed tab's row — the accent voice the
   inline tab's underline would have carried. Produced only by ds-nav.js →
   initPriorityNav's overflow renderer (renderDropdownItem with `current`). */
.nav-dropdown-item.is-current .dd-title { color: var(--accent-primary); }

/* Dropdown panel — transparent overlay. The glass surface is provided by
   #navbar::before which extends down via --nav-dropdown-height when
   .nav-dropdown-active is set. The dropdown itself only hosts content.
   Content reveal is delayed so the pill expansion happens first. */
.nav-dropdown {
  position: absolute;
  top: 100%; left: 0; right: 0;
  width: auto;
  background: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border: none;
  border-radius: 0;
  box-shadow: none;
  opacity: 0; visibility: hidden; pointer-events: none;
  transform: translateY(-4px);
  /* Close: instant — content hides immediately so pill can shrink cleanly.
     Open: delayed — pill expands first (0.28s), then content fades in. */
  transition:
    opacity 0.2s ease,
    visibility 0s linear 0.2s,
    transform 0.2s ease;
  z-index: 998;
}
/* Bridge strip — guards the hairline seam between pill bottom and panel top */
.nav-dropdown::before {
  content: ''; position: absolute; top: -4px; left: 0; right: 0; height: 4px;
}
/* Content reveal is gated SOLELY on `.nav-open` — the same single JS state
   that drives the chrome pill (`.nav-dropdown-active`). One source of truth,
   so content + chrome can never desync. The old `li:hover` clause was an
   INDEPENDENT second gate: moving the cursor from the panel back up to the
   tab fired the panel's close timer (removing `.nav-open` → collapsing the
   pill) while real `:hover` kept the items painted on the canvas with no
   glass behind them. JS `li.mouseenter` already adds `.nav-open` the instant
   the cursor enters, so there is no open-latency gap. See CLAUDE.md →
   "Navbar dropdown desync" + bug-history. */
.nav-links > li.nav-open > .nav-dropdown {
  opacity: 1; visibility: visible; pointer-events: auto;
  transform: translateY(0);
  transition:
    opacity 0.22s ease 0.22s,
    visibility 0s linear 0s,
    transform 0.22s ease 0.22s;
}

/* Dropdown inner — side-by-side: wide left column with title/description
   takes up space, link grid right-aligns so columns sit near their trigger. */
.nav-dropdown-inner {
  max-width: none; margin: 0;
  padding: var(--space-40) var(--space-48);
  display: flex; gap: 0;
}
.nav-dropdown-left {
  flex: 1 1 0%; min-width: 380px; padding-top: 4px;
  margin-right: var(--space-48); position: relative;
}
/* Vertical divider between title area and link grid */
.nav-dropdown-left::after {
  content: ''; position: absolute; top: 0; right: 0; bottom: 0; width: 1px;
  background: linear-gradient(to bottom,
    transparent,
    var(--border-subtle) 30%,
    var(--border-subtle) 70%,
    transparent);
}
.nav-dropdown-left h3 {
  font-size: var(--text-h2); font-weight: var(--fw-extrabold); color: rgba(255,255,255,0.95);
  letter-spacing: var(--tracking-snug); margin: 0 0 var(--space-12);
}
[data-theme="light"] .nav-dropdown-left h3 { color: rgba(0,0,0,0.9); }
.nav-dropdown-left p {
  font-size: var(--text-body); color: rgba(255,255,255,0.45);
  line-height: var(--lh-body); margin: 0; max-width: 360px;
}
[data-theme="light"] .nav-dropdown-left p { color: rgba(0,0,0,0.4); }
/* Link grid — doesn't grow, keeps its natural column width. */
.nav-dropdown-right { flex-shrink: 0; min-width: 0; }
/* Flexible spacer on the right so the link columns appear centred
   between the left title column and the pill's right edge. */
.nav-dropdown-inner::after {
  content: '';
  flex: 1 1 0%;
  min-width: 0;
}
/* The link grid auto-reflows: as many ≥240px columns as fit the
   available width. The `.three-col` modifier acts as a HINT (3 if
   the inner has room for it, otherwise 2 or 1) rather than a forced
   count — that's what eliminates clipping at intermediate widths
   like 1280-1480px where a fixed 3-column grid would push the right
   column past the navbar pill's edge. */
.nav-dropdown-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--space-4) var(--space-8);
}
.nav-dropdown-grid.three-col {
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}

/* ── Nav-dropdown responsive drop-off ─────────────────────────────────
   The dropdown's title-and-tagline column on the left is a brand
   moment that works on roomy desktop viewports but becomes the
   load-bearing cause of right-column truncation under ~1280px.

   Strategy:
   - 1280-769 → drop the left column entirely. The dropdown trigger
                label already names the category (Developers / Products
                / etc.), so a duplicate title inside the panel is mostly
                decoration. Hiding it gives the link grid the full inner
                width and the rightmost column stops clipping.
   - 1100-769 → collapse 3-col grid to 2-col. Once you've reclaimed the
                left column's width, two columns at ~50% each is the
                comfortable shape; three columns starts squeezing items
                again.
   - ≤768    → existing rule (vertical stack) takes over.

   Note: capping `.nav-dropdown-inner` at `max-width: calc(100vw - 40px)`
   so a wide grid can never extend past the navbar pill's bounds, even
   under awkward intermediate widths. */
@media (max-width: 1280px) {
  .nav-dropdown-left {
    display: none;
  }
  /* Remove the flexible spacer that previously balanced the grid
     against the title column — without the title, no balance needed. */
  .nav-dropdown-inner::after {
    display: none;
  }
  /* Let the right column take the full inner width */
  .nav-dropdown-right {
    flex: 1 1 100%;
    min-width: 0;
    width: 100%;
  }
  .nav-dropdown-inner {
    max-width: calc(100vw - 40px);
    padding: var(--space-32) var(--space-32);
  }
}
/* No explicit 3-col → 2-col breakpoint needed: the base
   `repeat(auto-fit, minmax(…))` collapses the column count
   naturally as the inner shrinks. The 1280px rule above is the
   only structural break — everything below it handles itself
   until the 768px vertical-stack rule takes over. */
.nav-dropdown-item {
  display: flex; align-items: flex-start; gap: var(--cluster-md);
  padding: var(--space-16) var(--space-16); border-radius: var(--space-12); text-decoration: none;
  transition: background 0.3s cubic-bezier(0.4,0,0.2,1),
              transform 0.3s cubic-bezier(0.4,0,0.2,1);
}
.nav-dropdown-item::after { display: none; }
.nav-dropdown-item:hover { background: var(--overlay-hover); transform: translateX(4px); }
.nav-dropdown-item:hover .dd-icon  { background: var(--overlay-press); }
.nav-dropdown-item:hover .dd-title { color: var(--text-primary); }
[data-theme="light"] .nav-dropdown-item:hover .dd-title { color: var(--text-primary); }
.nav-dropdown-item:hover .dd-arrow { opacity: 1; transform: translateX(0); }
.dd-icon {
  width: var(--space-40); height: var(--space-40); min-width: var(--space-40); border-radius: var(--space-12);
  background: var(--overlay-press);
  display: flex; align-items: center; justify-content: center;
  transition: background 0.3s;
}
.dd-icon svg {
  width: var(--space-20); height: var(--space-20); stroke: var(--text-muted);
  fill: none; stroke-width: 1.5; stroke-linecap: round;
  stroke-linejoin: round; opacity: 0.7; transition: opacity 0.3s;
}
.dd-text { flex: 1; min-width: 0; }
.dd-title {
  font-size: var(--text-sm); font-weight: var(--fw-semibold);
  color: color-mix(in oklch, var(--text-primary) 88%, transparent);
  transition: color 0.3s; display: flex; align-items: center; gap: var(--space-8);
  /* Long product names ("Swap & Bridge SDK") shouldn't clip — let
     them wrap to a second line if the column is narrow. The `gap`
     above keeps the trailing arrow stuck to the last text line. */
  flex-wrap: wrap;
  overflow-wrap: break-word;
}
[data-theme="light"] .dd-title { color: color-mix(in oklch, var(--text-primary) 80%, transparent); }
.dd-arrow {
  opacity: 0; transform: translateX(-6px);
  transition: opacity 0.3s, transform 0.3s;
  font-size: var(--text-caption); color: color-mix(in oklch, var(--text-primary) 55%, transparent);
}
[data-theme="light"] .dd-arrow { color: color-mix(in oklch, var(--text-primary) 50%, transparent); }
.dd-desc {
  font-size: var(--text-caption);
  color: color-mix(in oklch, var(--text-primary) 45%, transparent);
  margin-top: var(--space-4); line-height: var(--lh-caption); font-weight: var(--fw-regular);
  /* Single-line description rule (Apr 2026) — see CLAUDE.md →
     "Single-line descriptions in menus and navigation". Truncates
     with ellipsis if the copy exceeds the column width; authors
     should shorten the copy rather than rely on the ellipsis. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
[data-theme="light"] .dd-desc { color: color-mix(in oklch, var(--text-primary) 40%, transparent); }
.nav-dropdown-divider {
  height: 1px;
  background: linear-gradient(to right, var(--border-subtle), transparent);
  margin: var(--space-8) 0 var(--space-4); grid-column: 1 / -1;
}
.nav-dropdown-label {
  font-size: var(--text-caption); font-weight: var(--fw-semibold);
  color: color-mix(in oklch, var(--text-primary) 35%, transparent);
  text-transform: uppercase; letter-spacing: var(--tracking-wide);
  padding: var(--space-16) var(--space-16) var(--space-8); grid-column: 1 / -1;
}
[data-theme="light"] .nav-dropdown-label { color: color-mix(in oklch, var(--text-primary) 30%, transparent); }

/* `.nav-dropdown--index` (+ `.nav-index-group/-col/-label/-link/-icon`)
   retired June 2026 — the 4-column sitemap dropdown variant had zero live
   consumers (ds-nav.js dropped `type: 'index'`; every DS tab uses the
   2-col titled list). Its `:hover`-gated reveal rules were also the same
   independent-gate anti-pattern as the navbar-dropdown desync (see
   bug-history → "Navbar dropdown desync"). Restore from git history if a
   future surface wants the multi-column index dropdown again. */

/* Staggered entry animation. Gated SOLELY on `.nav-open` (the single JS
   open-state) — NOT `:hover` — so the item reveal can't fire independently
   of the chrome. (A `:hover`-gated reveal was a second independent open gate
   that, paired with the now-removed parent `li:hover` rule, let items animate
   onto the canvas while the pill was collapsed. See the `.nav-dropdown`
   open-rule above + CLAUDE.md → "Navbar dropdown desync".) */
.nav-links > li.nav-open > .nav-dropdown .nav-dropdown-item {
  animation: ddItemIn 0.35s cubic-bezier(0.16,1,0.3,1) both;
}
.nav-links > li.nav-open > .nav-dropdown .nav-dropdown-item:nth-child(1) { animation-delay: 0.04s; }
.nav-links > li.nav-open > .nav-dropdown .nav-dropdown-item:nth-child(2) { animation-delay: 0.08s; }
.nav-links > li.nav-open > .nav-dropdown .nav-dropdown-item:nth-child(3) { animation-delay: 0.12s; }
.nav-links > li.nav-open > .nav-dropdown .nav-dropdown-item:nth-child(4) { animation-delay: 0.16s; }
.nav-links > li.nav-open > .nav-dropdown .nav-dropdown-item:nth-child(5) { animation-delay: 0.20s; }
.nav-links > li.nav-open > .nav-dropdown .nav-dropdown-item:nth-child(6) { animation-delay: 0.24s; }
@keyframes ddItemIn {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Theme toggle in navbar — sits at end of nav-links on both surfaces. */
.nav-theme-toggle-wrap {
  list-style: none;
  display: flex; align-items: center;
  margin-left: 8px;
}
.nav-theme-toggle-wrap .theme-toggle {
  margin-left: 0;
}
/* `.nav-cta` retired May 2026 — defined but never consumed in HTML/JS.
   If a future surface needs a button-group / CTA slot, author it as a
   BEM-scoped child of its navbar (e.g. `.navbar-site__cta`,
   `.navbar-scan__cta`) rather than reviving the bare `.nav-cta` name. */


.rwa-chip {
  display: inline-flex; align-items: center; padding: var(--space-4) var(--space-8);
  border-radius: var(--space-4); font-size: var(--text-caption); font-weight: var(--fw-bold); letter-spacing: var(--tracking-loose);
  background: var(--overlay-brand); color: var(--accent-primary);
  border: 1px solid var(--border-brand);
}


/* ══════════════════════════════════════════
   3.5 SIDE NAV — .side-nav  (tier-2 universal)
   ──────────────────────────────────────────
   Universal vertical navigation primitive. Used for:

   • Long-scroll reference rails with anchor + scroll-spy
     — compose with `.side-nav--sticky`. The catalog and
     dashboard pages run in this mode.
   • Destination-switching rails inside an arbitrary container
     — a widget's chrome, a settings drawer's left rail, a
     dashboard section nav. This is the DEFAULT mode (no
     viewport coupling, no scroll-spy assumption — set the
     active state from your own router).
   • Compact (`--sm`) and icon-rail (`--rail`) variants for
     dense product surfaces. Nested children via
     `__item--parent` + `__sub`. Pinned bottom slot via
     `__foot` (pairs with `.avatar`).

   History — promoted from `.ds-doc-sidebar` (tier-1 catalog-
   meta) to tier-2 universal in May 2026. Same playbook as the
   v8 chart-family promotion and v9 avatar promotion: the shape
   was already general; only the prefix and the catalog-coupled
   sticky/spy behaviour kept it filed at tier-1. Sticky/spy
   moves under `--sticky`; the base class is now a clean
   container.

   Structure:
     <aside class="side-nav side-nav--sticky">
       <div class="side-nav__group">
         <div class="side-nav__title">Foundations</div>
         <a href="#brand-colors" class="side-nav__item">
           <svg class="side-nav__icon">…</svg>
           <span class="side-nav__label">Brand Colors</span>
           <span class="side-nav__trail">
             <span class="badge badge--neutral">12</span>
           </span>
         </a>
       </div>
     </aside>

   Composition matrix:
     (default)       free-standing column, lives in any
                     container, externally-set active state
     --sticky        position:sticky + scroll-spy + mobile
                     accordion (catalog / dashboard mode)
     --sm            compact density (sizing-ladder tier)
     --rail          ~64px icon-only column; pair with the
                     existing `.tooltip` for label on hover

   Parts:
     __group         optional section wrapper
     __title         group label (uppercase mono caption)
     __item          one row (flex) — host on <a> (routing /
                     scroll-spy) or <button> (JS view-switching)
     __icon          16×16 leading glyph (Lucide @ stroke 1.5)
     __label         text slot — enables ellipsis truncation
     __trail         optional trailing slot (badge / count)
     __sub           nested children, paired with
                     __item--parent + [aria-expanded]
     __foot          pinned bottom slot — pairs with `.avatar`
   ══════════════════════════════════════════ */

/* Layout helper — pairs a side-nav with a sibling content
   column. Optional (the side-nav works without it). Kept
   namespaced as `.ds-doc-layout` for migration safety. */
.ds-doc-layout {
  display: flex;
  min-height: 100vh;
}
.ds-doc-content {
  flex: 1;
  min-width: 0;
}

/* Base — free-standing vertical column. No positioning quirks,
   no viewport assumption. Lives in whatever container the
   consumer puts it in. */
.side-nav {
  display: flex;
  flex-direction: column;
  width: 240px;
  min-width: 240px;
  padding: var(--space-16);
  font-family: var(--font-sans);
}

.side-nav__group + .side-nav__group {
  margin-top: var(--space-16);
}

/* Leading .search-trigger → first nav group — a category change
   (search affordance → nav), bumped slightly above inter-group
   rhythm so the trigger doesn't run into the first group title. */
.search-trigger + .side-nav__group {
  margin-top: var(--space-20);
}

.side-nav__title {
  font-family: var(--font-sans);
  font-size: var(--text-caption);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: color-mix(in oklch, var(--text-primary) 30%, transparent);
  padding: 0 var(--space-12);
  margin-bottom: var(--space-12);
}

/* Row — every nav row is `.side-nav__item`, on an `<a>` (routing /
   scroll-spy rails) or a `<button>` (JS view-switching). One
   explicit host class. The legacy bare-`<a>` descendant fallback
   (`.side-nav a`) was retired May 2026 — every consumer (the
   catalog sidebars + demos, 160 anchors across 6 pages) migrated
   to `.side-nav__item` atomically; no aliases. */
.side-nav__item {
  display: flex;
  align-items: center;
  gap: var(--space-12);
  padding: var(--space-8) var(--space-12);
  border-radius: var(--space-8);
  font-size: var(--text-sm);
  font-weight: var(--fw-medium);
  color: color-mix(in oklch, var(--text-primary) 55%, transparent);
  text-decoration: none;
  transition: color 0.2s ease, background 0.2s ease;
  margin-bottom: var(--space-4);
  /* Truncation context lives on `.side-nav__label`, not here
     — see CLAUDE.md → "Flex containers + text-overflow:
     ellipsis". */
  min-width: 0;
  /* Dual-host reset — .side-nav__item also renders on <button> (single-page
     view-switching), not just <a>. Neutralize the <button> UA chrome so both
     hosts render identically; no-op for <a>. font-family (NOT the `font`
     shorthand) so the rule's own font-size/weight stand. Per CLAUDE.md →
     ".menu-item on a <button> host leaks UA chrome". */
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
  border: 0;
  text-align: left;
  font-family: inherit;
  cursor: pointer;
  /* Fill the container so every row's hover/active fill is the same width.
     A block-level flex <a> stretches to fill its block parent, but a
     block-level flex <button> keeps its intrinsic content width — so without
     this, button-hosted rows (the Portal's single-page view-switchers) render
     ragged content-width pills while <a> rows fill. width:100% reconciles the
     two hosts; no-op for <a> and for the flex-column catalog rails where items
     already stretch. Same dual-host reset family as the UA-chrome block above. */
  width: 100%;
}

.side-nav__item:hover {
  color: var(--text-primary);
  background: var(--overlay-hover);
}

.side-nav__item.is-active,
.side-nav__item[aria-current="page"] {
  color: var(--accent-primary-mid);
  background: color-mix(in oklch, var(--accent-primary) 8%, transparent);
}

.side-nav__item.is-active .side-nav__icon,
.side-nav__item[aria-current="page"] .side-nav__icon {
  color: var(--accent-primary-mid);
}

.side-nav__icon {
  width: var(--space-16);
  height: var(--space-16);
  flex-shrink: 0;
  color: color-mix(in oklch, var(--text-primary) 30%, transparent);
  transition: color 0.2s ease;
}

.side-nav__item:hover .side-nav__icon {
  color: color-mix(in oklch, var(--text-primary) 70%, transparent);
}

/* Label — explicit truncation context. Wrap the text node in
   `<span class="side-nav__label">` when long labels are
   possible; bare text children render fine too (just won't
   truncate). */
.side-nav__label {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Trail — optional trailing slot. Hosts a `.badge`, a count,
   a chevron, or any small trailing widget. */
.side-nav__trail {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  gap: var(--space-4);
  color: color-mix(in oklch, var(--text-primary) 40%, transparent);
  font-size: var(--text-caption);
}

/* Foot — pinned bottom slot. Pair with `.avatar` for a user /
   team chip. The parent must have enough height for
   `margin-top: auto` to push the foot down (so this works
   naturally inside `--sticky`, which fills the viewport;
   in a free-standing rail set a `min-height` on the wrapper). */
.side-nav__foot {
  margin-top: auto;
  padding: var(--space-12);
  display: flex;
  align-items: center;
  gap: var(--space-12);
  border-top: 1px solid var(--border-subtle);
}
.side-nav__foot-title {
  font-size: var(--text-sm);
  font-weight: var(--fw-semibold);
  color: var(--text-primary);
  line-height: 1.2;
}
.side-nav__foot-sub {
  font-size: var(--text-caption);
  color: color-mix(in oklch, var(--text-primary) 50%, transparent);
  line-height: 1.2;
}

/* ── Nested children ───────────────────────────────────────
   A parent row uses `<button class="side-nav__item side-nav__item--parent" aria-expanded="false">`.
   Its sibling `<ul class="side-nav__sub">` is hidden by
   default and reveals when aria-expanded="true". Chevron
   rotates on open. JS owns the toggle. */
.side-nav__item--parent {
  cursor: pointer;
  border: 0;
  background: transparent;
  text-align: left;
  width: 100%;
  font: inherit;
}
.side-nav__item--parent::after {
  content: '';
  margin-left: auto;
  flex-shrink: 0;
  width: var(--space-12);
  height: var(--space-12);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
  transition: transform 0.2s ease;
  opacity: 0.6;
}
.side-nav__item--parent[aria-expanded="true"]::after {
  transform: rotate(180deg);
}

.side-nav__sub {
  display: none;
  list-style: none;
  padding: 0 0 0 var(--space-8);
  margin: 0 0 var(--space-4) calc(var(--space-16) + var(--space-12));
  border-left: 1px solid var(--border-subtle);
}
.side-nav__item--parent[aria-expanded="true"] + .side-nav__sub {
  display: block;
}
.side-nav__sub .side-nav__item {
  padding: var(--space-4) var(--space-12);
  font-size: var(--text-caption);
  margin-bottom: 0;
}

/* ── Variant: --lg (comfortable density) ───────────────────
   Sizing-ladder companion, one tier UP from the default row:
   icon 16→20, font 14→16, pad 8/12→12/16, radius 8→12 (≈48px
   row). For roomy product rails (the Portal) where the nav IS
   the primary surface, not a dense secondary list. The group
   title stays caption-size on purpose — an eyebrow reads as a
   quiet constant across tiers (the retired --sm shrank it only
   because micro density forced it). (--sm retired June 2026 —
   no consumers; see CLAUDE.md → "Primitive retirement is atomic".) */
.side-nav--lg .side-nav__item {
  padding: var(--space-12) var(--space-16);
  gap: var(--space-12);
  border-radius: var(--space-12);
  font-size: var(--text-body);
  margin-bottom: var(--space-4);
}
.side-nav--lg .side-nav__icon {
  width: var(--space-20);
  height: var(--space-20);
}
.side-nav--lg .side-nav__trail {
  font-size: var(--text-sm);
}
.side-nav--lg .side-nav__item--parent::after {
  width: var(--space-16);
  height: var(--space-16);
}
/* Drill children stay one notch quieter than top rows (14 vs 16),
   mirroring the default tier's 12-vs-14 step. (0,3,0) beats both
   the base sub rule and `.side-nav--lg .side-nav__item`. */
.side-nav--lg .side-nav__sub .side-nav__item {
  padding: var(--space-8) var(--space-12);
  font-size: var(--text-sm);
}

/* ── Variant: --rail (icon-only narrow column) ─────────────
   ~64px wide. Label + trail are hidden — use the existing
   `.tooltip` primitive on each item to surface the label on
   hover. Active state still tints background + icon. */
.side-nav--rail {
  width: 64px;
  min-width: 64px;
  padding: var(--space-12) var(--space-8);
  align-items: stretch;
}
.side-nav--rail .side-nav__title { display: none; }
.side-nav--rail .side-nav__item {
  justify-content: center;
  padding: var(--space-12);
  gap: 0;
}
.side-nav--rail .side-nav__label,
.side-nav--rail .side-nav__trail { display: none; }
.side-nav--rail .side-nav__icon {
  width: var(--space-20);
  height: var(--space-20);
}
.side-nav--rail .side-nav__foot {
  padding: var(--space-8);
  justify-content: center;
}
.side-nav--rail .side-nav__foot > *:not(.avatar) { display: none; }

/* ── Rail items are icon buttons (May 2026) ────────────────
   The canonical rail item is a .btn-icon.btn-borderless —
   the real interactive icon primitive — paired with a .tooltip
   for the label (icon-only buttons MUST carry an aria-label; see
   the a11y rule in the Rules pane at #side-nav-rail). The bespoke
   <a><svg class="side-nav__icon"> placeholder markup is retired:
   btn-icon brings hover / active-press / focus-visible states
   for free, so the rail only layers the selected fill below.
   Buttons keep their own 40×40 geometry, so the base .side-nav__item
   rule doesn't reach them — the rail just centers
   each item (button or tooltip-wrap) in its 64px column with the
   same inter-row rhythm the <a> rows used. */
.side-nav--rail > .btn-icon,
.side-nav--rail > .tooltip-wrap {
  align-self: center;
  margin-bottom: var(--space-4);
}
/* Selected — mirrors the rail's accent-tinted active (8% accent
   fill + accent-mid icon). btn-borderless owns the NEUTRAL hover
   (per the list-row recipe: rows hover neutral, commit reads accent);
   the rail owns the accent selected state. Shared with --topbar so
   both icon-button presentations read the same selected state. */
.side-nav--rail .btn-icon.is-active,
.side-nav--topbar .btn-icon.is-active {
  background: color-mix(in oklch, var(--accent-primary) 8%, transparent);
  color: var(--accent-primary-mid);
}
.side-nav--rail .btn-icon.is-active:hover,
.side-nav--topbar .btn-icon.is-active:hover {
  background: color-mix(in oklch, var(--accent-primary) 12%, transparent);
}

/* ── Variant: --topbar (horizontal icon-tab bar) ───────────
   Sibling of --rail (May 2026): the SAME icon-button items, laid
   out as a horizontal row with the family's `.side-nav__label`
   shown as a caption under each icon — a mobile tab bar moved to
   the top. --rail hides `.side-nav__label` and surfaces the label
   on hover via `.tooltip`; --topbar shows the label and drops the
   redundant tooltip. Surface-less like the rest of the family: the
   consumer applies background / radius / elevation on the .side-nav
   element. Generalized out of the retired swap-widget "topbar"
   mode-nav so the pattern is reusable on any surface. Live
   reference: #side-nav-topbar in design-system/index.html. */
.side-nav--topbar {
  flex-direction: row;
  width: auto;
  min-width: 0;
  justify-content: space-between;   /* tabs spread across the bar; override to center for a compact bar */
  align-items: center;
  gap: var(--space-16);
  padding: var(--space-12) var(--space-16);
}
.side-nav--topbar .side-nav__title { display: none; }
/* Each item stacks the icon button over its caption. .tooltip-wrap is
   reused as the item wrapper (same as --rail); the hover tooltip is
   hidden because the caption is always visible. */
.side-nav--topbar > .btn-icon,
.side-nav--topbar > .tooltip-wrap {
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: 0;
}
.side-nav--topbar .btn-icon {
  --btn-icon-size: var(--space-20);
  width: var(--space-40);
  height: var(--space-40);
}
.side-nav--topbar .tooltip { display: none; }
/* Caption — the family's `.side-nav__label`, reset from the full-
   sidebar row treatment (flex-grow + truncation) to a centered
   caption under the icon. */
.side-nav--topbar .side-nav__label {
  display: block;
  flex: 0 0 auto;
  overflow: visible;
  text-align: center;
  font-size: var(--text-caption);
  font-weight: var(--fw-medium);
  line-height: var(--lh-caption);
  color: var(--text-secondary);
}
/* Active item's caption tracks the accent, mirroring the icon's
   active tint — no per-item state class needed (:has on the wrapper). */
.side-nav--topbar .tooltip-wrap:has(.is-active) .side-nav__label {
  color: var(--accent-primary-mid);
}

/* ── Variant: --sticky (catalog / dashboard mode) ──────────
   Stick to viewport, fill the column height, host a mobile
   accordion under 900px. This is the mode every DS doc
   surface runs in (catalog index, dashboard, brand-guide,
   etc.). Product / widget consumers should NOT compose this
   modifier — use the default free-standing mode and drive
   `.is-active` (or `[aria-current="page"]`) from your own
   router. */
.side-nav--sticky {
  position: sticky;
  top: 96px;
  align-self: flex-start;
  /* Width — 260px to host the vertical `.seg.seg--vertical`
     scope filter at the top of the catalog rail. A 4-item
     horizontal seg ("All / UI / Marketing / Docs") needs
     ~280px of inline width to render "Marketing" without
     truncation; stacking vertically eats less horizontal
     space (~210px content area is enough) and reads as a
     real control rather than a squeezed pill row. */
  width: 260px;
  min-width: 260px;
  height: calc(100vh - 96px);
  overflow-y: auto;
  overflow-x: hidden;
  /* Top padding matches the first-child section's padding-top
     (var(--space-64)) so the rail's first item sits on the
     same horizontal line as the main content's eyebrow. */
  padding: var(--space-64) 16px 40px 24px;
  border-right: 1px solid var(--border-subtle);
  scrollbar-width: thin;
  scrollbar-color: color-mix(in oklch, var(--text-primary) 8%, transparent) transparent;
}
.side-nav--sticky::-webkit-scrollbar { width: 4px; }
.side-nav--sticky::-webkit-scrollbar-thumb {
  background: color-mix(in oklch, var(--text-primary) 8%, transparent);
  border-radius: 4px;
}

/* Mobile (≤900px) — `--sticky` collapses into a sticky
   single-open accordion under the navbar. Each
   `.side-nav__group` becomes a tappable header that reveals
   its own links. JS owns the `data-open="true"` state toggle
   (`shared.js → initDsSidebarAccordion`); CSS owns the
   reveal. The free-standing default does NOT change shape
   on mobile — consumers wrap it in their own responsive
   container if needed. */
@media (max-width: 900px) {
  .ds-doc-layout { flex-direction: column; }

  .side-nav--sticky {
    position: sticky;
    top: 0;
    width: 100%;
    min-width: 0;
    height: auto;
    max-height: 70vh;
    padding: var(--space-8) var(--space-16);
    gap: 0;
    background: color-mix(in oklch, var(--surface-page) 94%, transparent);
    backdrop-filter: blur(20px) saturate(140%);
    -webkit-backdrop-filter: blur(20px) saturate(140%);
    border-right: none;
    border-bottom: 1px solid color-mix(in oklch, var(--text-primary) 8%, transparent);
    z-index: 50;
    overflow-x: visible;
  }

  .side-nav--sticky .side-nav__group {
    width: 100%;
    display: block;
    border-bottom: 1px solid color-mix(in oklch, var(--text-primary) 6%, transparent);
    margin-top: 0;
  }
  .side-nav--sticky .side-nav__group:last-child { border-bottom: none; }

  .side-nav--sticky .side-nav__title {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;
    padding: var(--space-12) var(--space-8);
    margin: 0;
    font-family: var(--font-sans);
    font-size: var(--text-sm);
    line-height: 1;
    font-weight: var(--fw-semibold);
    text-transform: none;
    letter-spacing: var(--tracking-normal);
    color: var(--text-primary);
    white-space: normal;
    cursor: pointer;
    user-select: none;
    -webkit-tap-highlight-color: transparent;
  }
  .side-nav--sticky .side-nav__title::after {
    content: '';
    flex-shrink: 0;
    width: var(--space-12);
    height: var(--space-12);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: center;
    background-size: contain;
    transition: transform 0.2s ease;
    opacity: 0.65;
    margin-left: 8px;
  }
  .side-nav--sticky .side-nav__group[data-open="true"] .side-nav__title::after {
    transform: rotate(180deg);
    opacity: 1;
  }

  .side-nav--sticky .side-nav__group > .side-nav__item {
    display: none;
    padding: var(--space-12);
    margin: 0;
    font-size: var(--text-sm);
    white-space: normal;
    flex-shrink: 1;
    border-radius: 8px;
  }
  .side-nav--sticky .side-nav__group[data-open="true"] > .side-nav__item {
    display: flex;
  }
  .side-nav--sticky .side-nav__group > .side-nav__item:last-of-type {
    margin-bottom: 8px;
  }

  .side-nav--sticky .side-nav__icon {
    display: inline-flex;
    width: 16px;
    height: 16px;
  }

  .ds-doc-content { width: 100%; }
}

@media (max-width: 600px) {
  .side-nav--sticky { top: 72px; padding: 8px 12px; }
}

/* §3.75 FLOW DIAGRAMS — RETIRED (June 2026). The .flow-* family
   (.flow / .flow-node / .flow-lane / .flow-connector / .flow-edges /
   .flow-annotation / .flow-cascade / …) was replaced at parity by the
   Diagram Engine — diagram-engine.js (window.LifiDiagram), spec:
   design/diagram-engine.md, catalog: design-system/index.html#diagram-engine.
   Atomic retirement per CLAUDE.md — no aliases. */

/* ── 3.76 DIAGRAM ENGINE host — .diagram-host ─────────────────────────
   diagram-engine.js (window.LifiDiagram) renders a pure-SVG scene into
   this host; the SVG's viewBox owns the geometry, the host owns the
   responsive width. All theming lives INSIDE the SVG as var(--token)
   inline styles — no CSS rules target the scene itself, so the export
   path (computed-style resolution) sees exactly what the engine
   declared. Canonical spec: design/diagram-engine.md. */
.diagram-host svg {
  display: block;
  width: 100%;
  height: auto;
}

/* ══════════════════════════════════════════════════════════════════════
   3.8 LOGO CLOUD — .logo-cloud
   ───────────────────────────────────────────────────────────────────
   A "trusted by / powered by / integrated with" section — a compact
   band of third-party brand logos with an optional heading, subtitle,
   and call-to-action. Uses the assets in `/avatars/chains/`,
   `/logos/partners/`, and `/avatars/bridges/` so the cloud rerenders
   in full brand colour without any per-logo tinting.

   Composition model
   ─────────────────
     <section class="logo-cloud">
       <div class="logo-cloud-eyebrow">Optional eyebrow</div>
       <h3  class="logo-cloud-title">Heading</h3>
       <p   class="logo-cloud-sub">Optional subtitle</p>
       <div class="logo-cloud-grid logo-cloud-grid--5">
         <div class="logo-cloud-item">
           <img class="avatar avatar--square" src="logos/…" alt="…">
         </div>
         …
       </div>
       <a class="logo-cloud-cta" href="#">View all 140+ sources →</a>
     </section>

   Variants (modifiers on `.logo-cloud`)
   ────────────────────────────────────────
   - `--left`      left-aligned heading + row (default is centred)
   - `--split`     two-column: heading / copy left, logo grid right
   - `--boxed`     logos inside 1-px bordered tiles with rounded corners
   - `--on-brand`  accent-tinted background band for a dedicated section
   - `--stats`     includes a `.logo-cloud-stat` row under the grid
   - `--marquee`   horizontally-scrolling auto-marquee row (paused on hover)

   Grid column modifiers (`.logo-cloud-grid--{3|4|5|6}`)
   ────────────────────────────────────────────────────────
   - 3 / 4 / 5 / 6  — desktop column count. Collapses gracefully to 3
     (≤720px) and 2 (≤480px) via the media queries at the bottom.

   Logo rendering
   ──────────────
   Default: full-colour brand logos (via `.avatar`). For the neutral
   Tailwind aesthetic, add `.logo-cloud--mono` on the section — it
   drops a `grayscale + opacity` filter on every logo inside.

   Palette discipline — no hex literals in any of the rules below. The
   headings, eyebrow, border, and accent background all resolve to
   tokens (`--text-*`, `--surface-*`, `--accent-*`, `--border-*`).
   ══════════════════════════════════════════════════════════════════════ */

/* Outer band — generous vertical padding so the cloud reads as its
   own page section, not a crowded strip. 96/48 gives the content a
   clear cushion on every edge while still leaving room for the logos
   to breathe horizontally inside a 5-column grid. */
.logo-cloud {
  width: 100%;
  padding: var(--space-96) var(--space-48);
  text-align: center;
  color: var(--text-primary);
  box-sizing: border-box;
}
.logo-cloud--left  { text-align: left; }

/* Header stack — headline scale matches LI.FI section-level typography
   (`text-h3` / 700 / tracking-tight). The logos are still the hero,
   but the heading carries confidence — not the Tailwind-style quiet
   intro. Split variant upgrades to `text-h2` since the title there
   carries a full section-headline role, not an intro. */
.logo-cloud-eyebrow {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--accent-secondary);
  margin-bottom: var(--space-16);
}
.logo-cloud-title {
  font-family: var(--font-sans);
  font-size: var(--text-h3);           /* 24px — LI.FI section-intro scale */
  line-height: var(--lh-h3);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-tight);
  color: var(--text-primary);
  margin: 0;
  max-width: 720px;
}
.logo-cloud:not(.logo-cloud--left) .logo-cloud-title,
.logo-cloud:not(.logo-cloud--left) .logo-cloud-sub {
  margin-left: auto;
  margin-right: auto;
}
.logo-cloud-sub {
  font-family: var(--font-sans);
  font-size: var(--text-body-xl);      /* 20px — lede-paragraph scale, gives the subtitle real hierarchy below the 24px title */
  line-height: var(--lh-body-xl);
  font-weight: var(--fw-regular);
  color: var(--text-muted);
  margin: var(--space-16) 0 0;
  max-width: 640px;
}
/* 56px gap between heading stack and logo row/grid — LI.FI's bigger
   section-level rhythm. Tight enough to read as related; generous
   enough that the logos land as a distinct block. */
.logo-cloud-title + .logo-cloud-grid,
.logo-cloud-title + .logo-cloud-row,
.logo-cloud-sub + .logo-cloud-grid,
.logo-cloud-sub + .logo-cloud-row,
.logo-cloud-eyebrow + .logo-cloud-grid,
.logo-cloud-eyebrow + .logo-cloud-row {
  margin-top: var(--space-56);
}

/* Grid container — 5 cols default. 56/40 gap (row > col) gives each
   logo a clean personal zone without the grid feeling sparse. */
.logo-cloud-grid {
  display: grid;
  grid-template-columns: repeat(var(--logo-cloud-cols, 5), 1fr);
  align-items: center;
  justify-items: center;
  gap: var(--space-56) var(--space-40);
}
.logo-cloud-grid--3 { --logo-cloud-cols: 3; }
.logo-cloud-grid--4 { --logo-cloud-cols: 4; }
.logo-cloud-grid--5 { --logo-cloud-cols: 5; }
.logo-cloud-grid--6 { --logo-cloud-cols: 6; }

/* Single-row variant — flex row with wrap, wider horizontal gap than
   the grid since single-row layouts don't have to align to columns,
   so logos can breathe more between siblings. */
.logo-cloud-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-40) var(--space-64);
}
.logo-cloud--left .logo-cloud-row { justify-content: flex-start; }

/* Individual logo cell — houses an <img class="avatar …">. The hover
   lift is a subtle -2px translateY that signals "clickable" without
   feeling like a full card interaction. The cell itself has no
   background or border (the boxed variant adds those separately) —
   it's just the hit-target and the hover affordance carrier. */
.logo-cloud-item {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 56px;
  transition: transform 0.2s ease, opacity 0.2s ease;
}
.logo-cloud-item:hover {
  transform: translateY(-2px);
}
.logo-cloud-item .avatar {
  display: block;
  /* The avatar's own border-radius shouldn't clip any part of the
     logo itself — each brand SVG is pre-plated to its own viewBox.
     But defensively ensure nothing outside the 48×48 tile gets trimmed
     by ancestor `overflow:hidden`. */
  overflow: visible;
}
/* Wordmark logos — brands like MetaMask, Chainlink, Aave ship as
   wide-aspect SVGs with typography built in. Using `.avatar` would
   crop them into a 48×48 square, which is wrong. Render a raw
   `<img>` (no `.avatar` class) and this rule constrains by HEIGHT
   and lets the width auto-compute. `object-fit: contain` protects
   against any source SVG that uses `preserveAspectRatio="none"`. */
.logo-cloud-item > img:not(.avatar) {
  display: block;
  max-height: var(--logo-cloud-wordmark-h, 40px);
  width: auto;
  max-width: 100%;
  object-fit: contain;
}
.logo-cloud-item > img:not(.avatar).is-sm { --logo-cloud-wordmark-h: 28px; }
.logo-cloud-item > img:not(.avatar).is-lg { --logo-cloud-wordmark-h: 48px; }

/* Labelled item — logo + brand name underneath */
.logo-cloud-item--labelled {
  flex-direction: column;
  gap: var(--space-8);
}
.logo-cloud-item-label {
  font-family: var(--font-sans);
  font-size: var(--text-caption);
  line-height: var(--lh-caption);
  font-weight: var(--fw-medium);
  color: var(--text-muted);
  text-align: center;
  white-space: nowrap;
}

/* Call-to-action below the grid — 40px gap matches heading-to-grid rhythm */
.logo-cloud-cta {
  display: inline-flex;
  align-items: center;
  gap: var(--space-8);
  margin-top: var(--space-40);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: var(--fw-semibold);
  color: var(--accent-primary);
  text-decoration: none;
  transition: color 0.18s ease, transform 0.18s ease;
}
.logo-cloud-cta svg { width: var(--space-12); height: var(--space-12); transition: transform 0.18s ease; }
.logo-cloud-cta:hover { color: color-mix(in oklch, var(--accent-primary) 80%, var(--text-primary)); }
.logo-cloud-cta:hover svg { transform: translateX(3px); }

/* Stat row — headline number + label beneath the grid. 48px gap from
   logos because the stat value is the section climax and deserves to
   land without collision. */
.logo-cloud-stat {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: var(--space-12);
  margin-top: var(--space-48);
  flex-wrap: wrap;
  color: var(--text-muted);
  font-size: var(--text-body);
  max-width: 680px;
  margin-left: auto;
  margin-right: auto;
}
.logo-cloud--left .logo-cloud-stat { justify-content: flex-start; }
.logo-cloud-stat-value {
  font-family: var(--font-sans);
  font-size: var(--text-h2);
  line-height: 1;
  font-weight: var(--fw-extrabold);
  letter-spacing: var(--tracking-tighter);
  color: var(--accent-primary);
}

/* Split variant — two columns: heading/copy on left, logo grid on right.
   The title upgrades to h2 because it's carrying a full section-headline
   role here, not acting as an intro. Heavier weight + tighter tracking
   pushes the heading into "editorial" territory appropriate for a
   content section the cloud IS, not a strip the cloud sits in. */
.logo-cloud--split {
  text-align: left;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.3fr);
  gap: var(--space-64);
  align-items: center;
}
.logo-cloud--split .logo-cloud-title {
  font-size: var(--text-h2);
  line-height: var(--lh-h2);
  font-weight: var(--fw-extrabold);
  letter-spacing: var(--tracking-tighter);
  max-width: 440px;
  margin-left: 0;
  margin-right: 0;
}
.logo-cloud--split .logo-cloud-sub {
  margin-left: 0; margin-right: 0;
  max-width: 440px;
}
/* Split variant's grid doesn't need the 40px "lift" from the heading
   block since the heading is adjacent horizontally, not above */
.logo-cloud--split .logo-cloud-title + .logo-cloud-grid,
.logo-cloud--split .logo-cloud-sub + .logo-cloud-grid {
  margin-top: 0;
}
.logo-cloud-split-copy {
  display: flex;
  flex-direction: column;
  gap: var(--space-12);
}

/* Boxed variant — each logo sits in a bordered tile. Tile height set
   generously (128px) so the inner logo + optional label breathe. Tile
   gap tightens from the default grid so boxed layouts feel like a
   directory grid, not a scattered cloud. */
.logo-cloud--boxed .logo-cloud-grid {
  gap: var(--space-16);
}
.logo-cloud--boxed .logo-cloud-item {
  width: 100%;
  min-height: 128px;
  padding: var(--space-24);
  border: 1px solid var(--border-subtle);
  border-radius: var(--r-lg);
  background: var(--surface-raised);
  transition: border-color 0.2s ease, background 0.2s ease, transform 0.2s ease;
}
.logo-cloud--boxed .logo-cloud-item:hover {
  border-color: color-mix(in oklch, var(--accent-primary) 35%, var(--border-subtle));
  background: color-mix(in oklch, var(--accent-primary) 4%, var(--surface-raised));
  transform: translateY(-2px);
}

/* On-brand variant — accent-tinted background band */
.logo-cloud--on-brand {
  background: linear-gradient(
    135deg,
    color-mix(in oklch, var(--accent-primary) 10%, transparent) 0%,
    color-mix(in oklch, var(--accent-secondary) 10%, transparent) 100%
  );
  border: 1px solid color-mix(in oklch, var(--accent-primary) 18%, transparent);
  border-radius: var(--r-xl);
}
.logo-cloud--on-brand .logo-cloud-title { color: var(--text-primary); }
.logo-cloud--on-brand .logo-cloud-eyebrow { color: var(--accent-primary); }

/* Mono variant — desaturates logos for the neutral "trusted by" aesthetic.
   Selector reaches any `.avatar` inside a mono cloud, whether it's
   wrapped in `.logo-cloud-item` or sits directly inside `.logo-cloud-row`. */
.logo-cloud--mono .avatar {
  filter: grayscale(100%) opacity(0.72);
  transition: filter 0.2s ease;
}
.logo-cloud--mono .avatar:hover,
.logo-cloud--mono .logo-cloud-item:hover .avatar {
  filter: grayscale(0%) opacity(1);
}

/* Marquee variant — infinite horizontal scroll, pauses on hover */
.logo-cloud--marquee {
  padding-left: 0;
  padding-right: 0;
  overflow: hidden;
  position: relative;
}
.logo-cloud--marquee::before,
.logo-cloud--marquee::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  width: 96px;
  z-index: 2;
  pointer-events: none;
}
.logo-cloud--marquee::before {
  left: 0;
  background: linear-gradient(to right, var(--surface-page), transparent);
}
.logo-cloud--marquee::after {
  right: 0;
  background: linear-gradient(to left, var(--surface-page), transparent);
}
.logo-cloud-marquee {
  display: flex;
  width: max-content;
  gap: var(--space-40);
  align-items: center;
  animation: logo-cloud-marquee 38s linear infinite;
}
.logo-cloud--marquee:hover .logo-cloud-marquee { animation-play-state: paused; }
@keyframes logo-cloud-marquee {
  from { transform: translateX(0); }
  /* Translates one half of the doubled track, so the second half
     lines up seamlessly with the first as the animation loops. */
  to   { transform: translateX(-50%); }
}

/* Responsive — collapse columns on narrow viewports */
@media (max-width: 900px) {
  .logo-cloud-grid--5,
  .logo-cloud-grid--6 { --logo-cloud-cols: 4; }
  .logo-cloud--split {
    grid-template-columns: 1fr;
    gap: var(--space-32);
  }
}
@media (max-width: 600px) {
  .logo-cloud-grid--4,
  .logo-cloud-grid--5,
  .logo-cloud-grid--6 { --logo-cloud-cols: 3; }
}
@media (max-width: 420px) {
  .logo-cloud-grid--3,
  .logo-cloud-grid--4,
  .logo-cloud-grid--5,
  .logo-cloud-grid--6 { --logo-cloud-cols: 2; }
}

/* ══════════════════════════════════════════
   4. MOBILE MENU
   ══════════════════════════════════════════ */
button.hamburger {
  display: none;
  z-index: 1001;
  margin-left: auto;
  width: var(--space-40); height: var(--space-40); flex-shrink: 0;
}
.mobile-menu {
  display: none; position: fixed; inset: 0;
  background: var(--nav-mobile-bg); backdrop-filter: blur(50px);
  z-index: 999; flex-direction: column; align-items: flex-start;
  justify-content: flex-start; padding: var(--space-96) var(--space-24) var(--space-40);
  gap: 0; overflow-y: auto;
}
.mobile-menu.open { display: flex; }
.mobile-menu-section { width: 100%; border-bottom: 1px solid var(--border-subtle); }
.mobile-menu-trigger {
  display: flex; align-items: center; justify-content: space-between;
  width: 100%; padding: var(--space-20) 0; background: none; border: none;
  color: var(--text-secondary); font-size: var(--text-body); font-weight: var(--fw-semibold);
  font-family: var(--font-sans); cursor: pointer; transition: color 0.2s;
}
.mobile-menu-trigger:hover,
.mobile-menu-section.open .mobile-menu-trigger { color: var(--text-primary); }
.mobile-menu-trigger .chevron {
  width: var(--space-12); height: var(--space-12); opacity: 0.4;
  transition: transform 0.3s, opacity 0.3s;
}
.mobile-menu-section.open .mobile-menu-trigger .chevron {
  transform: rotate(180deg); opacity: 0.6;
}
.mobile-menu-sub {
  max-height: 0; overflow: hidden;
  transition: max-height 0.4s cubic-bezier(0.4,0,0.2,1);
}
.mobile-menu-section.open .mobile-menu-sub { max-height: 500px; padding-bottom: 16px; }
.mobile-menu-sub a {
  display: flex; align-items: center; gap: var(--cluster-md); padding: 12px 16px;
  color: var(--text-muted); text-decoration: none;
  font-size: var(--text-sm); font-weight: var(--fw-medium); border-radius: 12px;
  transition: background 0.25s, color 0.25s, transform 0.25s;
}
.mobile-menu-sub a:hover {
  background: var(--overlay-hover); color: var(--text-primary); transform: translateX(4px);
}
.mobile-menu-sub .dd-icon {
  width: var(--space-32); height: var(--space-32); min-width: var(--space-32); border-radius: var(--space-12);
  background: var(--overlay-hover);
  display: flex; align-items: center; justify-content: center;
}
.mobile-menu-sub .dd-icon svg {
  width: var(--space-16); height: var(--space-16); stroke: var(--text-muted);
  fill: none; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round;
}
.mobile-menu-link {
  display: block; width: 100%; padding: var(--space-20) 0;
  color: var(--text-secondary); text-decoration: none;
  font-size: var(--text-body); font-weight: var(--fw-semibold);
  border-bottom: 1px solid var(--border-subtle); transition: color 0.2s;
}
.mobile-menu-link:hover { color: var(--text-primary); }


/* ══════════════════════════════════════════
   5. BUTTONS — Systematic component library
   ══════════════════════════════════════════
   Types:    primary, secondary, neutral, tertiary, destructive, success, link
   Colors:   accent1 (default), accent2 (.btn-accent2), accent3 (.btn-accent3)
   Styles:   filled (default), outline (.btn-outline)
   Sizes:    sm (.btn-sm), md (default), lg (.btn-lg), xl (.btn-xl)
   Icon:     .btn-with-icon (shows trailing →)
   Layout:   .btn-block (full width) · .btn-justify (label ↔ icon to edges)
   ══════════════════════════════════════════ */

/* ── Shared base ───────────────────────────────────────────────────────── */
/* .btn-neutral is in the base group so a bare <button class="btn-neutral"> renders
   with the canonical button shape (padding / radius / font-size / line-height /
   transition) — same as any other type. Composed forms (.btn-neutral.btn-sm,
   .btn-neutral.btn-icon, etc.) compose like any other button. The neutral
   block below overrides only background / color / border / backdrop-filter. */
.btn-primary, .btn-secondary, .btn-tertiary, .btn-link,
.btn-destructive, .btn-success, .btn-neutral {
  /* Filled primary paints the brand accent SEED (--accent-primary), NOT the
     +10L lightened --accent-primary-mid. June 2026: the mid (light-mode L0.62,
     rgb 96,126,229) is light enough that the auto-contrast formula picked DARK
     text — which read muddy and only passed AA as the higher-contrast option
     (white-on-mid was 3.74:1, fails AA at the 14px label). On the true seed
     (#405CCF, L0.52) the formula flips to white automatically (0.52 < 0.62) at
     an AA-passing 5.75:1, and the CTA is painted in the real brand colour
     instead of a tint. This also makes plain primary consistent with the
     .btn-accent2/.btn-accent3 modifiers, which already paint their seed. */
  --btn-accent: var(--accent-primary);
  --btn-accent-strong: var(--accent-primary);
  --btn-accent-hover: color-mix(in oklch, var(--accent-primary) 90%, #fff);
  --btn-accent-glow: color-mix(in oklch, var(--accent-primary) 16%, transparent);
  --btn-accent-bg: color-mix(in oklch, var(--accent-primary) 10%, transparent);
  --btn-accent-text: color-mix(in oklch, var(--accent-primary) 60%, #fff);
  /* Contrast-aware text — see .btn-accent2 below for formula rationale.
     Source must track --btn-accent (now the seed) so the label contrasts
     against what's actually painted. */
  --btn-on-accent: oklch(from var(--accent-primary) clamp(0.08, (0.62 - l) * 100, 0.96) 0 0);
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-8);
  padding: var(--space-12) var(--space-24); border-radius: calc(var(--button-radius) - var(--space-4));
  font-size: var(--text-sm); font-weight: var(--fw-semibold);
  line-height: 1;                         /* decouple from body line-height cascade */
  letter-spacing: var(--tracking-normal);
  /* CTAs never wrap — AND an inline-flex box with a wrappable label resolves its
     content width to the LONGEST WORD, not the full phrase, so a 2+-word label
     (e.g. "Connect wallet") in a content-width context collapses + wraps to two
     lines regardless of available parent width. Pinning nowrap fixes both. An
     auto-width button (the default) grows to the full label; a constrained-width
     button (.btn-block / width:100%) narrower than its label would clip via the
     base `overflow: hidden` — but every constrained context here (mobile full-
     width, the 320px rail block) exceeds the longest label, so no real button
     clips. June 2026 — see bug-history → "Button label wraps". */
  white-space: nowrap;
  border: 1px solid transparent;
  cursor: pointer; font-family: var(--font-sans);
  text-decoration: none; position: relative; overflow: hidden;
  /* Hover transitions — May 2026: simplified the curve and shortened the
     duration to match .btn-icon.btn-borderless's feel (the reference
     "subtle hover" pattern). Was 0.3s cubic-bezier(0.4,0,0.2,1) on bg /
     box-shadow — Material's Standard Easing — which has a flat first ~30%
     before accelerating. At 0.3s that flat segment is ~90 ms of "nothing
     happens" before a fast rush, perceived as a delay-then-pop rather than
     a smooth fade. Reverted to `ease` (cubic-bezier(0.25,0.1,0.25,1))
     which starts moving immediately and decelerates throughout — same
     curve the borderless icon button uses. Transform keeps its bouncy
     curve — the overshoot IS the "lift" cue and only fires when the
     button actually translates, which now requires the opt-in `.btn-lift`
     modifier (buttons are static by default — May 2026 inversion). The
     transition stays declared on the base so a composed `.btn-lift`
     animates smoothly. `rotate` added so per-component rotation animations
     (e.g. quote-card chevron toggle) inherit smoothly without per-component
     transition overrides blowing away the bg/color/border transitions. */
  transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease,
              box-shadow 0.2s ease, transform 0.25s cubic-bezier(0.34,1.56,0.64,1),
              rotate 0.15s ease;
}

/* ── Disabled — gate ("can't do this yet") OR busy ("Confirming…") ───────
   Promoted June 2026 from a widget-scoped `.action-bar .btn-primary` band-aid
   (was in swap.css, retired in the same commit — no aliases, per CLAUDE.md →
   Primitive retirement is atomic) to the canonical family so every disabled
   button reads as disabled site-wide. Both the native `disabled` attribute
   and `[aria-disabled="true"]` are first-class — use aria-disabled when the
   control must stay in the tab order / keep its role. `pointer-events: none`
   is the only thing that makes aria-disabled actually non-interactive (the
   attribute alone does NOT block clicks; the native `disabled` does) and it
   matches the button-shaped siblings (.seg-item, .action-card, .tc-arrow).
   It suppresses direct hover — a "why disabled" tooltip goes on a WRAPPER,
   not the button. Dims uniformly: a loading button's spinner stays legible
   at 0.4. `.tc-arrow:disabled` (page.css) overrides opacity to 0.25 — same
   (0,1,1) specificity, later source order, so the carousel arrows keep their
   fainter treatment. */
.btn-primary:disabled,      .btn-primary[aria-disabled="true"],
.btn-secondary:disabled,    .btn-secondary[aria-disabled="true"],
.btn-tertiary:disabled,     .btn-tertiary[aria-disabled="true"],
.btn-link:disabled,         .btn-link[aria-disabled="true"],
.btn-destructive:disabled,  .btn-destructive[aria-disabled="true"],
.btn-success:disabled,      .btn-success[aria-disabled="true"],
.btn-neutral:disabled,      .btn-neutral[aria-disabled="true"] {
  opacity: 0.4;
  box-shadow: none;
  cursor: not-allowed;
  pointer-events: none;
}

/* Bare-icon background reset — completes the .btn-icon UA-chrome reset (see the
   .btn-icon base rule below, which strips appearance + the 2px outset border but
   deliberately does NOT set background). The UA `button { background: ButtonFace }`
   would otherwise leak a light-grey fill on a truly bare .btn-icon — invisible in
   light mode, an obvious grey box in dark. It MUST live here, BEFORE the type
   blocks (.btn-primary etc.), not in the base rule at line ~4040: a base-rule
   `background: transparent` is declared AFTER the filled variants and would win
   on source order, clobbering their fills. Placed here it loses to every later
   .btn-* background (equal 0,1,0 specificity, declared later) while still beating
   the UA `button` type selector (0,0,1) — so ONLY a bare .btn-icon falls through
   to transparent; .btn-borderless re-asserts transparent later anyway. */
.btn-icon { background: transparent; }

/* ── Color modifiers ───────────────────────────────────────────────────── */
.btn-accent2 {
  --btn-accent: var(--accent-secondary);
  --btn-accent-strong: var(--accent-secondary);
  --btn-accent-hover: color-mix(in oklch, var(--accent-secondary) 92%, #fff);
  --btn-accent-glow: color-mix(in oklch, var(--accent-secondary) 14%, transparent);
  --btn-accent-bg: color-mix(in oklch, var(--accent-secondary) 10%, transparent);
  --btn-accent-text: var(--accent-secondary);
  /* Contrast-aware text: step around a perceptual lightness threshold so
     saturated mid-lightness backgrounds (e.g. a vivid purple at l=0.5) still
     get near-white text instead of mid-gray. Multiplying by 100 turns the
     linear difference into a steep step, and clamp() pins the result to
     near-black (0.08) or near-white (0.96). Threshold 0.62 is a reasonable
     cutoff: anything darker needs light text, anything lighter needs dark text.
     Note: OKLCH `l` in relative syntax uses 0–1 scale, not 0–100. */
  --btn-on-accent: oklch(from var(--accent-secondary) clamp(0.08, (0.62 - l) * 100, 0.96) 0 0);
}
.btn-accent3 {
  --btn-accent: var(--accent-tertiary);
  --btn-accent-strong: var(--accent-tertiary);
  --btn-accent-hover: color-mix(in oklch, var(--accent-tertiary) 92%, #fff);
  --btn-accent-glow: color-mix(in oklch, var(--accent-tertiary) 14%, transparent);
  --btn-accent-bg: color-mix(in oklch, var(--accent-tertiary) 10%, transparent);
  --btn-accent-text: var(--accent-tertiary);
  --btn-on-accent: oklch(from var(--accent-tertiary) clamp(0.08, (0.62 - l) * 100, 0.96) 0 0);
}

/* ── Primary — filled accent bg, white text ────────────────────────────── */
.btn-primary {
  background: var(--btn-accent);
  color: var(--btn-on-accent);
  border-color: transparent;
}
.btn-primary:hover {
  background: var(--btn-accent-hover);
}

/* ── Primary outline — transparent bg, accent border+text ──────────────── */
.btn-primary.btn-outline {
  background: transparent;
  color: var(--btn-accent-text);
  border-color: var(--btn-accent-text);
  border-width: 1px; border-style: solid;
}
.btn-primary.btn-outline:hover {
  background: var(--btn-accent-bg);
  border-color: var(--btn-accent);
  color: var(--btn-accent);
}

/* ── Secondary — muted accent bg, accent text ──────────────────────────── */
.btn-secondary {
  background: var(--btn-accent-bg);
  color: var(--btn-accent-text);
  border-color: transparent;
}
.btn-secondary:hover {
  background: color-mix(in oklch, var(--btn-accent-strong) 22%, transparent);
  color: var(--btn-accent-strong);
}

/* ── Tertiary — transparent bg, accent text, very subtle ───────────────── */
.btn-tertiary {
  background: transparent;
  color: var(--btn-accent-text);
  border-color: transparent;
}
.btn-tertiary:hover {
  background: var(--btn-accent-bg);
  color: var(--btn-accent);
}

/* ── Link — borderless, alpha-transparent hover bg ─────────────────────── */
.btn-link {
  background: transparent;
  color: var(--btn-accent-text);
  border-color: transparent;
  padding: var(--space-12) var(--space-16);
}
.btn-link:hover {
  background: var(--btn-accent-bg);
  color: var(--btn-accent);
}

/* ── Destructive / Success — semantic-state buttons ─────────────────────
   These variants reuse the Primary button's exact visual recipe —
   filled: background / color / hover lift + glow shadow
   outline: transparent / colored text + border / hover tint + glow
   The only delta is the color source: Primary pulls from the palette
   (--accent-*), these pull from semantic state tokens (--danger,
   --success) which are intentionally palette-independent so the red
   stays red and the green stays green across every theme. ──────────── */
.btn-destructive {
  --btn-accent:       var(--danger);
  --btn-accent-hover: color-mix(in oklch, var(--danger) 92%, #fff);
  --btn-accent-glow:  color-mix(in oklch, var(--danger) 16%, transparent);
  --btn-accent-bg:    color-mix(in oklch, var(--danger) 10%, transparent);
  --btn-accent-text:  var(--danger);
  --btn-on-accent:    #ffffff;

  background: var(--btn-accent);
  color: var(--btn-on-accent);
  border-color: transparent;
}
.btn-destructive:hover {
  background: var(--btn-accent-hover);
}
.btn-destructive.btn-outline {
  background: transparent;
  color: var(--btn-accent-text);
  border-color: var(--btn-accent-text);
  border-width: 1px; border-style: solid;
}
.btn-destructive.btn-outline:hover {
  background: var(--btn-accent-bg);
  border-color: var(--btn-accent);
  color: var(--btn-accent);
}

.btn-success {
  --btn-accent:       var(--success);
  --btn-accent-hover: color-mix(in oklch, var(--success) 92%, #fff);
  --btn-accent-glow:  color-mix(in oklch, var(--success) 16%, transparent);
  --btn-accent-bg:    color-mix(in oklch, var(--success) 10%, transparent);
  --btn-accent-text:  var(--success);
  --btn-on-accent:    #ffffff;

  background: var(--btn-accent);
  color: var(--btn-on-accent);
  border-color: transparent;
}
.btn-success:hover {
  background: var(--btn-accent-hover);
}
.btn-success.btn-outline {
  background: transparent;
  color: var(--btn-accent-text);
  border-color: var(--btn-accent-text);
  border-width: 1px; border-style: solid;
}
.btn-success.btn-outline:hover {
  background: var(--btn-accent-bg);
  border-color: var(--btn-accent);
  color: var(--btn-accent);
}


/* ── Inline link — for text-level links inside paragraphs, hints, etc. ── */
.ds-doc-link {
  color: var(--accent-primary-mid);
  text-decoration: underline;
  text-decoration-color: color-mix(in oklch, var(--accent-primary-mid) 40%, transparent);
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
  font-weight: var(--fw-medium);
  transition: color 0.2s, text-decoration-color 0.2s;
}
.ds-doc-link:hover {
  color: var(--accent-primary);
  text-decoration-color: var(--accent-primary);
}
/* Accent variants */
.ds-doc-link--accent2 {
  color: var(--accent-secondary);
  text-decoration-color: color-mix(in oklch, var(--accent-secondary) 40%, transparent);
}
.ds-doc-link--accent2:hover {
  color: var(--accent-secondary);
  text-decoration-color: var(--accent-secondary);
}
.ds-doc-link--accent3 {
  color: var(--accent-tertiary);
  text-decoration-color: color-mix(in oklch, var(--accent-tertiary) 40%, transparent);
}
.ds-doc-link--accent3:hover {
  color: var(--accent-tertiary);
  text-decoration-color: var(--accent-tertiary);
}
/* Muted — for less prominent contexts (footers, fine print) */
.ds-doc-link--muted {
  color: var(--text-muted);
  text-decoration-color: color-mix(in oklch, var(--text-muted) 40%, transparent);
  font-weight: var(--fw-regular);
}
.ds-doc-link--muted:hover {
  color: var(--text-secondary);
  text-decoration-color: var(--text-secondary);
}


/* ── Size modifiers ────────────────────────────────────────────────────── */
/* Three ladders, all on --space-* / --text-* tokens — every value the
   system already publishes, no off-grid pixels.

   Heights      32 → 40 → 48 → 56  (--space-32 / 40 / 48 / 56, +8 each)
   Labels       12 → 14 → 16 → 18  (--text-caption / --text-sm / --text-body / --text-body-lg, +2 each)
   Radii         8 → 12 → 16 → 20  (calc off --button-radius=16 → −8 / −4 / 0 / +4; lg is the anchor, +4 each)
   Pad X        20 → 24 → 32 → 40  (--space-20 / 24 / 32 / 40)
   Pad Y         8 → 12 → 12 → 16  (--space-8 / 12 / 12 / 16)

   The calc()-based line-heights widen the line-box so a progressive label
   still lands cleanly on the height ladder. Math is padY × 2 + line-box
   + 2px border = height:
     sm: 8 + 8 + 14 + 2 = 32    (12px label in 14px line-box)
     md: 12 + 12 + 14 + 2 = 40  (14px label in 14px line-box)
     lg: 12 + 12 + 22 + 2 = 48  (16px label in 22px line-box)
     xl: 16 + 16 + 22 + 2 = 56  (18px label in 22px line-box) */
.btn-sm { font-size: var(--text-caption); line-height: calc(14 / 12); padding: var(--space-8) var(--space-20); border-radius: calc(var(--button-radius) - var(--space-8)); }
.btn-lg { font-size: var(--text-body);    line-height: calc(22 / 16); padding: var(--space-12) var(--space-32); border-radius: var(--button-radius); }
.btn-xl { font-size: var(--text-body-lg); line-height: calc(22 / 18); padding: var(--space-16) var(--space-40); border-radius: calc(var(--button-radius) + var(--space-4)); }

/* ── Button icon classes — June 2026 rename (atomic, no aliases) ──────────
   .btn-icon (label button + icon)       → .btn-with-icon
   .btn-icon-only (icon-hugging button)  → .btn-icon   (reused the freed name)
   Splits the label-button-with-icon (asymmetric label padding, optional
   leading/trailing icons by DOM position) from the icon-hugging button
   (square/circle, tight symmetric padding, requires aria-label). Ordered
   sweep across product + marketing + catalog + specs. The old names appear
   only in this breadcrumb + bug-history.md. */

/* ── Icon modifier — hidden by default, shown with .btn-with-icon ──────────── */
[class*="btn-"] > svg { display: none; }
.btn-with-icon > svg { display: inline-block; }
.btn-with-icon::after {
  /* Trailing arrow = the library Lucide arrow-right (icons/arrow-right.svg)
     rendered as a currentColor mask, so the zero-markup default matches an
     explicit <svg arrow-right> child exactly. Replaces the former Unicode →
     (U+2192) Figtree glyph, which rendered thinner/smaller and didn't match
     the library icon. Sized at --space-20 (the icon-button md tier), the same
     20px the explicit-SVG consumers use. */
  content: '';
  width: var(--space-20); height: var(--space-20); flex-shrink: 0;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14'/%3E%3Cpath d='m12 5 7 7-7 7'/%3E%3C/svg%3E") center / contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14'/%3E%3Cpath d='m12 5 7 7-7 7'/%3E%3C/svg%3E") center / contain no-repeat;
}
.btn-with-icon > svg + .btn-with-icon::after,
.btn-with-icon:has(svg)::after { display: none; }

/* ── Icon-only button — square, centers a single icon ─────────────────── */
/* Uses the same type/accent/style/size system as text buttons.
   Accepts <svg>, <img>, or a custom icon element as its only child.
   Size tokens match text button heights exactly so they sit inline.
   Default icon: 20px. Use --btn-icon-size to override.                   */
.btn-icon {
  --btn-icon-size: var(--space-20);
  padding: 0;
  /* UA-chrome reset — .btn-icon has its OWN base (doesn't compose the shared
     text-button rule), so it carries NO <button> UA-chrome reset. Strip the
     native widget rendering + reserve a 1px transparent border slot so a bare
     or bespoke-bg .btn-icon (the case that bit the former .host-nav__more)
     doesn't leak the UA 2px outset border — the dark ring. The slot keeps the
     40px size ladder (per the "reserved border slots" rule — NOT `border: 0`,
     which drops 2px). Background is deliberately NOT reset here: every typed
     variant (.btn-primary / .btn-neutral / …) sets its own fill, and a base
     `background: transparent` would clobber the FILLED variants declared ABOVE
     this rule (line ~3800–3918) via source order — and no bare .btn-icon
     consumer exists that would need it (the UA ButtonFace bg only shows on a
     truly bare .btn-icon, of which there are none in the codebase). Third
     instance of the UA-chrome-leak family — cf. .menu-item / .side-nav__item. */
  appearance: none; -webkit-appearance: none;
  border: 1px solid transparent;
  width: var(--space-40); height: var(--space-40);   /* matches md text-button height */
  border-radius: var(--button-radius);               /* tracks the button-radius customization, like text buttons */
  flex-shrink: 0;
  cursor: pointer;                                    /* own base — doesn't compose the text-button shared rule */
  font-family: var(--font-sans);                      /* defensive — <button> UA default is Arial; render brand Figtree if ever given a text label / ::after glyph */
  display: inline-flex; align-items: center; justify-content: center;
}
.btn-icon > svg,
.btn-icon > img,
.btn-icon > [class*="icon"] {
  display: block;
  width: var(--btn-icon-size); height: var(--btn-icon-size);
  flex-shrink: 0;
  pointer-events: none;
}
/* Stroke-based icons (Lucide) already carry fill="none" inline.
   Only reset fill for SVGs that don't explicitly declare their own. */
.btn-icon > svg:not([fill]) { fill: none; }
.btn-icon > img { object-fit: contain; }

/* Remove trailing arrow that .btn-with-icon normally adds */
.btn-icon::after { display: none !important; }

/* Size overrides — every value resolves to a --space-* token (4px grid). */
.btn-icon.btn-xs {
  --btn-icon-size: var(--space-12);
  width: var(--space-24); height: var(--space-24);
  border-radius: var(--button-radius);
}
.btn-icon.btn-sm {
  --btn-icon-size: var(--space-16);
  width: var(--space-32); height: var(--space-32);
  border-radius: var(--button-radius);
}
.btn-icon.btn-lg {
  --btn-icon-size: var(--space-24);
  width: var(--space-56); height: var(--space-56);
  border-radius: var(--button-radius);
}

/* Circle variant */
.btn-icon.btn-circle { border-radius: var(--r-full); }

/* Borderless variant — ghost icon button with no background, border, or shadow.
   Shows a subtle tinted bg on hover. Pairs with any type for color inheritance.
   May 2026: default colour bumped from --text-muted to --text-primary so a
   bare borderless icon button reads at the same weight as the labels it sits
   next to (screen-header buttons, list-row affordances, navbar utilities).
   The previous muted default is preserved as the `.btn-muted` variant for
   cases that genuinely want deemphasis (a row affordance that yields focus
   to the row's primary content, a secondary toolbar action). */
.btn-icon.btn-borderless {
  background: transparent;
  border-color: transparent;
  box-shadow: none;
  color: var(--icon-primary);
  /* Icon-button hover contract — system-wide rule (May 2026):
     hover only tints the BACKGROUND; the icon color never shifts. The
     transition smooths the background fade so the affordance reads as
     a soft wash, not a snap. Bare borderless buttons don't match the
     base `.btn-*:hover` transition (which is scoped to type classes
     like .btn-primary/.btn-tertiary/.btn-neutral), so the transition is
     declared here on the borderless rule itself — otherwise the hover
     fill paints instantly with no animation. Curve: 0.2s ease, slightly
     snappier than the 0.3s cubic-bezier on typed buttons because icon
     buttons are smaller targets and a longer fade reads as laggy. */
  transition: background 0.2s ease;
}
/* Muted variant — opts back into the deemphasized colour for icon buttons
   that should defer to their context. Reads at `--icon-muted` and stays
   there on hover (icon-color-stays contract above) — only the background
   wash communicates the affordance. The muted-rest cue is the steady
   state; hover is the wash reveal, not a brightness lift. */
.btn-icon.btn-borderless.btn-muted {
  color: var(--icon-muted);
}
.btn-icon.btn-borderless:hover {
  background: color-mix(in oklch, var(--icon-primary) 8%, transparent);
  border-color: transparent;
  box-shadow: none;
  transform: none;
}
.btn-icon.btn-borderless:active {
  background: color-mix(in oklch, var(--icon-primary) 12%, transparent);
  transform: none;
}
/* No light-theme override — the base rule above mixes --text-primary
   (theme-aware: white in dark, black in light) so 8% / 12% lands as
   a visible black wash on white pages and a visible white wash on dark
   pages from the one rule. The previous override here mixed
   --surface-page instead (white in light), which painted white-on-white
   on light-mode hover and made both the fill AND the icon invisible.
   The accent-aware rules below override per-type, so .btn-tertiary etc.
   tint hover with their accent token's background — the icon color
   itself stays steady (icon-color-stays contract). */
/* Accent-aware borderless — when paired with a type, hover tints the
   background with the type's accent. Icon color stays steady; only the
   wash communicates the affordance (May 2026: removed the `color:
   var(--btn-accent)` shift to match the system-wide icon-button hover
   contract). */
.btn-primary.btn-icon.btn-borderless:hover   { background: var(--btn-accent-bg); }
.btn-secondary.btn-icon.btn-borderless:hover { background: var(--btn-accent-bg); }
.btn-tertiary.btn-icon.btn-borderless:hover  { background: var(--btn-accent-bg); }

/* ── Full-width modifier ───────────────────────────────────────────────── */
.btn-block { width: 100%; }

/* ── Justify modifier — push the label and the trailing affordance to the
   inline edges instead of the default centered cluster. Pure layout: it ONLY
   flips justify-content (center → space-between), so it needs a box wider
   than its content to show anything — compose `.btn-block` (or a fixed /
   contextual width). The trailing affordance reuses the existing icon
   mechanism — a child <svg> shown via `.btn-with-icon`, or `.btn-with-icon`'s `::after`
   arrow — so canonical usage is `.btn-primary .btn-block .btn-with-icon .btn-justify`.
   The base `gap` survives as the MINIMUM under space-between, so a long label
   never collides with the trailing icon. Common shape: a full-width card-footer
   / drawer / list-row CTA — label at the leading edge, → at the trailing edge.
   Single-class specificity (0,1,0) declared after the type rules, so it beats
   the base `justify-content: center` on source order. */
.btn-justify { justify-content: space-between; }

/* ── Legacy compat — btn-white maps to primary outline ─────────────────── */
.btn-white {
  display: inline-flex; align-items: center; gap: var(--space-8);
  background: transparent; color: var(--btn-accent-text);
  padding: var(--space-12) var(--space-24); border-radius: calc(var(--button-radius) - var(--space-4));
  font-size: var(--text-sm); font-weight: var(--fw-semibold); text-decoration: none;
  border: 1px solid rgba(255,255,255,0.12);
  transition: all 0.25s; font-family: var(--font-sans); cursor: pointer;
}
.btn-white:hover { background: var(--btn-accent-bg); border-color: rgba(255,255,255,0.25); }
[data-theme="light"] .btn-white { border-color: rgba(0,0,0,0.1); color: var(--accent-primary); }
[data-theme="light"] .btn-white:hover { background: color-mix(in oklch, var(--accent-primary) 6%, transparent); border-color: rgba(0,0,0,0.2); }

/* ── Neutral — workhorse tone, theme-adaptive currentColor overlay ──────
   Default (`.btn-neutral` alone): theme-adaptive 8 % `currentColor` overlay —
   white at 8 % in dark mode, black at 8 % in light mode. The most-used
   variant outside Primary: footer socials, design FAB, dismiss X, toolbar
   icon buttons, any "many buttons present, none of them THE action" surface.
   Sibling of `.chip-neutral` in the tone vocabulary — identical recipe,
   different family.

   Explicit variants override the adaptive default:
     .btn-neutral-light → forced light overlay (for dark backdrops)
     .btn-neutral-dark  → forced dark  overlay (for light backdrops)

   Migration note (May 2026 v16): renamed from `.btn-alpha`. Class was a
   Greek-letter dev-handle for the same recipe; the rename aligns the
   button family with the chip family's tone vocabulary. Full history in
   `.claude/skills/lifi-ds-docs/references/bug-history.md → v16`.
   ────────────────────────────────────────────────────────────────────── */
.btn-neutral {
  background: color-mix(in oklch, var(--text-primary) 8%, transparent);
  color: var(--text-primary);
  border-color: transparent;  /* keep the base rule's 1 px slot — 40 px ladder, not 38 (was `border: none`) */
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
}
.btn-neutral:hover {
  background: color-mix(in oklch, var(--text-primary) 14%, transparent);
}
.btn-neutral:active {
  background: color-mix(in oklch, var(--text-primary) 18%, transparent);
  transform: translateY(0);
}

/* Forced-light variant — white overlay regardless of page theme.
   Use on dark backdrops (hero, media overlays, dark cards). */
.btn-neutral.btn-neutral-light {
  background: rgba(255,255,255,0.10);
  color: rgba(255,255,255,0.95);
}
.btn-neutral.btn-neutral-light:hover { background: rgba(255,255,255,0.18); }
.btn-neutral.btn-neutral-light:active { background: rgba(255,255,255,0.24); }

/* Forced-dark variant — black overlay regardless of page theme.
   Use on light backdrops (white hero, bright media). */
.btn-neutral.btn-neutral-dark {
  background: rgba(0,0,0,0.08);
  color: rgba(0,0,0,0.88);
}
.btn-neutral.btn-neutral-dark:hover { background: rgba(0,0,0,0.14); }
.btn-neutral.btn-neutral-dark:active { background: rgba(0,0,0,0.20); }


/* ── Lift modifier — opt IN to the hover "lift" cues ───────────────────
   Buttons are STATIC by default (May 2026 inversion). A bare button's
   :hover paints only the bg / color / border wash — no movement, no glow.
   The bg wash alone communicates the hover affordance, which is the right
   default for product UI: dense rows, toolbars, forms, cards, and short-
   height containers where a lift would clip or read as fidgety.

   Compose `.btn-lift` to RE-INTRODUCE the hover lift + glow. This is
   mostly a marketing affordance — landing-page hero CTAs and other
   spacious website surfaces where the extra motion reads as inviting.
   Rarely wanted in product UI; reach for it deliberately.

   Replaces the retired `.btn-static` (inverted polarity — static used to
   be the opt-OUT and the default was lift; now static is the default and
   `.btn-lift` is the opt-IN). Per-variant values mirror the family's pre-
   inversion lift: -2px for filled / outline / neutral, -1px for secondary
   / tertiary; the glow was tightened (May 2026) so it doesn't clip at
   scroll-container edges. Each selector is fully qualified (variant +
   modifier) so it's purely additive over the static base — outline
   selectors carry one extra class so their tighter glow wins on
   specificity. Examples:
     class="btn-primary btn-lift"
     class="btn-secondary btn-lift"
     class="btn-success btn-outline btn-lift"
   ────────────────────────────────────────────────────────────────────── */
.btn-primary.btn-lift:hover,
.btn-destructive.btn-lift:hover,
.btn-success.btn-lift:hover {
  transform: translateY(-2px);
  box-shadow: 0 3px 10px var(--btn-accent-glow);
}
.btn-primary.btn-outline.btn-lift:hover,
.btn-destructive.btn-outline.btn-lift:hover,
.btn-success.btn-outline.btn-lift:hover {
  transform: translateY(-2px);
  box-shadow: 0 2px 8px var(--btn-accent-glow);
}
/* .btn-white maps to primary outline (see the legacy-compat rule above) —
   mirror the outline lift recipe so a white in-content CTA lifts in lockstep. */
.btn-white.btn-lift:hover {
  transform: translateY(-2px);
  box-shadow: 0 2px 8px var(--btn-accent-glow);
}
.btn-secondary.btn-lift:hover {
  transform: translateY(-1px);
  box-shadow: 0 2px 8px var(--btn-accent-glow);
}
.btn-tertiary.btn-lift:hover {
  transform: translateY(-1px);
}
.btn-neutral.btn-lift:hover {
  transform: translateY(-2px);
  box-shadow: 0 2px 8px color-mix(in oklch, var(--surface-page) 16%, transparent);
}


/* ══════════════════════════════════════════
   1b. SEGMENTED CONTROL — .seg

   A pill-shaped track housing equally-sized option buttons. The active
   segment gets a filled indicator that slides between options. Shares
   the button system's font, radius, and accent tokens so it pairs
   naturally alongside buttons in toolbars and forms.

   Anatomy:
     .seg                  — outer track (pill container)
     .seg-item             — individual option button
     .seg-item.is-active   — currently selected option

   Sizes:  .seg-sm, default, .seg-lg  (mirror .btn-sm / .btn-lg)
   Colors: alpha-surface scale only — no palette tint
   ══════════════════════════════════════════ */

/* ── Base ────────────────────────────────────────────────────
   Track + tab use neutral alpha-surface tokens (lighter tab floating
   above a darker track). The accent is carried ONLY by the active
   tab's text color and a soft halo in the drop shadow — the tab fill
   stays palette-neutral. No borders — the active tab's shadow carries
   the edge. Track padding is tight so the tab sits snug with minimal
   breathing room around it. */
/* Aligned to the catalog's Sizing Ladder (#sizing-ladders). Total seg
   height = wrapper padding × 2 + item-height. The wrapper uses --space-2
   (2px) on every size — the same token used for hairline borders and
   focus rings, sanctioned for tight inset wrappers. The 2px inset keeps
   the active pill snug against the track edge so the seg reads as a
   single tab strip rather than a heavily padded rail. Item math:
   padY × 2 + line-box = item-height (no border on seg-items). Labels
   follow the button ladder (12 / 14 / 16 / 18); calc-based line-height
   widens the line-box where padY tokens alone can't land the math.

     Wrapper r → Item r  (every radius is derived via calc() off
     --button-radius — the SAME channel the button ladder uses — so a
     .seg pairs concentrically with a .btn at the same tier AND tracks
     the playground per-theme Button-radius slider in lockstep. Values
     shown at the 16px anchor; item sits 4px tighter than its wrapper):
       sm:   8 → 4       default: 12 → 8
       lg:  16 → 12      xl:      20 → 16

     Heights:        32 → 40 → 48 → 56  (matches button ladder)
     Item heights:   28 → 36 → 44 → 52  (= height − 4 wrapper offset)
     Line-boxes:     12 → 20 → 20 → 20  (sm tight; default/lg/xl share 20)
*/
.seg {
  display: inline-flex; align-items: center;
  padding: var(--space-2); gap: var(--space-4);  /* 2px track inset — token-backed (see --space-2 comment) */
  border-radius: calc(var(--button-radius) - var(--space-4));  /* default wrapper — tracks --button-radius (ladder "default" = 12 at the 16 anchor) */
  /* Track — alpha-subtle: one step up the alpha scale so the recess
     reads more clearly. Light mode: ~6% black (darker well). Dark mode:
     ~8% white (more visible against the page). The active pill at
     --alpha-raised still floats clearly above. */
  background: var(--alpha-subtle);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  /* Never exceed the parent — if the track can't fit its natural
     content width, items shrink + truncate (see .seg-item below).
     Pair with `.seg--scroll` when truncation isn't acceptable and
     you'd rather swipe through the tabs. */
  max-width: 100%;
}
.seg-item {
  /* flex: 1 1 auto + min-width: 0 — critical pair. Default flex items
     have min-width: auto which refuses to shrink below their content's
     natural width, so long labels push the track wider than its
     parent. Setting min-width: 0 unlocks shrinking, and the overflow
     + text-overflow pair then truncates with an ellipsis. */
  flex: 1 1 auto;
  min-width: 0;
  position: relative;
  /* Inline-block (not inline-flex) so `text-overflow: ellipsis` actually
     renders the "…" character on long labels. inline-flex breaks ellipsis
     for anonymous text children — the text gets pixel-clipped by
     overflow:hidden but the ellipsis never paints. With inline-block,
     ellipsis works natively for the text-only case (the most common). For
     items that also carry a leading <svg>, the icon-aware layout below
     uses `:has(svg)` to flip back to inline-flex; those items don't ship
     long labels so ellipsis isn't required. See the catalog truncation
     demo (#segmented-control) for the canonical test. */
  display: inline-block;
  text-align: center;
  vertical-align: middle;
  padding: var(--space-8) var(--space-20);
  border: none; border-radius: calc(var(--button-radius) - var(--space-8)); /* default item — 4px tighter than wrapper, tracks --button-radius (8 at anchor) */
  background: transparent; color: var(--on-alpha-muted);
  font-family: var(--font-sans);
  font-size: var(--text-sm);                 /* 14px — default tier label */
  line-height: calc(20 / 14);                /* line-box 20 → 8+20+8 = 36 item → 40 seg */
  font-weight: var(--fw-semibold); letter-spacing: var(--tracking-normal);
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  text-decoration: none;  /* element-agnostic: no-op on <button>, suppresses the <a> underline so the seg works as a site-nav tab bar (.seg--raised) */
  transition: color 0.2s, background 0.2s, box-shadow 0.2s;
}
/* Icon-bearing items flip to inline-flex so a leading <svg> + label sit
   centered on a single baseline with token-driven gap. Pure-text items
   keep inline-block (above) so ellipsis works. `:has()` is supported in
   every browser this catalog targets. */
.seg-item:has(> svg) {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-8);
}
/* ── Icon support — Lucide outline SVGs inside a seg-item.
   Icon size scales with the seg size variant so icons feel
   proportional to the label and to the track. The +2px-over-cap-height
   convention keeps glyphs visually balanced with Figtree 600 labels.
     .seg-sm   → 14px icon  (label 14px)
     default   → 16px icon  (label 14px)
     .seg-lg   → 18px icon  (label 16px)
   Icons inherit `currentColor`, so the active/hover/focus colour rules
   above apply to them automatically. */
.seg-item > svg {
  flex: 0 0 auto;
  width: var(--space-16);
  height: var(--space-16);
}
.seg-item:hover {
  color: var(--on-alpha-default);
  background: var(--alpha-dim);
}
/* Keyboard focus ring — separate from hover so keyboard users see a
   ring even when they haven't moved the mouse. Offset so the ring
   lives outside the pill, not on top of its rounded edge. */
.seg-item:focus-visible {
  outline: 2px solid var(--accent-primary-mid);
  outline-offset: 2px;
  z-index: 1;
}
/* Disabled — both HTML `disabled` and ARIA `aria-disabled` so
   consumers can use either. */
.seg-item:disabled,
.seg-item[aria-disabled="true"] {
  opacity: 0.45;
  cursor: not-allowed;
  pointer-events: none;
}
/* Active: lighter alpha surface floating above the darker track.
   No drop shadow — the contrast between --alpha-raised and --alpha-dim
   carries the full "selected" signal. */
.seg-item.is-active {
  background: var(--alpha-raised);
  color: var(--text-primary);
}
.seg-item.is-active:hover {
  background: var(--alpha-overlay);
  color: var(--text-primary);
}

/* ── Size modifiers — aligned to the catalog's Sizing Ladder ──
   Wrapper radii derive from --button-radius via calc (anchor−8 / −4 / 0
   / +4 → 8 / 12 / 16 / 20 at the 16px anchor) — the same expressions the
   button ladder uses, so a .seg sits next to a .btn at the same tier with
   matching curves AND both track the playground Button-radius slider.
   Item radii sit 4px tighter (anchor−12 / −8 / −4 / 0 → 4 / 8 / 12 / 16).
   Wrapper padding stays at --space-2 (2px) on every size. */
.seg-sm { padding: var(--space-2); border-radius: calc(var(--button-radius) - var(--space-8)); }
.seg-sm .seg-item {
  padding: var(--space-8) var(--space-16);
  font-size: var(--text-caption);              /* 12px */
  line-height: 1;                              /* line-box 12 → 8+12+8 = 28 → 32 seg */
  border-radius: calc(var(--button-radius) - var(--space-12));
  gap: var(--space-8);
}
.seg-sm .seg-item > svg { width: var(--space-12); height: var(--space-12); }
.seg-lg { padding: var(--space-2); border-radius: var(--button-radius); }
.seg-lg .seg-item {
  padding: var(--space-12) var(--space-24);
  font-size: var(--text-body);                 /* 16px */
  line-height: calc(20 / 16);                  /* line-box 20 → 12+20+12 = 44 → 48 seg */
  border-radius: calc(var(--button-radius) - var(--space-4));
  gap: var(--space-8);
}
.seg-lg .seg-item > svg { width: var(--space-20); height: var(--space-20); }
.seg-xl { padding: var(--space-2); border-radius: calc(var(--button-radius) + var(--space-4)); }
.seg-xl .seg-item {
  padding: var(--space-16) var(--space-32);
  font-size: var(--text-body-lg);              /* 18px */
  line-height: calc(20 / 18);                  /* line-box 20 → 16+20+16 = 52 → 56 seg */
  border-radius: var(--button-radius);
  gap: var(--space-8);
}
.seg-xl .seg-item > svg { width: var(--space-24); height: var(--space-24); }

/* ── .seg--icon-only — squarer items for label-less segs.
   Icons centered in a stretched 10/20-padding pill read fine when
   the seg is .seg--block .seg--equal (the rail's case), but a
   stand-alone icon-only seg needs tighter horizontal padding so each
   pill feels balanced around the glyph rather than letterbox-wide.
   Padding scales with the seg size variant the same way label
   padding does — about half the horizontal padding of the labeled
   variant, which lands the touch area close to the WCAG min on lg. */
.seg--icon-only .seg-item { padding: var(--space-8) var(--space-12); }
.seg-sm.seg--icon-only .seg-item { padding: var(--space-8) var(--space-8); }    /* padY matches sm label tier */
.seg-lg.seg--icon-only .seg-item { padding: var(--space-12) var(--space-16); }
.seg-xl.seg--icon-only .seg-item { padding: var(--space-16) var(--space-20); }  /* xl — padY 16 matches sm label tier */

/* ── Layout modifiers ──────────────────────────────────────────
   .seg--block    — fills its parent width, not just hugs content.
                    Good in form groups where a segmented control
                    should own its entire row.
   .seg--hug      — opposite of --block: items keep their natural
                    label width and the seg sizes to content, with
                    leftover row width left empty. Good when the
                    seg sits in a wide slot (screen-header title
                    column, toolbar, dashboard nav row) and the
                    fill-the-row default would create awkward
                    over-wide tabs. The seg-item still shrinks
                    (flex: 0 1 auto) so very long labels still
                    truncate gracefully if the column gets tight.
                    Auto-applied inside `.screen-header` (see
                    the screen-header section for the contextual
                    rule).
   .seg--scroll   — overflow-x scrollable with hidden scrollbar.
                    Use when there are more options than fit and
                    you'd rather swipe than truncate (e.g. tabs
                    for 10+ filter categories). Items keep their
                    natural width.
   .seg--equal    — all items exactly the same width regardless of
                    label length. Good for symmetric toggles where
                    visual rhythm matters more than label fit. */
.seg--block { display: flex; width: 100%; }
.seg--hug {
  /* Strip the inline-flex stretch when the seg sits in a wider
     grid/flex slot (e.g. the .screen-header title column at 1fr).
     `justify-self: start` takes effect when the seg IS a grid item;
     it's a no-op otherwise. */
  justify-self: start;
}
.seg--hug .seg-item {
  /* `0 1 auto` instead of base `1 1 auto` — items keep natural width,
     can still shrink + truncate if the parent gets tight. */
  flex: 0 1 auto;
}
.seg--scroll {
  display: flex;
  max-width: 100%;
  overflow-x: auto;
  scrollbar-width: none;
  scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch;
}
.seg--scroll::-webkit-scrollbar { display: none; }
.seg--scroll .seg-item {
  flex: 0 0 auto;
  min-width: auto;
  overflow: visible;
  text-overflow: clip;
  scroll-snap-align: start;
}
.seg--equal .seg-item {
  flex: 1 1 0;  /* 1 1 0 instead of 1 1 auto = strict equal width */
  text-align: center;
}

/* .seg--vertical — items stack vertically instead of inline.
   Use in narrow contexts (sidebars, drawer rails, modals with little
   horizontal real estate) where a 4+ item horizontal seg would
   truncate to "Mark…" and stop reading as a control.
   The track keeps its alpha-subtle recess + radius; only the layout
   axis flips. Combine with .seg--block to fill the parent's width
   (the common case in a sidebar). Sizes (.seg-sm, default, .seg-lg)
   compose normally — the padding tokens just flow vertically. */
.seg--vertical {
  display: inline-flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-4);
}
.seg--vertical .seg-item {
  width: 100%;
  text-align: left;
  /* Override the default inline-context flex sizing so each row owns
     the full track width. flex: 0 0 auto stops items from shrinking
     to a single line of overlapping pills when the track is narrow. */
  flex: 0 0 auto;
}
.seg--vertical.seg--block { display: flex; }

/* ── .seg--borderless — rail-less variant.
   Drops the track surface AND the wrapper's 2 px inset so the active
   item itself sits at the full ladder tier (32 / 40 / 48 / 56). A
   .seg-sm.seg--borderless item is 32 px tall — the SAME height as a
   sibling .btn-sm — not 28 px (which the inset-and-item math of the
   default seg gives). Pair them in a toolbar row and the items align
   head-to-head; the wrapper never reads.

   Implementation: pin .seg-item to a fixed height with `height:
   var(--space-N)` and use `line-height: var(--space-N)` to vertically
   center text in its inline-block context (the seg's chosen ellipsis-
   safe display mode). Icon-bearing items flip to inline-flex via the
   existing `:has(> svg)` rule and centre via align-items: center; the
   line-height-as-spacing-token value is harmless there.

   Active fill swaps to --alpha-subtle, NOT --alpha-raised. Without the
   rail to recess against, --alpha-raised (88% white in light) reads as
   white-on-white. --alpha-subtle (~6% black light, ~8% white dark)
   gives a quiet "shade darker / lighter than the page" — exactly the
   trading-app borderless tab idiom. Hover-on-active stays at --alpha-subtle
   (no extra lift — the page bg is already the baseline).

   Composes with every other seg modifier:
     .seg--borderless                 default size, label only
     .seg--borderless.seg-sm          all four tiers (sm/default/lg/xl)
     .seg--borderless.seg--icon-only  icon-only borderless tabs
     .seg--borderless.seg--block      borderless tabs that fill a row
     .seg--borderless.seg--hug        borderless tabs sized to their labels
     .seg--borderless.seg--equal      strict-equal-width borderless tabs

   Reach for it when:
     - the seg sits inside a screen header / dense toolbar and the rail
       competes with surrounding chrome
     - the parent surface already provides framing (modal head, card
       header, screen-header trailing slot)
     - "minimal, content-first" is the design intent (trading screens,
       data filters, dashboard mode pickers)
   Reach for the default .seg instead when the seg sits on a busy
   background or needs to read as a distinct control unit. */
.seg--borderless {
  padding: 0;                              /* drop the 2 px wrapper inset */
  background: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
.seg--borderless .seg-item {
  /* Item carries the ladder height directly (default tier — 40 px) so
     pairing with .btn (40), .input (40), .switch (40) lines up head-to-
     head. line-height = height centers the text in the inline-block. */
  height: var(--space-40);
  line-height: var(--space-40);
  padding-block: 0;
  /* Radius bump — in borderless, the wrapper paints no chrome (no bg,
     no track inset, padding:0), so the ITEM is the only visible
     silhouette. The default inner-concentric step (item = wrapper − 4)
     was tuned for the FILLED variant where the wrapper's painted track
     forms the outer corner; in borderless there's no outer corner to
     be concentric with. Bump each tier so item radius = wrapper radius
     = btn radius at that tier — stacking a borderless seg above a
     same-tier .btn now reads as one family. Still derives from
     --button-radius via calc(), so the playground per-theme
     Button-radius slider keeps rounding everything in lockstep.
     June 2026 v2 — see CLAUDE.md → Radius system → Migration history. */
  border-radius: calc(var(--button-radius) - var(--space-4));   /* default → 12 at the 16 anchor (was 8) */
}
.seg-sm.seg--borderless .seg-item { height: var(--space-32); line-height: var(--space-32); border-radius: calc(var(--button-radius) - var(--space-8)); } /* sm → 8 (was 4) */
.seg-lg.seg--borderless .seg-item { height: var(--space-48); line-height: var(--space-48); border-radius: var(--button-radius); }                        /* lg → 16 (was 12) */
.seg-xl.seg--borderless .seg-item { height: var(--space-56); line-height: var(--space-56); border-radius: calc(var(--button-radius) + var(--space-4)); } /* xl → 20 (was 16) */
.seg--borderless .seg-item.is-active {
  background: var(--alpha-subtle);
}
/* Hover — both inactive and active items lift to --alpha-subtle. The
   default seg's `.seg-item:hover { background: --alpha-dim }` was tuned
   to read against the track recess; without the rail, --alpha-dim
   (~3% black light, ~4% white dark) is effectively invisible. Bumping
   to --alpha-subtle gives a clear hover signal. Inactive items also
   inherit `color: --on-alpha-default` from the base hover rule (one
   notch brighter than --on-alpha-muted); active items keep their
   --text-primary label color through the existing .is-active cascade,
   so "hovering an inactive tab" and "the active tab" still read
   distinctly via text colour, not bg. */
.seg--borderless .seg-item:hover {
  background: var(--alpha-subtle);
}

/* ── .seg--raised — borderless tabs with a RAISED card-pill active item.
   Composes ON .seg--borderless (requires BOTH classes). Where borderless's
   active item is a quiet translucent shade (--alpha-subtle), --raised lifts
   it to an opaque card pill (--surface-card + --elev-1) floating on the
   transparent track — the web-app top-nav idiom (jumper.xyz / lifi.fi /
   Uniswap), vs. borderless's trading-app "shade darker" tab. Inactive-item
   hover drops to the lighter neutral list-row wash (4% --text-primary), not
   --alpha-subtle, so a grazed tab never masquerades as the selected pill.
   Pill radius (--r-full) — the tab-bar silhouette. Only THREE properties
   change vs. borderless (active fill, hover, radius); height ladder, track-
   clearing, icon/avatar support all inherit.

   Element-agnostic: <button role="tab"> for an in-place view switch, or
   <nav> + <a aria-current="page"> for site navigation wearing the same look
   — the variant styles the chrome, the consumer picks the semantics.

   Canonical (June 2026) — documented at #segmented-control (#seg-raised
   card) + design/components/foundations.md. First consumer: the .host-nav
   site-preview bar. Graduated from the Lab incubator on approval.

   Compose: .seg.seg--borderless.seg--raised  (+ a size tier — .seg-lg for nav). */
.seg--borderless.seg--raised .seg-item { border-radius: var(--r-full); }
.seg--borderless.seg--raised .seg-item.is-active,
.seg--borderless.seg--raised .seg-item.is-active:hover {
  background: var(--surface-card);
  box-shadow: var(--elev-1);
  color: var(--text-primary);
}
.seg--borderless.seg--raised .seg-item:hover {
  background: color-mix(in oklch, var(--text-primary) 4%, transparent);
  color: var(--text-primary);
}

/* ── Avatar tabs — content-driven square cells.
   When a borderless seg-item carries an .avatar-tandem (or a lone
   .avatar), the item becomes a square cell sized to its borderless
   tier height and the content centers. The avatar IS the trigger; no
   .seg--avatar opt-in modifier is needed. Per CLAUDE.md → "Auto-detect
   content-driven state via :has() — don't require an .is-foo opt-in
   modifier on the parent when the trigger IS the content shape." Same
   shape as the existing .seg-item:has(> svg) rule.

   Specificity (0,3,0) beats .seg-sm/.seg-lg/.seg-xl .seg-item padding
   (0,2,0) so size-modifier horizontal padding (16/20/24/32) doesn't
   bleed through and leave a wide pill around the avatar. flex: 0 0
   auto stops the cell from stretching to share the track width — the
   square reads as content-fit, not row-distributed.

   Recommended tandem pairing (24 / 32 / 40 / 48 tandems each leave a
   4 px symmetric breathing slot inside the cell):
     .seg-sm.seg--borderless  (32 cell) → .avatar-tandem--xs  (24)
     .seg.seg--borderless     (40 cell) → .avatar-tandem--sm  (32)
     .seg-lg.seg--borderless  (48 cell) → .avatar-tandem      (40)
     .seg-xl.seg--borderless  (56 cell) → .avatar-tandem--lg  (48) */
.seg--borderless .seg-item:has(> .avatar-tandem),
.seg--borderless .seg-item:has(> .avatar) {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  aspect-ratio: 1 / 1;
  padding-inline: 0;
}

/* ════════════════════════════════════════════════════════════════
   .seg-overflow — surplus tabs collapse into a "···" menu
   ════════════════════════════════════════════════════════════════
   When the mode set outgrows the bar, the trailing tabs move into an
   overflow menu opened by a "···" trigger. The trigger is a .seg-item
   that's ALSO a .dropdown__trigger, so it reuses initDropdown()'s
   Popover-API machinery: the panel renders in the browser top-layer
   (escaping the host card's overflow:hidden clip — see CLAUDE.md →
   "Floating element clipped by ancestor overflow"), and outside-click /
   Escape / scroll-resize reposition all come for free. The .dropdown
   wrapper is the seg's flex child and does NOT grow (flex: 0 0 auto)
   so the dots button hugs its glyph instead of claiming a track share.

   Active state — when the active mode lives inside the menu, swap.js →
   applyMode() adds .is-active to the trigger (borderless fill) and
   .is-current to the matching .menu-item (checkmark). The trigger stays
   a fixed "···" so the bar geometry never shifts per selection.

   Chrome neutralization — the trigger composes `.dropdown__trigger--seg`
   (sibling to --bare / --flush in the dropdown variant family) to
   suppress every chrome property the default trigger ships. That rule
   lives next to its dropdown family in the Dropdown section; see the
   `.dropdown__trigger--seg` block lower in this file. .seg-item--more
   stays as a markup-only marker (semantic + JS hook for swap.js); the
   visual contract belongs to --seg. */
.seg-overflow {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: stretch;
}

/* ════════════════════════════════════════════════════════════════
   .seg-menu — trailing tab that's ALSO a dropdown (the "menu tab")
   ════════════════════════════════════════════════════════════════
   Sibling to .seg-overflow. Where .seg-overflow collapses surplus
   tabs behind a STATIC "···" glyph (and never reveals which mode is
   held behind it), the menu tab shows the SELECTED option's LABEL +
   a chevron — the trailing slot is a swappable "alternative" tab.
   First N tabs are fixed; the last tab lets the user pick a different
   option, and the pick becomes the tab's label + the active mode.

   SPLIT-TAB interaction — two hit regions inside one pill:
     • .seg-item--menu   (label half)  → click ACTIVATES the current
       option, like a normal tab. It IS a .seg-item, so it inherits
       every seg typography / colour / sizing-ladder rule + the
       borderless active/hover fills, and it participates in tab
       activation (carries data-mode + role="tab").
     • .seg-menu__toggle (chevron half) → click OPENS the dropdown
       menu. It's the .dropdown__trigger; composing .dropdown__trigger--seg
       strips the dropdown's own chrome so the seg owns the visuals.

   The two are SIBLINGS, never nested — a <button> inside a <button>
   is auto-closed by the HTML parser (see CLAUDE.md → "Card root element
   — button-in-button is a parser footgun"). The .seg-menu wrapper is
   the .dropdown AND a non-growing seg flex child; overflow:hidden +
   the item-radius tier clip both halves into one rounded pill, so the
   per-half hover bg and the unified active fill read as a single tab.
   Chevron rotates on [data-open] via the inherited .dropdown__chevron
   rule.

   Behaviour: shared.js → initSegMenu() swaps the label on pick and
   routes through the canonical activation path (label .click()).
   Open/close/reposition reuse initDropdown()'s Popover-API machinery
   (top-layer escape from any ancestor overflow:hidden). Canonical
   context is .seg--borderless (screen-header mode pickers); that's
   what the active/hover fills below are tuned for.

   TWO CHEVRON BACKENDS — the chevron half is backend-agnostic:
     • Dropdown popover (catalog #seg-menu, the live backend): wrapper is
       also a .dropdown, chevron is a .dropdown__trigger; pick runs through
       shared.js → initSegMenu() and open/close reuse initDropdown()'s
       Popover API.
     • Custom action: the chevron can instead carry a data-action that opens
       any picker (e.g. a full-screen modes SCREEN), with applyMode-style
       adoption of the active mode into the label — no .dropdown, no panel.
       The playground swap widget used this until June 2026 (the tabs
       presentation); the primitive keeps the capability so an overflow
       chevron can re-mount when modes exceed three (DCA / perps).

   Selector is `.seg .seg-menu` (specificity 0,2,0), NOT bare
   `.seg-menu`: it must win regardless of the wrapper. Where the wrapper is
   a `.dropdown`, that rule (`display: inline-flex`, declared LATER in this
   file) would otherwise govern at equal specificity (0,1,0); and the
   playground variant where the wrapper is NOT a `.dropdown` has no `display`
   source at all and would render block. Qualifying with the always-present
   `.seg` ancestor wins cleanly in both cases — the two halves lay out as a
   flex row with the per-half flex sizing below. (.seg-overflow has the same latent
   collision but never noticed: single ··· child, block-vs-flex moot.) */
.seg .seg-menu {
  flex: 0 0 auto;                  /* hug label + chevron; don't claim a track share */
  display: inline-flex;
  align-items: stretch;            /* both halves fill the item height */
  /* Mirrors the .seg-item radius ladder exactly — the menu sits in the
     track in PLACE of an item, so it wears the item's corners. Derived
     via calc() off --button-radius (anchor−8 default → 8 at the 16
     anchor) so it tracks the playground per-theme Button-radius slider
     in lockstep with seg-items. June 2026 — was literal var(--space-8);
     the seg-item token-binding migration's missed sibling (value-paired
     but token-disconnected). */
  border-radius: calc(var(--button-radius) - var(--space-8));
  overflow: hidden;                /* clip both halves' hover/active bg into one pill */
}
.seg-sm .seg-menu { border-radius: calc(var(--button-radius) - var(--space-12)); }
.seg-lg .seg-menu { border-radius: calc(var(--button-radius) - var(--space-4)); }
.seg-xl .seg-menu { border-radius: var(--button-radius); }

/* Borderless variant — the menu split-picker sits in the track in PLACE
   of a regular item, so it mirrors the seg-item ladder. When the item
   ladder bumps up in borderless (see .seg--borderless .seg-item above),
   the split-menu wrapper bumps with it — otherwise a menu-tab would
   read tighter than its sibling items at the same tier. */
.seg--borderless .seg-menu       { border-radius: calc(var(--button-radius) - var(--space-4)); } /* default → 12 (was 8)  */
.seg-sm.seg--borderless .seg-menu { border-radius: calc(var(--button-radius) - var(--space-8)); } /* sm      → 8  (was 4)  */
.seg-lg.seg--borderless .seg-menu { border-radius: var(--button-radius); }                        /* lg      → 16 (was 12) */
.seg-xl.seg--borderless .seg-menu { border-radius: calc(var(--button-radius) + var(--space-4)); } /* xl      → 20 (was 16) */

/* Label half — reuses .seg-item wholesale (typography, colour states,
   the borderless height ladder, active/hover fills, data-mode-driven
   activation). The wrapper owns the corners, so the item drops its own
   radius; its right padding trims to --space-12 because the chevron
   half sits to its right and carries the trailing breathing room. */
.seg-menu .seg-item--menu {
  border-radius: 0;
  padding-right: var(--space-12);
}

/* Chevron half — opens the picker (a .dropdown__trigger in the popover
   backend, or a plain data-action button in a custom-action backend; see
   the TWO CHEVRON BACKENDS note above). Deliberately NOT a .seg-item: that
   keeps the `.seg-item > svg { 16px }` rule from inflating the chevron,
   which stays 12px via .dropdown__chevron. In the popover backend chrome
   is stripped by .dropdown__trigger--seg; a custom-action backend has no
   default chrome to strip. Layout only here — hug the chevron, square
   corners (wrapper clips), stretch to the half's height via the wrapper's
   align-items:stretch. The :where()-stripped .dropdown__trigger padding
   (specificity 0) loses to padding-inline. */
.seg-menu__toggle {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding-inline: var(--space-12);
  /* Reset the UA <button> chrome. The popover backend's chevron is a
     .dropdown__trigger (which resets border to 1px transparent), but a
     custom-action chevron is a PLAIN <button> — without these resets it
     leaks the UA default border (2px outset) + buttonface background, which
     reads as a hard box around the chevron half (glaring in dark mode). Per
     CLAUDE.md → "border: 0 is explicit, not omitted" — a <button> carries a
     UA border that leaks if you only omit the declaration. Height comes from
     the wrapper's align-items:stretch, not a border slot, so border:0 is
     safe (no ladder math to preserve). */
  background: transparent;
  border: 0;
  border-radius: 0;
  cursor: pointer;
}
/* Render the chevron in a custom-action backend too — the popover backend
   gets `display: inline-block` from `.dropdown__trigger > .dropdown__chevron`,
   but a plain data-action toggle isn't a .dropdown__trigger, so assert it
   here so the chevron shows regardless of backend. */
.seg-menu__toggle > .dropdown__chevron {
  display: inline-block;
}

/* Per-half hover — the label half inherits the borderless hover fill via
   `.seg--borderless .seg-item:hover`; mirror it onto the chevron half so
   both regions give the same hover signal. The split is legible because
   each half tints independently (you see which action you're about to
   trigger), clipped to the pill by the wrapper's overflow:hidden. */
.seg--borderless .seg-menu__toggle:hover {
  background: var(--alpha-subtle);
}

/* Unified active fill — when the menu-tab's option is the active mode,
   the LABEL half fills via the inherited `.seg--borderless .seg-item.is-active`
   rule; this mirrors that fill onto the CHEVRON half so the whole pill
   reads as one active tab. Content-driven via :has() (the label being
   active IS the trigger) per CLAUDE.md → "Auto-detect content-driven
   state via :has()". */
.seg--borderless .seg-menu:has(> .seg-item--menu.is-active) > .seg-menu__toggle {
  background: var(--alpha-subtle);
}

/* Focus ring on the WRAPPER, not the halves. Two reasons: (1) the chevron
   half is a plain <button> with no seg focus styling, so it falls back to
   the UA default black outline; (2) the wrapper's overflow:hidden (which
   clips the two halves into one pill) would ALSO clip a ring drawn on an
   inner half. So: suppress both halves' own outline on focus, and draw one
   accent ring around the whole pill on keyboard focus — the wrapper's own
   outline isn't clipped by its own overflow. Matches .seg-item:focus-visible
   (2px accent-primary-mid, offset 2). Suppressing on :focus (not just
   :focus-visible) also kills the black box after a mouse/programmatic focus
   return (e.g. closing the modes screen); the ring only paints for keyboard
   users via :has(:focus-visible). */
.seg .seg-menu .seg-item--menu:focus,
.seg .seg-menu__toggle:focus {
  outline: none;
}
.seg .seg-menu:has(:focus-visible) {
  outline: 2px solid var(--accent-primary-mid);
  outline-offset: 2px;
  z-index: 1;
}

/* ════════════════════════════════════════════════════════════════
   .ds-doc-tab-nav — quiet tab / filter nav (link + underline)
   ════════════════════════════════════════════════════════════════
   The "docs-site nav" pattern: a horizontal row of links / buttons
   where the active item is marked with brand-accent text + an
   accent-coloured 2 px underline along the bottom edge.

   Reach for this when:
     - filtering a long page by category (catalog scope filter)
     - tab-style navigation that anchors to sections (`#topline`,
       `#kpis`) on a dashboard / report page
     - in-card sub-views where the segmented .seg feels too pill-y

   Reach for .seg instead when:
     - the choice is N equal-weight options that should read as a
       "control" (mutually exclusive, no obvious "default")
     - the visual needs a clearly-bounded track to read as one unit
       inside a busy surface

   Reach for .ds-doc-viewport-tabs instead when:
     - the tabs sit inside a .ds-doc-viewport-tabbed frame and swap
       panes; that's the documented per-card pattern.

   API
     <nav class="ds-doc-tab-nav" aria-label="…">          ← anchor links
       <a href="#…" class="ds-doc-tab-nav-item is-active">…</a>
       <a href="#…" class="ds-doc-tab-nav-item">…</a>

     <div class="ds-doc-tab-nav" role="tablist">          ← buttons / tabs
       <button class="ds-doc-tab-nav-item is-active" role="tab"
               aria-selected="true">…</button>

   The `is-active` class AND `[aria-selected="true"]` both trigger
   the active visual, so consumers can pick whichever pairs cleanly
   with their JS. Keep `aria-selected` for true tablist semantics
   (catalog filter, dashboard section nav); use `is-active` alone
   for plain `<a>` link nav (no implicit ARIA needed).

   VARIANTS
     (default)              body (16 px), 12/16 px padding — the catalog default
     .ds-doc-tab-nav--sm        sm (14 px), 8/12 px padding — tight contexts
     .ds-doc-tab-nav--lg        body-lg (20 px), 16/20 px padding — hero / page-section nav
     .ds-doc-tab-nav--borderless drop the wrapper's bottom hairline (e.g. inside
                              cards where the surrounding surface already frames it)
     .ds-doc-tab-nav--equal     items flex 1 1 0 → strict equal width across track

   ACCESSIBILITY
     Wrap in <nav aria-label="…"> for link-style use; in <div role="tablist">
     for filter/tab use. The CSS doesn't enforce either; that's a contract
     the consumer signs.
*/
.ds-doc-tab-nav {
  display: flex;
  align-items: stretch;
  gap: var(--space-4);
  border-bottom: 1px solid color-mix(in oklch, var(--text-primary) 8%, transparent);
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  /* Counter the wrapper's bottom border so the active item's underline
     sits flush with the bottom edge (the item's `margin-bottom: -1px`
     overlaps this border). */
  margin-bottom: 0;
}
.ds-doc-tab-nav::-webkit-scrollbar { display: none; }

.ds-doc-tab-nav-item {
  /* Reset for both <a> and <button> hosts. */
  font-family: var(--font-sans);
  font-size: var(--text-body);
  font-weight: var(--fw-medium);
  line-height: 1;
  color: var(--text-muted);
  text-decoration: none;
  white-space: nowrap;
  padding: var(--space-12) var(--space-16);
  display: inline-flex;
  align-items: center;
  border: 0;
  background: transparent;
  border-bottom: 2px solid transparent;
  /* Overlap the wrapper's bottom border so the underline reads as a
     single line (instead of stacking the wrapper's hairline with the
     item's accent border). */
  margin-bottom: -1px;
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease;
  flex-shrink: 0;
  letter-spacing: var(--tracking-normal);
}

.ds-doc-tab-nav-item:hover {
  color: var(--text-primary);
  border-bottom-color: color-mix(in oklch, var(--accent-primary) 30%, transparent);
}

.ds-doc-tab-nav-item.is-active,
.ds-doc-tab-nav-item[aria-selected="true"] {
  color: var(--accent-primary);
  border-bottom-color: var(--accent-primary);
  font-weight: var(--fw-semibold);
}

.ds-doc-tab-nav-item:focus-visible {
  outline: 2px solid color-mix(in oklch, var(--accent-primary) 50%, transparent);
  outline-offset: 4px;
  border-radius: 4px;
}

/* ── Sizes ─────────────────────────────────────────────────────── */
.ds-doc-tab-nav--sm .ds-doc-tab-nav-item {
  font-size: var(--text-sm);
  padding: var(--space-8) var(--space-12);
}

.ds-doc-tab-nav--lg .ds-doc-tab-nav-item {
  font-size: var(--text-body-xl);
  padding: var(--space-16) var(--space-20);
}

/* ── Modifiers ────────────────────────────────────────────────── */

/* Drop the wrapper's bottom hairline — useful inside cards or
   dense surfaces where the surrounding chrome already frames the
   nav. The active item still gets its accent underline. */
.ds-doc-tab-nav--borderless {
  border-bottom: 0;
}

/* Equal-width items — useful for short, fixed-count tabs that
   should distribute evenly across the available width. */
.ds-doc-tab-nav--equal { gap: 0; }
.ds-doc-tab-nav--equal .ds-doc-tab-nav-item {
  flex: 1 1 0;
  justify-content: center;
}

/* ════════════════════════════════════════════════════════════════
   .ds-doc-section-toc — auto-generated chapter nav at section top
   ════════════════════════════════════════════════════════════════
   When a section contains 2+ `<h2 class="ds-doc-subsection">` headings
   with id attributes, `initDsSectionTocs()` (shared.js) prepends one
   of these — a horizontal anchor strip listing each chapter. Click =
   jump-scroll to the chapter; scroll-spy highlights the active
   chapter via IntersectionObserver.

   Why a section-level TOC (and not sidebar-deepening or accordion):
     - Designers already used the sidebar to land on the section.
       This is wayfinding WITHIN the section — scrubbing, not jumping.
     - Sidebar deepening duplicates the same job in a busier place.
     - Accordions hide content; TOC shows the whole map.

   AUTO-DEPLOY
     - 0–1 chapters: no TOC. The section reads as today.
     - 2+ chapters: TOC renders. Cards above the first chapter heading
       are "overview" cards and live before any TOC entry — they don't
       need a chapter, they ARE the lede.

   SCROLL-SPY
     The active chapter is whichever heading is currently nearest the
     top of the viewport (with a small offset to account for catalog
     hero / page chrome). IntersectionObserver tracks all chapter
     headings; the most-recently-passed one wins.

   COMPOSITION
     Reuses .ds-doc-tab-nav as the visual primitive — same underline,
     same hover, same accent. The .ds-doc-section-toc class adds:
       - margin spacing appropriate for sitting at the top of a section
       - tighter type (smaller than the page-level .ds-doc-tab-nav--lg
         scope filter so the hierarchy stays readable)

   The TOC is sized at default `.ds-doc-tab-nav` (16 px, 12/16 padding) —
   between page-level .ds-doc-tab-nav--lg (20 px, scope filter) and per-card
   .ds-doc-viewport-tab (14 px, pane switcher). Three sizes signal three
   levels of nav.
*/
.ds-doc-section-toc {
  /* Appears after the section's eyebrow + title + description, before
     any cards. Inline; scrolls with the section. */
  margin: var(--space-24) 0 var(--space-32);
}

/* Light-section "TOC absent" state — when JS finds < 2 chapter
   headings, no TOC is rendered. Sections without enough chapters
   read identically to today. No CSS rule needed for that case. */

/* No [data-theme="light"] overrides needed — alpha surface tokens
   auto-invert and --accent-* is palette-aware. */


/* ══════════════════════════════════════════
   5b. SLIDER — Apple HIG-inspired range input

   Anatomy:
     .slider              — wrapper (provides layout row with optional icons)
     .slider-range        — the <input type="range"> itself
     .slider-min-icon     — optional leading icon (min value indicator)
     .slider-max-icon     — optional trailing icon (max value indicator)
     .slider-label        — optional label (top)
     .slider-value        — live numeric readout (e.g. "65%")
     .slider-ticks        — container for tick marks (optional, for discrete)

   Variants:
     .slider--sm          — 4px track / 16px thumb (compact, e.g. sidebar)
     .slider--lg          — 8px track / 24px thumb (comfortable touch target)
     .slider--accent2     — secondary accent (teal) fill
     .slider--accent3     — tertiary accent (pink) fill
     .slider--discrete    — show tick marks for stepped values

   States: default, hover, focus-visible, active (dragging), disabled.

   Fill uses a linear-gradient background whose size is driven by the
   CSS custom property `--slider-pct` (0..100) — update it via JS on input.
   ══════════════════════════════════════════ */

.slider {
  display: flex; align-items: center; gap: var(--cluster-md);
  width: 100%; position: relative;
}
.slider-label-row {
  display: flex; justify-content: space-between; align-items: baseline;
  width: 100%; margin-bottom: var(--space-4);
}
/* May 2026 — .slider-label + .slider-value match the theme composer's
   .ds-doc-control-label + .ds-doc-control-val recipe: plain typography,
   no accent / gradient text-fill, semibold value with muted ink.
   The accent-tinted "branded readout" recipe was retired in favour of
   visual integration with the rest of the rail editor surface (settings
   rows, theme composer rows). The track + thumb still consume the
   accent via --_slider-fill — chrome stays brand-forward, the readout
   gets out of the way.

   Type sizes bumped one tier (label 14 → 16, value 12 → 14) so the
   label-row reads as the slider's HEADER, not its caption — designers
   scan the label first, slider chrome second. Hierarchy preserved:
   label (16 / medium / secondary) > value (14 / semibold / muted)
   > tick (12 / medium / faint), each one tier above the next.
   June 2026 — readouts moved off mono onto the standard sans (designer
   direction: no monotype in the composer or the controls; tabular-nums
   keeps digits stable while dragging). */
.slider-label {
  font-size: var(--text-body); font-weight: var(--fw-medium); color: var(--text-secondary);
  letter-spacing: var(--tracking-snug);
}
.slider-value {
  font-family: var(--font-sans);
  font-size: var(--text-sm); font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-normal);
  color: var(--text-muted);
  /* Tabular numerals so digits don't shift width while dragging */
  font-variant-numeric: tabular-nums;
  min-width: 3ch; text-align: right;
}

/* ── Editable readout — input.slider-value ────────────────────────
   The readout can be an <input inputmode="decimal"> wearing the same
   .slider-value class: visually identical to the static span at rest
   (transparent, right-aligned), it reveals an affordance on
   hover and becomes a precision entry field on focus. Commit / clamp /
   arrow-step semantics are wired by shared.js → lifiBindSliderValue
   (Enter/blur commits, ↑/↓ steps, Shift+↑ steps ×10, Esc reverts).
   June 2026 — the Theme Composer redesign promoted this onto the base
   slider so every slider consumer can opt into numeric precision. */
input.slider-value {
  appearance: none;
  -webkit-appearance: none;
  border: 0;
  background: transparent;
  margin: 0;
  padding: var(--space-2) var(--space-4);
  /* Right-edge compensation — the input's own padding would inset the
     digits from the row edge the static span sits flush against. */
  margin-right: calc(-1 * var(--space-4));
  width: 7ch;
  border-radius: var(--space-4);
  transition: background 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;
}
@supports (field-sizing: content) {
  input.slider-value { width: auto; field-sizing: content; min-width: 3ch; }
}
input.slider-value::-webkit-outer-spin-button,
input.slider-value::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
input.slider-value:hover {
  background: color-mix(in oklch, var(--text-primary) 5%, transparent);
}
input.slider-value:focus-visible {
  outline: none;
  background: color-mix(in oklch, var(--text-primary) 5%, transparent);
  box-shadow: 0 0 0 1px var(--field-border-focus), 0 0 0 4px var(--field-ring);
  color: var(--text-primary);
}
input.slider-value:disabled {
  opacity: 0.5;
  pointer-events: none;
  background: transparent;
}

/* Value cluster — readout + unit as one right-aligned pair in the
   label row. */
.slider-value-group {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-4);
}

/* ── .slider-unit — optional unit suffix beside the readout ───────
   Units (px · ° · L) live OUTSIDE the editable value so parsing stays
   trivial and the digits stay clean. Quiet mono, one ink step below
   the readout. */
.slider-unit {
  font-family: var(--font-sans);
  font-size: var(--text-caption);
  font-weight: var(--fw-medium);
  color: var(--text-faint);
  flex-shrink: 0;
}

/* ── .slider-field — labeled-control unit (label row + range) ─────
   The universal wrapper for "label + numeric readout above a range" —
   the shape the Theme Composer, catalog control demos, and future
   settings panes all approximate. Hosts a .slider-label-row followed
   by a .slider (or bare .slider-range).

   .slider-field--compact is the dense-panel tier: label drops to
   --text-sm and the readout to --text-caption — for multi-control
   editors (the Theme Composer) where the default 16/14 voice reads
   oversized. Standalone feature sliders keep the default tier. */
.slider-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  width: 100%;
}
.slider-field--compact .slider-label { font-size: var(--text-sm); }
.slider-field--compact .slider-value { font-size: var(--text-caption); }
.slider-field--compact .slider-unit  { font-size: var(--text-micro); }

.slider-min-icon, .slider-max-icon {
  color: var(--text-faint); flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  transition: color 0.2s;
}
.slider-min-icon svg, .slider-max-icon svg { width: var(--space-20); height: var(--space-20); }
.slider:hover .slider-min-icon,
.slider:hover .slider-max-icon { color: var(--text-muted); }

/* ── The range input itself ──────────────────────────────────────── */
.slider-range {
  /* Reset */
  -webkit-appearance: none; appearance: none;
  background: transparent; margin: 0; padding: 0;
  font-family: inherit;                              /* defensive — <input> UA default is Arial; inherit brand Figtree (input equivalent of the button reset) */
  /* Layout */
  flex: 1; height: var(--space-24); /* touch target per Apple HIG ≥20pt */
  cursor: pointer; outline: none;
  /* Fill % — DO NOT set this directly from a naive `(value-min)/(max-min)`
     formula. Use the canonical setter `window.lifiSliderFill(input)` in
     shared.js, which corrects for the WebKit thumb-inset offset (browsers
     constrain the thumb's center to the track, so a naive pct paints the
     gradient stop off from the thumb's actual visual center — up to
     ±half-thumb-width error at the extremes). The helper auto-wires every
     `.slider-range` on the page via DOMContentLoaded + MutationObserver,
     so newly-rendered sliders pick up the correct fill without explicit
     wiring. Initial value here is a placeholder used only if JS doesn't
     run (graceful degradation). */
  --slider-pct: 50%;
  --slider-track-h: var(--space-8);
  --slider-thumb-d: var(--space-20);
  /* Inherit the fill token for use in child ::value calculations */
  --_fill: var(--_slider-fill, var(--accent-primary));
}
.slider-range:disabled {
  cursor: not-allowed; opacity: 0.45;
  filter: saturate(0.6);
}

/* Track — WebKit/Blink
   Uses a subtle gradient fill (brand blue → mid shift toward secondary) for
   depth, plus a soft inset shadow on the empty portion for a recessed look. */
.slider-range::-webkit-slider-runnable-track {
  height: var(--slider-track-h); border-radius: var(--r-full);
  background:
    linear-gradient(to right,
      color-mix(in oklch, var(--_fill) 100%, var(--accent-secondary) 0%) 0%,
      color-mix(in oklch, var(--_fill) 75%, var(--accent-secondary) 25%) calc(var(--slider-pct) * 0.5),
      var(--_fill) var(--slider-pct),
      color-mix(in oklch, var(--text-primary) 10%, transparent) var(--slider-pct) 100%);
  box-shadow:
    inset 0 1px 1px color-mix(in oklch, var(--surface-page) 60%, transparent),
    inset 0 -1px 0 color-mix(in oklch, var(--text-primary) 5%, transparent);
  transition: box-shadow 0.25s ease;
}
/* Track — Firefox (no paint split; use flat empty + progress pseudo for fill) */
.slider-range::-moz-range-track {
  height: var(--slider-track-h); border-radius: var(--r-full);
  background: color-mix(in oklch, var(--text-primary) 10%, transparent);
  box-shadow: inset 0 1px 1px color-mix(in oklch, var(--surface-page) 60%, transparent);
}
.slider-range::-moz-range-progress {
  height: var(--slider-track-h); border-radius: var(--r-full);
  background: linear-gradient(to right,
    color-mix(in oklch, var(--_fill) 75%, var(--accent-secondary) 25%) 0%,
    var(--_fill) 100%);
}

/* Thumb — WebKit/Blink
   Premium "glass bead" feel: vertical white gradient with a top highlight,
   thin accent border, and a layered ambient + drop shadow stack. */
.slider-range::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: var(--slider-thumb-d); height: var(--slider-thumb-d);
  border-radius: 50%;
  background:
    /* Top highlight — bright white arc */
    radial-gradient(circle at 50% 28%,
      #fff 0%,
      color-mix(in oklch, #fff 95%, var(--_fill)) 55%,
      color-mix(in oklch, #fff 86%, var(--_fill)) 100%);
  border: 1.5px solid color-mix(in oklch, var(--_fill) 85%, var(--surface-page));
  box-shadow:
    /* Inner rim highlight */
    inset 0 1px 0 color-mix(in oklch, #fff 90%, transparent),
    inset 0 -1px 0 color-mix(in oklch, var(--_fill) 18%, transparent),
    /* Contact shadow */
    0 1px 2px color-mix(in oklch, var(--surface-page) 45%, transparent),
    /* Ambient drop */
    0 4px 12px color-mix(in oklch, var(--surface-page) 28%, transparent),
    /* Fill-coloured glow */
    0 2px 8px color-mix(in oklch, var(--_fill) 18%, transparent);
  margin-top: calc((var(--slider-thumb-d) - var(--slider-track-h)) / -2 - 0.5px);
  cursor: grab;
  transition: transform 0.25s cubic-bezier(0.34,1.56,0.64,1),
              box-shadow 0.25s ease, border-color 0.25s;
}
/* Thumb — Firefox */
.slider-range::-moz-range-thumb {
  width: var(--slider-thumb-d); height: var(--slider-thumb-d);
  border-radius: 50%;
  background:
    radial-gradient(circle at 50% 28%,
      #fff 0%,
      color-mix(in oklch, #fff 95%, var(--_fill)) 55%,
      color-mix(in oklch, #fff 86%, var(--_fill)) 100%);
  border: 1.5px solid color-mix(in oklch, var(--_fill) 85%, var(--surface-page));
  box-shadow:
    inset 0 1px 0 color-mix(in oklch, #fff 90%, transparent),
    inset 0 -1px 0 color-mix(in oklch, var(--_fill) 18%, transparent),
    0 1px 2px color-mix(in oklch, var(--surface-page) 45%, transparent),
    0 4px 12px color-mix(in oklch, var(--surface-page) 28%, transparent),
    0 2px 8px color-mix(in oklch, var(--_fill) 18%, transparent);
  cursor: grab;
  transition: transform 0.25s cubic-bezier(0.34,1.56,0.64,1),
              box-shadow 0.25s ease, border-color 0.25s;
}

/* Hover + focus — grow the thumb, deepen the glow, add a branded ring */
.slider-range:hover::-webkit-slider-thumb,
.slider-range:focus-visible::-webkit-slider-thumb {
  transform: scale(1.14);
  border-color: var(--_fill);
  box-shadow:
    inset 0 1px 0 color-mix(in oklch, #fff 90%, transparent),
    inset 0 -1px 0 color-mix(in oklch, var(--_fill) 22%, transparent),
    0 2px 4px color-mix(in oklch, var(--surface-page) 50%, transparent),
    0 8px 20px color-mix(in oklch, var(--surface-page) 32%, transparent),
    0 4px 14px color-mix(in oklch, var(--_fill) 28%, transparent),
    0 0 0 5px color-mix(in oklch, var(--_fill) 16%, transparent);
}
.slider-range:hover::-moz-range-thumb,
.slider-range:focus-visible::-moz-range-thumb {
  transform: scale(1.14);
  border-color: var(--_fill);
  box-shadow:
    inset 0 1px 0 color-mix(in oklch, #fff 90%, transparent),
    inset 0 -1px 0 color-mix(in oklch, var(--_fill) 22%, transparent),
    0 2px 4px color-mix(in oklch, var(--surface-page) 50%, transparent),
    0 8px 20px color-mix(in oklch, var(--surface-page) 32%, transparent),
    0 4px 14px color-mix(in oklch, var(--_fill) 28%, transparent),
    0 0 0 5px color-mix(in oklch, var(--_fill) 16%, transparent);
}

/* Hover — also subtly brighten the track */
.slider-range:hover::-webkit-slider-runnable-track {
  box-shadow:
    inset 0 1px 1px color-mix(in oklch, var(--surface-page) 60%, transparent),
    inset 0 -1px 0 color-mix(in oklch, var(--text-primary) 8%, transparent),
    0 0 0 1px color-mix(in oklch, var(--_fill) 8%, transparent);
}

/* Active (dragging) — compress slightly and intensify fill glow */
.slider-range:active::-webkit-slider-thumb {
  cursor: grabbing; transform: scale(1.06);
  box-shadow:
    inset 0 1px 0 color-mix(in oklch, #fff 85%, transparent),
    inset 0 -2px 0 color-mix(in oklch, var(--_fill) 28%, transparent),
    0 1px 2px color-mix(in oklch, var(--surface-page) 55%, transparent),
    0 4px 10px color-mix(in oklch, var(--surface-page) 35%, transparent),
    0 0 0 8px color-mix(in oklch, var(--_fill) 10%, transparent);
}
.slider-range:active::-moz-range-thumb {
  cursor: grabbing; transform: scale(1.06);
  box-shadow:
    inset 0 1px 0 color-mix(in oklch, #fff 85%, transparent),
    inset 0 -2px 0 color-mix(in oklch, var(--_fill) 28%, transparent),
    0 1px 2px color-mix(in oklch, var(--surface-page) 55%, transparent),
    0 4px 10px color-mix(in oklch, var(--surface-page) 35%, transparent),
    0 0 0 8px color-mix(in oklch, var(--_fill) 10%, transparent);
}

/* ── Size variants ────────────────────────────────────────────── */
.slider--sm .slider-range { height: var(--space-20); --slider-track-h: var(--space-4); --slider-thumb-d: var(--space-16); }
.slider--lg .slider-range { height: var(--space-32); --slider-track-h: var(--space-8); --slider-thumb-d: var(--space-24); }

/* ── Accent variants — remap the fill token only ───────────────── */
.slider--accent2 .slider-range { --_slider-fill: var(--accent-secondary); }
.slider--accent3 .slider-range { --_slider-fill: var(--accent-tertiary); }

/* ── Discrete / tick marks variant ──────────────────────────────
   .slider-tick positioning anchors tick CENTERS to thumb-track
   positions. flex `justify-content: space-between` distributes BOX
   GAPS evenly — which mis-aligns tick centers when ticks have
   different content widths ("0" vs "12"). Absolute positioning via
   per-tick `--tick-i` (a unitless 0..1 fraction of the thumb-track)
   gives exact centers regardless of label width.
   --slider-thumb-d is hoisted to .slider--discrete so both .slider-range
   (thumb size) and .slider-ticks (edge inset) read the same source. */
.slider--discrete {
  flex-direction: column; align-items: stretch; gap: 0;
  --slider-thumb-d: var(--space-20);
}
.slider--discrete.slider--sm { --slider-thumb-d: var(--space-16); }
.slider--discrete.slider--lg { --slider-thumb-d: var(--space-24); }
.slider--discrete .slider-row { display: flex; align-items: center; gap: var(--cluster-md); }
.slider-ticks {
  position: relative;
  min-height: var(--space-24);
  font-family: var(--font-sans);
  font-size: var(--text-micro); font-weight: var(--fw-medium);
  letter-spacing: var(--tracking-loose); color: var(--text-faint);
  text-transform: uppercase;
}
.slider-tick {
  position: absolute; top: 0;
  /* Center the tick at: half-thumb-inset + (tick-i × thumb-track width).
     thumb-track width = container width − thumb-d. */
  left: calc(var(--slider-thumb-d) / 2 + var(--tick-i, 0) * (100% - var(--slider-thumb-d)));
  transform: translateX(-50%);
  padding-top: var(--space-4); text-align: center;
  white-space: nowrap;
}
.slider-tick::before {
  content: ''; position: absolute; top: 0; left: 50%; transform: translateX(-50%);
  width: 1px; height: var(--space-4);
  background: color-mix(in oklch, var(--text-primary) 20%, transparent);
}

/* ── Light mode — darker empty-track portion + neutral inset ────── */
[data-theme="light"] .slider-range::-webkit-slider-runnable-track {
  background:
    linear-gradient(to right,
      color-mix(in oklch, var(--_fill) 100%, var(--accent-secondary) 0%) 0%,
      color-mix(in oklch, var(--_fill) 75%, var(--accent-secondary) 25%) calc(var(--slider-pct) * 0.5),
      var(--_fill) var(--slider-pct),
      color-mix(in oklch, var(--text-primary) 12%, transparent) var(--slider-pct) 100%);
  box-shadow:
    inset 0 1px 1px color-mix(in oklch, var(--text-primary) 10%, transparent),
    inset 0 -1px 0 color-mix(in oklch, var(--surface-page) 5%, transparent);
}
[data-theme="light"] .slider-range::-moz-range-track {
  background: color-mix(in oklch, var(--text-primary) 12%, transparent);
  box-shadow: inset 0 1px 1px color-mix(in oklch, var(--text-primary) 10%, transparent);
}
[data-theme="light"] .slider-tick::before {
  background: color-mix(in oklch, var(--text-primary) 22%, transparent);
}


/* ══════════════════════════════════════════
   6. SECTION PRIMITIVES
   ══════════════════════════════════════════ */
.section { padding: var(--pad-section-y-lg) 0; position: relative; z-index: 2; }
.section-inner { max-width: 1200px; margin: 0 auto; padding: 0 var(--pad-section-x); }

/* First-child section in a sidebar-flanked DS layout — replace the
   standard 240 px top padding with a breathing-room value that matches
   the sidebar's own top padding (so both columns sit on the same
   horizontal line below the hero seam). The standard 240 px section
   padding belongs BETWEEN sections, not above the very first one
   (where the navbar + hero seam already provide structural break). */
.ds-doc-content main > section.section:first-child,
.ds-doc-content > section.section:first-child {
  padding-top: var(--space-64);
}

/* ── .container — page-level content rail ────────────────────────────────
   The shared content container that every section's inner content sits
   inside. Used across the marketing site, the design-system catalog, and
   downstream documents. Originally lived in `lifi-design-system.css`; that
   file was unloaded in Apr 2026 v4 (FOUC fix), so the rule moved here.

   Sizing — `1280px` max-width with `80px` horizontal padding. On a 1440px
   viewport this leaves an 80px gutter on each side and caps the content
   rail at 1120px (1280 − 160). At ≤1280px viewports the padding becomes
   the only spacer and the layout collapses to viewport-width minus 160.

   On narrow viewports the 80px padding eats too much screen, so a tighter
   value kicks in below the lg breakpoint (matches the rest of the site's
   responsive rhythm).

   Sister primitive — `.section-inner` (above) which is for tighter rails
   (1200px / 48px) inside individual sections. Pick `.container` for the
   page-level rail; pick `.section-inner` for a narrower in-section rail.
   ──────────────────────────────────────────────────────────────────── */
.container {
  max-width: 1280px;
  margin: 0 auto;
  padding: 0 var(--space-80);   /* 80px */
}
@media (max-width: 768px) {
  .container { padding: 0 var(--space-24); }   /* 24px on phones */
}

/* ── Eyebrow — shared base ──────────────────────────────────────────────── */
/* Sits ABOVE a section-title. The 12-px gap below is part of the
   documentation rhythm — eyebrow→title is the tightest pair in the
   page (they read as one unit), title→desc is wider (20 px), and
   desc→first-card is widest (64 px). See `design.md §13 →
   Documentation rhythm` for the full table. */
.eyebrow-code, .eyebrow-word {
  display: inline-block;
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--accent-secondary);
  margin-bottom: var(--space-12);
}
[data-theme="light"] .eyebrow-code,
[data-theme="light"] .eyebrow-word { color: var(--accent-primary); }

/* Neutral tone modifier — drops the eyebrow to --text-muted while
   keeping the uppercase, weight, and tracking. Use on footer / metadata
   labels where the accent colour would over-emphasize the secondary
   information. Promoted from the .identity-tile-foot scoped override
   (May 2026) — applies to both .eyebrow-code and .eyebrow-word. */
.eyebrow-code--muted,
.eyebrow-word--muted { color: var(--text-muted); }
[data-theme="light"] .eyebrow-code--muted,
[data-theme="light"] .eyebrow-word--muted { color: var(--text-muted); }

/* ── Eyebrow large — section label ──────────────────────────────────────── */
/* Stronger hierarchy for above-the-fold labels and section-defining eyebrows.
   Use this instead of .eyebrow-code/.eyebrow-word when the label needs to
   hold its own against a big metric or headline. */
.eyebrow-lg {
  display: inline-block;
  font-size: var(--text-body);          /* 16px — large label */
  line-height: var(--lh-body);          /* 24px → --space-24 */
  letter-spacing: var(--tracking-wider);
  font-weight: var(--fw-extrabold);
  text-transform: uppercase;
  color: var(--accent-secondary);
  margin-bottom: var(--space-16);
}
[data-theme="light"] .eyebrow-lg { color: var(--accent-primary); }

/* ── Metric display — big stat numbers ─────────────────────────────────── */
/* For hero metric rows and stat callouts. Gradient fill via --grad-brand-rich
   so numbers feel like "liquid" branded metrics. Clamps between H2 (32px)
   and Display (56px) — both on the spacing grid. */
.metric-display {
  font-size: clamp(var(--text-h2), 5vw, var(--text-display));
  line-height: 1;                      /* ratio 1.0 — tight numeric display (height = font-size at every clamp step) */
  letter-spacing: var(--tracking-tight);
  font-weight: var(--fw-extrabold);
  background: linear-gradient(var(--accent-primary), var(--accent-primary)); /* TEMP solid — gradient text paused */
  -webkit-background-clip: text; -webkit-text-fill-color: transparent;
  background-clip: text;
  display: inline-block;
}
.metric-label {
  font-size: var(--text-caption);       /* 12px */
  line-height: var(--lh-caption);       /* 16px → --space-16 */
  letter-spacing: var(--tracking-wide); /* 0.1em */
  font-weight: var(--fw-semibold);
  text-transform: uppercase;
  color: var(--text-muted);
  margin-top: var(--space-8);
  display: block;
}

/* ── Eyebrow numbered — number · label pattern ─────────────────────
   Family member for chapter / section / slide / card numbering.
   Sits above a heading the same way .eyebrow-code / .eyebrow-word
   do, but composes a numeric prefix + visible separator + label:

     <div class="eyebrow-numbered">
       <span class="eyebrow-numbered__num">01</span>
       <span class="eyebrow-numbered__label">Model Tiers</span>
     </div>

   Both segments resolve their color through CSS custom properties
   (--eyebrow-num-tone, --eyebrow-label-tone) — same `--*-tone`
   indirection that .tile, .accent-card, and .brand-card use — so
   per-instance accent overrides happen at the token layer instead
   of via a combinatorial set of modifier classes. Default both
   muted (matches the chapter-eyebrow look on ai.html); the single
   .eyebrow-numbered--accent modifier paints the number in
   --accent-primary while the label stays muted (the common
   "number pops, label supports" pattern). For unusual color
   pairings, designers override the tokens inline:
     style="--eyebrow-num-tone: var(--accent-secondary);
            --eyebrow-label-tone: var(--text-primary);"

   The " · " separator is CSS-rendered via ::before on the label
   span — same trick as .header-eyebrow--breadcrumb's " / ". The
   separator inherits the label's color so the dot rides with the
   label tone, not the number tone (mirrors the breadcrumb where
   the slashes stay in the muted parent color even when the leaf
   carries an accent). */
.eyebrow-numbered {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  line-height: var(--lh-caption);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  margin-bottom: var(--space-12);
  --eyebrow-num-tone:   var(--text-faint);
  --eyebrow-label-tone: var(--text-faint);
}
.eyebrow-numbered__num   { color: var(--eyebrow-num-tone); margin-right: 4px; }
.eyebrow-numbered__label { color: var(--eyebrow-label-tone); }
.eyebrow-numbered__label::before { content: "· "; }

/* Number painted in accent-primary, label stays muted.
   The common "number is the eye-catcher, label is supportive" pattern. */
.eyebrow-numbered--accent { --eyebrow-num-tone: var(--accent-primary); }

/* Tighter margin — pairs with secondary headlines below dense lists,
   or as a quiet kicker above a sub-heading inside a section. */
.eyebrow-numbered--tight { margin-bottom: var(--space-4); }

/* eyebrow-code — developer/technical sections, has // prefix.
   Uses --text-caption (12px) so it lands on the spacing grid. */
.eyebrow-code {
  font-size: var(--text-caption);
  line-height: var(--lh-caption);
  font-family: var(--font-mono);
  opacity: 0.85;
}
.eyebrow-code::before { content: '// '; opacity: 0.5; }

/* eyebrow-word — general section titles, hero eyebrows */
.eyebrow-word {
  font-size: var(--text-caption);
  line-height: var(--lh-caption);
}

/* .chip-pill (May 2026) and .scope-chip (May 2026) removed —
   merged into the unified .chip family. Card-tier labels now use
   .chip + tone (with .chip--outline for the bordered variant);
   proposal-scale scope pills use .chip.chip--xl with an optional
   .chip-icon slot for the leading / trailing glyph. See .chip block in this file (search "Chip — universal
   pill primitive") for the full surface. */

/* Badge pill inside cards gets bottom spacing and hugs its label —
   .accent-card is a flex column, which would otherwise stretch the
   chip to the card's full width and defeat .chip's inline-flex. */
.accent-card .chip { align-self: flex-start; margin-bottom: 24px; }

/* ── Color Chip — inline token swatch for documentation ──────────────── */
.color-chip {
  display: inline-flex; align-items: center; gap: var(--space-8);
  font-family: var(--font-mono, 'Geist Mono', monospace);
  font-size: var(--text-caption); font-weight: var(--fw-medium);
  line-height: var(--lh-caption);
  padding: var(--space-4) var(--space-12) var(--space-4) var(--space-4);
  border-radius: var(--r-full);
  background: color-mix(in oklch, var(--text-primary) 8%, transparent);
  color: var(--text-secondary);
  white-space: nowrap;
}
/* Swatch-only — no label, collapses to a tinted circle */
.color-chip--swatch-only {
  padding: var(--space-4);
  gap: 0;
}
/* Inner dot (.color-chip-swatch) migrated to .swatch--xs in May 2026
   unification. The wrapper modifiers (.color-chip / --custom / --swatch-only
   / --on-dark / --on-light) remain — they style the OUTER pill. Color
   flow into the inner .swatch--xs is handled by the CSS bridge rules
   in the .swatch unification block (search ".color-chip .swatch--xs"). */

/* Canonical chip — any color via --chip-color CSS variable.
   Use this for every color reference in documentation. Set the chip
   color inline: style="--chip-color:var(--accent-primary);" (or any
   CSS color value — hex, oklch, currentColor, …). */
.color-chip--custom { background: color-mix(in oklch, var(--chip-color) 10%, transparent); color: var(--chip-color); }

/* ── Alpha-token preview chips ────────────────────────────────
   Use when a chip shows a low-alpha token value (e.g. oklch(100% 0 0 / 0.04))
   against a simulated theme backdrop. The chip uses the backdrop
   color + a high-contrast label color so the text is always legible
   regardless of the page's current theme. The inner .swatch--xs gets
   a 1px border (light or dark, depending on backdrop) so very low-alpha
   values still read as a circle — see the bridge rules
   `.color-chip--on-dark .swatch--xs` / `--on-light .swatch--xs` in the
   .swatch unification block.
     .color-chip--on-dark  → dark backdrop, white label
     .color-chip--on-light → light backdrop, black label
   Both accept the alpha color via --chip-color (set inline). */
.color-chip--on-dark {
  background: oklch(12% 0.015 264);
  color: rgba(255,255,255,0.88);
}
.color-chip--on-light {
  background: oklch(96% 0.005 255);
  color: rgba(0,0,0,0.82);
}
/* Keep the same appearance regardless of the active site theme —
   these chips are demonstrating a specific theme, not following it. */
[data-theme="light"] .color-chip--on-dark {
  background: oklch(12% 0.015 264);
  color: rgba(255,255,255,0.88);
}
[data-theme="light"] .color-chip--on-light {
  background: oklch(96% 0.005 255);
  color: rgba(0,0,0,0.82);
}

/* Light-mode flips */
[data-theme="light"] .color-chip { background: color-mix(in oklch, var(--surface-page) 6%, transparent); color: var(--text-secondary); }
/* Legacy named-variant light overrides removed — .color-chip--custom
   handles all colors via --chip-color, which itself flips per theme. */
[data-theme="light"] .color-chip--custom { background: color-mix(in oklch, var(--chip-color) 10%, transparent); }

/* ════════════════════════════════════════════════════════════════
   .swatch — color swatch card (3 sizes)
   ════════════════════════════════════════════════════════════════
   The canonical color-display primitive across the design system.
   Drives every brand-color swatch in the catalog (3 identity accents,
   4 semantic colors, 12 spectrum hues) and is reusable for marketing
   brand-guideline pages, presentation decks, and PDF books.

   Why a primitive — earlier iterations had inline-styled swatches in
   each section, with each consumer inventing its own dimensions, font
   sizes, paddings, on-tile label positioning. The result: visually
   inconsistent, hard to bulk-update, and the spectrum 12-grid was
   visually competing with the 3-accent grid above it. The `.swatch`
   primitive is the single source of truth so a designer who wants to
   reuse the swatch shape for a brand-guidelines PDF gets the exact
   same thing the catalog ships.

   Anatomy
     <figure class="swatch [--sm | --lg]"
             style="--swatch-color: var(--accent-primary);">
       <div class="swatch-tile"></div>      ← color fill
       <figcaption class="swatch-meta">
         <div class="swatch-name">Coral</div>
         <code class="swatch-token">--spectral-1</code>
         <code class="swatch-value">oklch(70% 0.18 20)</code>
         <span class="swatch-role">(--lg only) Primary brand</span>
       </figcaption>
     </figure>

   Input
     --swatch-color  the color to display. Pass a token (var(--accent-primary))
                     or a literal (oklch / hex / hsl). Required.

   Sizes — each shows a different metadata depth so the swatch's
   visual weight matches its information density.

     .swatch--sm   80 px tile · name + token             · spectrum 12-up grids
     .swatch       120 px tile · name + token + value    · semantic 4-up grids (default)
     .swatch--lg   200 px tile · name (on-tile) + role + token + value + hex
                                                          · brand identity 3-up grids

   On-tile labels — only --lg shows a label inside the tile (visible on
   the color itself). The text color is auto-computed from the swatch's
   own OKLCH lightness — no manual on-color picking needed.

   Reuse outside the catalog — the primitive only needs --swatch-color
   to render. Marketing brand-guideline PDFs that paste the markup get
   the same look without any token-system wiring.
*/

/* swatch primitive — see comment above.

   May 2026 size-ladder renumber (Phase 2): the named size tiers
   shifted UP one slot to make room for two new tiers below for the
   inline-dot and composer-row use cases that previously lived under
   .color-chip-swatch / .pc-swatch / .ds-doc-preset. The named
   ladder is now:

     .swatch--xs   16 px   inline dot (leaf form)        [Phase 1]
     .swatch--sm   32 px   composer dot, preset button   [Phase 2]
     .swatch       80 px   compact card (was --sm)
     .swatch--lg  120 px   catalog reference (was default)
     .swatch--xl  200 px   hero brand identity (was --lg)

   Pre-renumber consumers using bare .swatch / .swatch--sm / .swatch
   --lg were migrated in the same edit. .swatch--xs and .swatch--sm
   are leaf-form (the element IS the colored cell — no .swatch-tile
   / .swatch-meta children); the default and larger tiers are
   card-form with the metadata block underneath. */
.swatch {
  /* Required input — the color to display. Override at the instance:
     style="--swatch-color: var(--accent-primary);"  */
  --swatch-color: var(--text-primary);

  /* Variant-aware state token — every state rule on .swatch--interactive
     consumes --swatch-tone, never bakes --accent-primary. Per CLAUDE.md
     "Variant-aware states" rule. */
  --swatch-tone: var(--swatch-color);

  /* Auto-computed legible label color for on-tile text.
     Same OKLCH-clamp trick used by the brand --on-* tokens:
     reads the source color's lightness and returns black-on-light or
     white-on-dark with a smooth handover at L≈0.62. */
  --swatch-on-color: oklch(from var(--swatch-color) clamp(0.08, (0.62 - l) * 100, 0.96) 0 0);

  /* Size knobs — defaults match the new DEFAULT tier (80 px, was old
     --sm). Override via --lg / --xl modifiers for the larger card tiers.
     The 16 px (--xs) and 32 px (--sm) leaf-form tiers ignore these knobs
     and set their own width/height directly. */
  --swatch-tile-h:    var(--space-80);
  --swatch-min:       180px;
  --swatch-radius:    var(--r-md);
  --swatch-pad:       var(--space-12);
  --swatch-meta-gap:  var(--space-4);
  /* --swatch-edge — the content-aware hairline — is defined on :root so
     the standalone .swatch-ramp__step (not a .swatch) resolves it too. */

  display: flex;
  flex-direction: column;
  margin: 0;
  min-width: var(--swatch-min);
  border-radius: var(--swatch-radius);
  background: var(--surface-card);
  border: 1px solid var(--border-subtle);
  overflow: hidden;
  transition: border-color 0.18s ease, transform 0.18s ease, box-shadow 0.18s ease;
}
.swatch:hover {
  border-color: color-mix(in oklch, var(--swatch-tone) 40%, transparent);
  transform: translateY(-1px);
  box-shadow: 0 6px 20px -4px color-mix(in oklch, var(--swatch-tone) 18%, transparent);
}

.swatch-tile {
  position: relative;
  height: var(--swatch-tile-h);
  background: var(--swatch-color);
  /* Content-aware edge — see --swatch-edge on .swatch. Keeps the tile's
     edge readable against a same-lightness surface in BOTH modes without
     a fixed white border. */
  box-shadow: inset 0 0 0 1px var(--swatch-edge);
}

.swatch-meta {
  display: flex;
  flex-direction: column;
  gap: var(--swatch-meta-gap);
  padding: var(--swatch-pad);
  /* Subtle hairline separator between the tile and the metadata block —
     uses the same border token as the outer card so the rhythm reads
     as one chrome system. The tile's own inset shadow already implies
     this edge; the line just clarifies it on hover when the card lifts. */
  border-top: 1px solid var(--border-subtle);
}

/* ── Metadata typography — DEFAULT tier (80 px tile, was old --sm) ──
   The default tier serves compact reference grids (semantic 4-up,
   spectrum 12-up). Smaller typography keeps each card scan-readable
   at 180 px column width without wrap. The .swatch--lg and --xl
   blocks below upsize these for their larger tiles. */
.swatch-name {
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: var(--fw-bold);
  color: var(--text-primary);
  line-height: 1.2;
  letter-spacing: var(--tracking-tight);
}

.swatch-token {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  font-weight: var(--fw-semibold);
  color: var(--text-primary);
  letter-spacing: 0;
}

.swatch-value,
.swatch-hex {
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  font-weight: var(--fw-medium);
  color: var(--text-muted);
  letter-spacing: 0;
}

/* The role chip is reserved for .swatch--xl (hero); default and --lg
   tiers don't render it. Selectors target .swatch--xl below. */
.swatch-role {
  display: inline-flex;
  align-items: center;
  align-self: flex-start;
  margin-bottom: var(--space-4);
  padding: var(--space-4) var(--space-8);
  border-radius: var(--r-sm);
  background: color-mix(in oklch, var(--swatch-tone) 14%, transparent);
  color: var(--swatch-tone);
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}
.swatch:not(.swatch--xl):not(.swatch--label-inside) .swatch-role { display: none; }

/* On-tile label — only used by .swatch--xl (hero), but defined here
   so any size can opt in if the consumer wants the loud-and-proud
   treatment. Floats over the bottom-left of the tile in the auto-
   computed legible text color. */
.swatch-tile-label {
  position: absolute;
  left: var(--space-16);
  bottom: var(--space-12);
  font-family: var(--font-sans);
  font-size: var(--text-h4);
  font-weight: var(--fw-extrabold);
  letter-spacing: var(--tracking-tight);
  color: var(--swatch-on-color);
  line-height: 1;
}

/* ── Size: --sm (32 px leaf-form, composer dot / preset button) ──
   Leaf-form: the swatch element IS the colored cell (no .swatch-tile
   / .swatch-meta children). Sits in .swatch-row containers next to
   sibling label + hex, or in flat preset rows. Pair with
   .swatch--circle for the canonical composer dot, or .swatch--square
   for a sharp-cornered preset. Replaces .pc-swatch (24 px) and
   .ds-doc-preset (32 px) in one tier. */
.swatch--sm {
  display: inline-block;
  width: var(--space-32);
  height: var(--space-32);
  min-width: 0;
  margin: 0;
  padding: 0;
  background: var(--swatch-color);
  border: 1px solid var(--alpha-border-strong);
  border-radius: var(--r-sm);
  vertical-align: middle;
  flex-shrink: 0;
  overflow: hidden;
  box-shadow: none;
  transform: none;
}
.swatch--sm:hover { transform: none; box-shadow: none; }
.swatch--sm > .swatch-tile,
.swatch--sm > .swatch-meta { display: none; }

/* ── Size: --lg (120 px tile, catalog reference card — was old default) ──
   The catalog default. For semantic-pair indexes, spectrum 12-up,
   and most reference grids. Upsizes typography over the default. */
.swatch--lg {
  --swatch-tile-h: var(--space-120);
  --swatch-min:    220px;
  --swatch-radius: var(--r-lg);
  --swatch-pad:    var(--space-20);
  --swatch-meta-gap: var(--space-8);
}
.swatch--lg .swatch-name  { font-size: var(--text-body); }
.swatch--lg .swatch-token { font-size: var(--text-caption); }
.swatch--lg .swatch-value { font-size: var(--text-caption); }
.swatch--lg .swatch-hex   { font-size: var(--text-caption); }

/* ── Size: --xl (200 px tile, hero brand identity — was old --lg) ──
   Hero. For brand-identity rows and marketing brand-guideline
   spreads. Tile carries an on-color name label; metadata block below
   adds the role chip + hex line in addition to token + OKLCH. */
.swatch--xl {
  --swatch-tile-h: 200px;
  --swatch-min:    280px;
  --swatch-radius: var(--r-xl);
  --swatch-pad:    var(--space-24);
  --swatch-meta-gap: var(--space-8);
}
.swatch--xl .swatch-name {
  font-size: var(--text-h3);
}
.swatch--xl .swatch-token,
.swatch--xl .swatch-value,
.swatch--xl .swatch-hex { font-size: var(--text-sm); }
.swatch--xl .swatch-role { font-size: 11px; padding: var(--space-4) var(--space-12); }

/* ── Variant: --state (component state-colour reference for Colors tabs) ──
   Horizontal row layout: a fixed 96 px-wide tile column on the left,
   metadata block fills the rest with generous internal spacing so the
   rationale sentence has room to breathe. Multiple --state swatches
   stack vertically inside .swatch-grid via `grid-column: 1 / -1` —
   each one spans the full container width.

   Three extra slots beyond the default tier:
     .swatch-flag       — source classification pill (inherited / override / convention)
     .swatch-selector   — the scoped CSS selector that consumes the colour
     .swatch-rationale  — one-sentence reason for the intensity / scope choice
   Use exclusively inside a Colors tab to document a component's
   interaction states (hover, selected, focus, active, disabled, pinned). */
.swatch--state {
  flex-direction: row;
  align-items: stretch;
  grid-column: 1 / -1;
  --swatch-min: 0;
  --swatch-pad: var(--space-20);
  --swatch-meta-gap: var(--space-12);
  /* Strip the .swatch base card chrome — this variant is a static
     documentation row, not an interactive card. No hover lift, no
     border-tint, no shadow. Keeps the subtle outer border + bg for
     visual separation between adjacent rows. */
  transition: none;
}
.swatch--state:hover {
  transform: none;
  border-color: var(--border-subtle);
  box-shadow: none;
}

/* Documentation chips inside .swatch--state are static labels, not
   interactive. The bare code:hover rule (background-color + color
   shift with a 0.15s transition) creates a perceived ripple of
   colour changes as the cursor moves across the row — reads as
   visual instability even though nothing geometrically shifts.
   Override: match at-rest values exactly so :hover is a visual no-op,
   and drop the transition so any cascade reordering can't cause
   a frame of animation. */
.swatch--state code,
.swatch--state .code-chip {
  transition: none;
}
.swatch--state code:hover,
.swatch--state .code-chip:hover {
  background: color-mix(in oklch, currentColor 7%, transparent);
  color: var(--text-primary);
}
.swatch--state .swatch-selector:hover {
  color: var(--text-muted);
}
.swatch--state .swatch-tile {
  flex: 0 0 var(--space-96);
  height: auto;
  min-height: var(--space-96);
}
.swatch--state .swatch-meta {
  flex: 1 1 auto;
  border-top: none;
  border-left: 1px solid var(--border-subtle);
}

/* Source flag — small uppercase pill at the top of the meta block.
   Hidden by default; only .swatch--state renders it. Three tone
   modifiers communicate where the colour comes from at a glance. */
.swatch-flag {
  display: none;
  align-items: center;
  align-self: flex-start;
  padding: var(--space-2) var(--space-8);
  border-radius: var(--r-sm);
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  background: color-mix(in oklch, var(--text-primary) 7%, transparent);
  color: var(--text-muted);
}
.swatch--state .swatch-flag { display: inline-flex; }

/* Flag tone modifiers — source classification:
     inherited  — colour comes from the base primitive; no scoped override.
     override   — deliberate scoped override of a system default.
     convention — locked widget brand convention; do not import elsewhere. */
.swatch-flag--inherited  { background: color-mix(in oklch, var(--text-primary) 7%, transparent);  color: var(--text-muted); }
.swatch-flag--override   { background: color-mix(in oklch, var(--lifi-ink) 14%, transparent);    color: var(--lifi-ink); }
.swatch-flag--convention { background: color-mix(in oklch, var(--lifi-sapphire) 14%, transparent);    color: var(--accent-primary); }

/* Selector slot — mono, dimmer than .swatch-token. Long selectors wrap
   rather than overflow the card. Sits one row below the recipe to keep
   the WHAT (token) vs WHERE (selector) separation visible at a glance.
   `white-space: normal` overrides the bare `code` rule's `nowrap` so
   long selectors (e.g., `.ui-token-list .list-item.is-pinned [data-pin]`)
   can wrap to two lines instead of forcing a horizontal scroll. */
.swatch-selector {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  font-weight: var(--fw-medium);
  color: var(--text-muted);
  letter-spacing: 0;
  white-space: normal;
  word-break: break-word;
  overflow-wrap: anywhere;
}

/* Chips inside .swatch--state hug their content instead of stretching
   to the meta column width. The parent (.swatch-meta) is a flex column
   with default `align-items: stretch`, which would otherwise blow the
   chips out to full width — `align-self: flex-start` opts each chip out
   of the cross-axis stretch so the background hugs the text. */
.swatch--state .swatch-token,
.swatch--state .swatch-selector {
  align-self: flex-start;
  max-width: 100%;
}

/* Rationale sentence — final slot. One short sentence, sans, muted,
   readable line-height. Optional — omit when the recipe alone is
   self-explanatory. */
.swatch-rationale {
  margin-top: var(--space-2);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: var(--fw-regular);
  color: var(--text-muted);
  line-height: 1.4;
}

/* ════════════════════════════════════════════════════════════════
   TEMPLATE: .swatch--label-inside — name + hex ON the tile, no card
   ════════════════════════════════════════════════════════════════
   The brand-identity flagship (Brand Book "Core colours", node 524:23).
   The whole swatch IS the colored tile; name + hex (+ an optional role
   chip) render INSIDE in the auto-contrast --swatch-on-color, no meta
   block below. Reuses the shared .swatch-name / -hex / -role sub-
   elements, repositioned by the modifier — same typography as the
   caption-below card so the family reads as one system.

   Markup:
     <figure class="swatch swatch--label-inside [--lg|--xl]"
             style="--swatch-color: var(--lifi-sapphire);">
       <span class="swatch-role">Action</span>          ← optional
       <figcaption class="swatch-meta">
         <span class="swatch-name">Sapphire</span>
         <code class="swatch-hex">#405CCF</code>        ← + optional --swatch-token / metadata
       </figcaption>
     </figure>
*/
.swatch--label-inside {
  position: relative;
  --swatch-tile-h: 200px;             /* large by default — this is a large-swatch template */
  height: var(--swatch-tile-h);
  min-width: var(--swatch-min);
  background: var(--swatch-color);
  border: 0;
  border-radius: var(--swatch-radius);
  box-shadow: inset 0 0 0 1px var(--swatch-edge);
  padding: var(--space-16);
  justify-content: flex-end;          /* flex column → label block sits at the bottom */
  overflow: hidden;
  color: var(--swatch-on-color);
}
/* Reference tile — no hover lift / border-tint (it's a static display,
   not an interactive card). Re-assert the edge so :hover is a no-op. */
.swatch--label-inside:hover {
  transform: none;
  box-shadow: inset 0 0 0 1px var(--swatch-edge);
}
.swatch--label-inside > .swatch-tile { display: none; }   /* card-form tile unused here */
.swatch--label-inside > .swatch-meta {
  position: static;
  padding: 0;
  border-top: 0;
  gap: var(--space-2);
}
.swatch--label-inside .swatch-name { color: var(--swatch-on-color); font-size: var(--text-body); }
.swatch--label-inside .swatch-token,
.swatch--label-inside .swatch-value,
.swatch--label-inside .swatch-hex {
  color: var(--swatch-on-color);
  opacity: 0.82;
  font-size: var(--text-caption);
}
/* Role chip → outlined on-color pill, top-left (the metadata slot:
   primary / secondary / surface / tonal #, etc.). */
.swatch--label-inside .swatch-role {
  position: absolute;
  top: var(--space-12);
  left: var(--space-16);
  margin: 0;
  background: transparent;
  color: var(--swatch-on-color);
  box-shadow: inset 0 0 0 1px color-mix(in oklch, var(--swatch-on-color) 30%, transparent);
}
/* --lg / --xl scale the tile + typography (same knobs as the card form). */
.swatch--label-inside.swatch--lg .swatch-name { font-size: var(--text-body-lg); }
.swatch--label-inside.swatch--xl { --swatch-tile-h: 240px; }
.swatch--label-inside.swatch--xl .swatch-name { font-size: var(--text-h3); }

/* ════════════════════════════════════════════════════════════════
   TEMPLATE: .swatch-ramp — a tonal scale (header + N steps)
   ════════════════════════════════════════════════════════════════
   Brand Book "Tonal scales" (node 651:2). A labeled scale header
   (anchor name + hex) over a row of N unlabeled steps. The --surfaces
   variant adds a hex caption below each step (the white→black row).
   Steps read --swatch-color inline; the shared edge keeps pale steps
   visible on white and dark steps visible on ink.

   Markup:
     <div class="swatch-ramp">
       <div class="swatch-ramp__head">
         <span class="swatch-name">Ink</span><code class="swatch-hex">#0C0E2E</code>
       </div>
       <div class="swatch-ramp__steps">
         <span class="swatch-ramp__step" style="--swatch-color: var(--ink-1);"></span> … ×9
       </div>
     </div>
*/
.swatch-ramp { display: flex; flex-direction: column; gap: var(--space-8); }
.swatch-ramp__head { display: flex; align-items: baseline; gap: var(--space-8); }
.swatch-ramp__head .swatch-name { font-size: var(--text-sm); }
.swatch-ramp__head .swatch-hex  { color: var(--text-secondary); }
/* Fixed column grid (not flex) so a ramp with FEWER steps (the surface
   scale = 7) sits at the SAME step size as the 9-step tonal ramps and
   left-anchors to the same columns — rather than stretching wider.
   --swatch-ramp-cols defaults to 9 (the tonal-ramp length); override for
   a longer max scale. */
.swatch-ramp__steps {
  --swatch-ramp-cols: 9;
  display: grid;
  grid-template-columns: repeat(var(--swatch-ramp-cols), 1fr);
  gap: var(--space-8);
}
.swatch-ramp__step {
  position: relative;
  min-height: var(--space-64);
  border-radius: var(--r-sm);
  background: var(--swatch-color);
  box-shadow: inset 0 0 0 1px var(--swatch-edge);
  /* Per-step auto-contrast so an inside label (tonal number + hex) stays
     legible on any step lightness — same OKLCH-clamp as .swatch. */
  --swatch-on-color: oklch(from var(--swatch-color) clamp(0.08, (0.62 - l) * 100, 0.96) 0 0);
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: var(--space-8);
}
/* Inside labels — tonal number (top) + hex (bottom), auto-contrast.
   Optional: an empty step renders as a plain chip. */
.swatch-ramp__num {
  font-family: var(--font-sans);
  font-size: var(--text-caption);
  font-weight: var(--fw-bold);
  line-height: 1;
  color: var(--swatch-on-color);
}
.swatch-ramp__step .swatch-hex {
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  color: var(--swatch-on-color);
  opacity: 0.82;
  letter-spacing: 0;
}
/* --anchor — marks THE brand-anchor step in a ramp (e.g. sapphire-6 =
   the Sapphire seed). A small auto-contrast dot, top-right (clear of the
   number top-left + hex bottom-left). Absorbed from the Brand Book's
   bespoke ramp so brand-guide can consume the canonical primitive. */
.swatch-ramp__step--anchor::after {
  content: '';
  position: absolute;
  top: var(--space-8);
  right: var(--space-8);
  width: var(--space-8);
  height: var(--space-8);
  border-radius: var(--r-full);
  background: var(--swatch-on-color);
  opacity: 0.9;
}
/* Surface steps carry a NAME (not a tonal number) as the top inside-label
   — same inside style as the tonal accents (the surface ladder is named,
   not numbered). Auto-contrast like the number + hex. */
.swatch-ramp__step .swatch-name {
  font-size: var(--text-caption);
  font-weight: var(--fw-bold);
  line-height: 1;
  letter-spacing: var(--tracking-tight);
  color: var(--swatch-on-color);
}

/* ── Layout grids — auto-fit, not strict columns ─────────────────
   The grids reflow naturally based on container width: as soon as
   the inner content can't fit the swatch's --swatch-min, the grid
   drops a column. No hardcoded media queries; the breakpoints are
   intrinsic to the content. Consumers can author their own grid
   container too — these are just shortcuts that pair the right
   minimum width with each swatch size.

   Grid                   Min width   Pairs with
   ────────────────────   ─────────   ────────────
   .swatch-grid (default) 180 px      .swatch (default, 80 px)
   .swatch-grid--lg       220 px      .swatch--lg (120 px)
   .swatch-grid--xl       280 px      .swatch--xl (200 px)

   .swatch--sm (32 px) and .swatch--xs (16 px) are leaf-form dots —
   they don't need a grid container; pair with .swatch-row, .swatch-
   strip, or a bare flex row.

   May 2026 renumber: the container ladder shifted with the size
   ladder. Pre-renumber consumers were migrated atomically. */
/* Publishes --card-gap (per CLAUDE.md → "Card gap ladder") so per-instance
   overrides can flip the gap inline without touching this rule. Tiers
   step md → lg → xl across the size modifiers. The --xl tier rounds up
   from a prior off-ladder 20 px to --gap-card-xl (24 px) so the variant
   lands on the canonical ladder. */
.swatch-grid {
  --card-gap: var(--gap-card-md);
  display: grid;
  gap: var(--card-gap);
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}
.swatch-grid--lg {
  --card-gap: var(--gap-card-lg);
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.swatch-grid--xl {
  --card-gap: var(--gap-card-xl);
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}

/* ════════════════════════════════════════════════════════════════
   .swatch — UNIFICATION ADDITIONS (May 2026, Phase 1 — additive)
   ════════════════════════════════════════════════════════════════
   Folds every other color-display primitive in the system into the
   one .swatch family. Pre-May-2026 the codebase shipped ~10 separate
   primitives doing the same job (.color-chip, .pc-swatch, .palette-
   strip, .brand-chip-swatch, .ds-doc-preset, .neutral-swatch, …),
   each invented for one surface, none reusable across surfaces.

   What this phase adds — composable modifiers + containers that
   don't touch the existing size-ladder names. Consumers continue
   to render as-is until Phase 2 migrates them.

     SHAPE      .swatch--circle      50 % radius
                .swatch--square      0 radius
                .swatch--strip       band shape, sits in .swatch-strip

     DOT TIER   .swatch--xs          16 px leaf-form, inline in prose
                                     (defaults to circle, no children)

     STATE      .swatch--interactive button-form, hover/focus/active
                                     consumes --swatch-tone (CLAUDE.md
                                     "Variant-aware states" rule)

     CONTAINERS .swatch-strip        horizontal band (palette preview)
                .swatch-row          composer-style row (sm + label + hex)
                .swatch-trio         3-cell composition (bg/border/fg)
                .swatch-input        native color-input overlay

   Size renumber + consumer migration land in Phase 2/3. Until then,
   the existing .swatch--sm (80 px), .swatch (120 px) and .swatch--lg
   (200 px) tiers are unchanged. The new .swatch--xs sits below them;
   a future .swatch--xl (Phase 3) will sit alongside or replace --lg.

   Documented at design-system/index.html#swatch. */

/* ── .swatch--xs — inline dot ─────────────────────────────────────
   Leaf-form: the .swatch element IS the colored cell, no children
   required. Replaces .color-chip-swatch (20 px circle), the inline
   dot inside .color-chip, and any ad-hoc "<span style='background…'>"
   inline color references. Use in prose:
     <span class="swatch swatch--xs" style="--swatch-color: var(--accent-primary)"></span>
   Defaults to circle (the inline-dot use case). Override with
   .swatch--square or .swatch--rounded if needed. */
.swatch--xs {
  display: inline-block;
  width: 16px;
  height: 16px;
  min-width: 0;
  margin: 0;
  padding: 0;
  background: var(--swatch-color);
  border: 1px solid var(--alpha-border-strong);
  border-radius: 50%;
  vertical-align: middle;
  flex-shrink: 0;
  overflow: hidden;
  box-shadow: none;
  transform: none;
}
.swatch--xs:hover { transform: none; box-shadow: none; }
.swatch--xs > .swatch-tile,
.swatch--xs > .swatch-meta { display: none; }

/* ── Shape modifiers — work with any size ─────────────────────────
   Defined AFTER the size tiers so shape always wins over a size's
   default shape (e.g. .swatch--xs defaults to circle; pairing with
   .swatch--square correctly produces a sharp 16 px box). */
.swatch--circle,
.swatch--circle > .swatch-tile {
  border-radius: 50%;
}

.swatch--square,
.swatch--square > .swatch-tile {
  border-radius: 0;
}

/* ── .swatch--strip — band shape, fills its strip container ──────
   Lives inside a .swatch-strip container. Overrides the card-form
   defaults (.swatch's flex column, border, surface-card background,
   min-width, radius, padding) and becomes a flex:1 band that adopts
   the parent's height and renders the --swatch-color edge to edge. */
.swatch--strip {
  display: block;
  flex: 1 1 0;
  width: auto;
  min-width: 0;
  height: 100%;
  margin: 0;
  padding: 0;
  background: var(--swatch-color);
  border: none;
  border-radius: 0;
  box-shadow: none;
  transform: none;
  overflow: hidden;
}
.swatch--strip:hover { transform: none; box-shadow: none; }
.swatch--strip > .swatch-tile,
.swatch--strip > .swatch-meta { display: none; }

/* ── .swatch--interactive — button-form ──────────────────────────
   Turns a .swatch into a clickable picker / preset / preview button.
   States consume --swatch-tone (defaults to --swatch-color, see base
   block above) so every interactive swatch glows in ITS OWN colour
   — never a hardcoded --accent-primary. Pattern matches the .tile /
   .brand-card / .accent-card variant-aware-state recipe documented
   in CLAUDE.md. Pair with .swatch-input below to make the swatch
   the trigger for a native <input type="color">. */
.swatch--interactive {
  position: relative;
  appearance: none;
  -webkit-appearance: none;
  cursor: pointer;
  font: inherit;
  color: inherit;
  text-align: inherit;
  transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
}

.swatch--interactive:hover {
  transform: scale(1.08);
  border-color: color-mix(in oklch, var(--swatch-tone) 40%, transparent);
}

.swatch--interactive:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px color-mix(in oklch, var(--swatch-tone) 35%, transparent);
}

.swatch--interactive.is-active {
  border-color: var(--swatch-tone);
  /* Two-layer glow — soft outer halo + tight inner accent ring so the
     selected state has clear presence even at small sizes (24-32 px
     composer dots). The outer halo blooms in the swatch's own colour
     (--swatch-tone) so it feels native to the swatch, not a generic
     selection chrome. */
  box-shadow:
    0 0 0 2px color-mix(in oklch, var(--swatch-tone) 48%, transparent),
    0 0 18px 4px color-mix(in oklch, var(--swatch-tone) 32%, transparent);
}

/* Checkmark glyph for the selected state. Rendered via a CSS mask so the
   glyph colour auto-adapts to the swatch's own contrast (--swatch-on-color,
   the OKLCH-clamp-derived "legible on this hue" colour we already use for
   the .swatch-tile-label). On a dark swatch the checkmark renders white;
   on a light swatch it renders dark. For gradient swatches --swatch-on-color
   falls back to inherited text colour (the from-color OKLCH parse fails on
   gradient values), which still reads cleanly against most gradients.

   The .swatch--xs leaf-form is too small to carry a legible checkmark — we
   suppress it there and rely on the glow + border alone for the selected
   state. .swatch--sm (32 px) and larger get the checkmark. */
.swatch--interactive.is-active::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-color: var(--swatch-on-color, currentColor);
  -webkit-mask: var(--swatch-checkmark) center / 50% 50% no-repeat;
  mask: var(--swatch-checkmark) center / 50% 50% no-repeat;
}
.swatch--xs.swatch--interactive.is-active::after { display: none; }

/* The checkmark glyph as an inline SVG mask. Defined as a custom property
   on :root so authors can swap it (e.g. a star for "favourite" pickers,
   a dot for "current" indicators) by overriding --swatch-checkmark on a
   parent. Default is a Lucide-style stroked check. */
:root {
  --swatch-checkmark: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3.5' stroke-linecap='round' stroke-linejoin='round'><path d='M20 6 9 17l-5-5'/></svg>");
}

/* Light-mode mix base — per CLAUDE.md variant-aware-states rule,
   color-mix base must be `transparent`, NEVER --surface-card or a
   literal white. In light mode --surface-page is OKLCH 100 % L /
   chroma 0 BUT with stored hue 268, which drifts every variant
   toward LI.FI blue in oklch interpolation. Mixing with transparent
   lets the page bg show through alpha while preserving the
   variant's actual hue at 100 %. */
[data-theme="light"] .swatch--interactive:hover {
  background: color-mix(in oklch, var(--swatch-tone) 6%, transparent);
  border-color: var(--swatch-tone);
}

/* ── Native colour-input overlay ─────────────────────────────────
   Pair with .swatch--interactive: place the input INSIDE the swatch
   element so the swatch IS the visible click target and the browser-
   native colour picker opens on click. The input itself is invisible
   (opacity:0) but receives the click + opens the OS picker. */
.swatch-input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: pointer;
  font-family: inherit;                              /* defensive — <input> UA default is Arial; inherit brand Figtree (input equivalent of the button reset) */
  border: none;
  padding: 0;
  margin: 0;
  border-radius: inherit;
  background: transparent;
}

/* ── .swatch-strip — horizontal palette band container ───────────
   Holds N .swatch--strip children as equal-width bands. Default
   width 100 % / height 36 px / 8 px radius / 1 px alpha border.
   Override per-consumer: width via inline style (theme cards use
   72 px fixed), height via the --swatch-strip-h custom property.
   Replaces .palette-strip (Phase 2 migration). */
.swatch-strip {
  display: flex;
  gap: 0;
  flex-shrink: 0;
  width: 100%;
  height: var(--swatch-strip-h, var(--space-36));
  border-radius: var(--space-8);
  overflow: hidden;
  border: 1px solid var(--alpha-border-strong);
  pointer-events: none;
}

/* ── .swatch-row — composer-style row container ──────────────────
   Holds a single small swatch (xs or sm) + a flex-1 label + a right-
   aligned hex value. Used by the theme composer's row picker and
   any preset-list UI. Replaces .pc-swatch-row (Phase 2). */
.swatch-row {
  display: flex;
  align-items: center;
  gap: var(--space-12);
  margin-bottom: var(--space-8);
}
/* Typography unified June 2026 (Theme Composer redesign): label and hex
   both read in the standard sans — the label at the control-label tier
   (sm/medium), the hex at the value-readout tier (--text-caption,
   semibold, tabular-nums). The hex was briefly mono ("it IS a code
   value") until the no-monotype-in-the-composer direction landed; the
   sans + tabular-nums recipe matches .slider-value — one value-readout
   voice everywhere. */
.swatch-row-label {
  flex: 1; min-width: 0;
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: var(--fw-medium);
  color: var(--text-primary);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.swatch-row-hex {
  flex-shrink: 0;
  font-family: var(--font-sans);
  font-size: var(--text-caption);
  font-weight: var(--fw-semibold);
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: var(--tracking-normal);
  font-variant-numeric: tabular-nums;
}

/* ── .swatch-trio — 3-cell composition container ─────────────────
   Used by the semantic composer to render bg / border / fg per
   role. Anchor cell (fg) takes 2× width so the dominant tile reads
   as primary. Replaces .semantic-composer-swatch-trio (Phase 2). */
.swatch-trio {
  --card-gap: var(--gap-card-xs);
  display: grid;
  grid-template-columns: 2fr 1fr 1fr;
  gap: var(--card-gap);
}
.swatch-trio .swatch {
  --swatch-min: 0;
  --swatch-tile-h: var(--space-56);
  --swatch-pad: 0;
  min-width: 0;
}
.swatch-trio .swatch .swatch-meta { display: none; }

/* ── .color-field — editable colour value field ───────────────────
   A field-row for colour I/O: swatch trigger (hosts the native
   <input type="color"> via .swatch-input) + an editable value
   input + trailing actions (copy button, format chip). Accepts typed /
   pasted HEX, rgb(), or oklch() — parsing + format cycling live in
   theme-composer-core.js (parseColorValue / formatColorValue); commit
   wiring is the consumer's (theme-editor.js is the canonical one).
   Field chrome follows the .field-* token family; invalid input flashes
   the danger ring via .is-invalid (consumer removes it after ~600ms).
   June 2026 — the Theme Composer redesign. */
.color-field {
  display: flex;
  align-items: center;
  gap: var(--space-8);
  height: var(--space-40);
  padding: var(--space-4) var(--space-8) var(--space-4) var(--space-4);
  background: var(--field-bg);
  border: 1px solid var(--field-border);
  border-radius: var(--space-12);
  transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
}
.color-field:focus-within {
  border-color: var(--field-border-focus);
  box-shadow: 0 0 0 3px var(--field-ring);
}
.color-field.is-invalid {
  border-color: var(--danger);
  box-shadow: 0 0 0 3px color-mix(in oklch, var(--danger) 22%, transparent);
}
.color-field__swatch {
  position: relative;
  flex: 0 0 auto;
  width: var(--space-32);
  height: var(--space-32);
  border-radius: var(--space-8);
  border: 1px solid var(--alpha-border-strong);
  overflow: hidden;
  cursor: pointer;
}
.color-field__value {
  flex: 1 1 auto;
  min-width: 0;
  appearance: none;
  -webkit-appearance: none;
  border: 0;
  background: transparent;
  padding: 0;
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: var(--fw-medium);
  letter-spacing: var(--tracking-normal);
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}
.color-field__value:focus { outline: none; }
.color-field__value::placeholder { color: var(--field-placeholder); }
/* Format chip — quiet toggle cycling HEX → RGB → OKLCH. */
.color-field__format {
  appearance: none;
  -webkit-appearance: none;
  border: 0;
  flex: 0 0 auto;
  padding: var(--space-2) var(--space-8);
  border-radius: var(--space-4);
  background: color-mix(in oklch, var(--text-primary) 7%, transparent);
  font-family: var(--font-sans);
  font-size: var(--text-micro);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-loose);
  text-transform: uppercase;
  color: var(--text-muted);
  cursor: pointer;
  transition: background 0.2s ease, color 0.2s ease;
}
.color-field__format:hover {
  background: color-mix(in oklch, var(--text-primary) 12%, transparent);
  color: var(--text-primary);
}
.color-field__format:focus-visible {
  outline: 2px solid var(--accent-primary);
  outline-offset: 1px;
}

/* ── .swatch-edit — expandable swatch-editor row ──────────────────
   A dedicated colour-token editor: collapsed head = swatch chip +
   name + value (hex) + chevron; expanded drawer = a SLOT for the
   editing controls (L/C/H sliders in the Theme Composer's Auto mode,
   the .color-field assign in Manual mode). Built ON the universal
   .reveal primitive for the open/close mechanics (delegated by
   shared.js → initSwatchEdit, mirroring initSettingRowExpand), but a
   FIRST-CLASS primitive — its head carries a swatch column the generic
   .setting-row__head doesn't have, so it's not a .setting-row--expand
   composition. Reads one tier lighter than the composer's framed
   setting-row--expand sections: a row in a list (hairline between
   siblings, hover wash on the head), not a card. The body is a slot —
   the primitive owns chrome + expand mechanics, the consumer owns what
   goes inside (per the .drawer-handle / .reveal split). June 2026 —
   the Theme Composer manual-palette + per-swatch override work. */
.swatch-edit { border-radius: var(--space-12); }
.swatch-edit + .swatch-edit { border-top: 1px solid var(--border-subtle); }
.swatch-edit__head {
  /* dual-host <button> reset — a <button> head leaks UA chrome if
     merely omitted (see .menu-item / .side-nav__item). */
  appearance: none; -webkit-appearance: none;
  border: 0;
  background: transparent;
  font-family: var(--font-sans);
  width: 100%;
  display: grid;
  grid-template-columns: var(--space-32) 1fr auto;  /* chip · meta · chevron */
  align-items: center;
  gap: var(--space-12);
  min-height: var(--space-48);
  padding: var(--space-8);
  border-radius: var(--space-12);
  cursor: pointer;
  text-align: left;
  transition: background 0.2s ease;
}
.swatch-edit__head:hover { background: color-mix(in oklch, var(--text-primary) 4%, transparent); }
.swatch-edit__head:focus-visible {
  outline: 2px solid color-mix(in oklch, var(--accent-primary) 50%, transparent);
  outline-offset: -2px;
}
/* The chip composes .swatch--sm (+ --circle / --square); the head grid
   reserves its 32 px column. */
.swatch-edit__chip { flex-shrink: 0; }
.swatch-edit__meta {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;   /* lets the value ellipsis */
}
.swatch-edit__name {
  font-size: var(--text-sm);
  font-weight: var(--fw-semibold);
  color: var(--text-primary);
  line-height: var(--lh-sm);
}
.swatch-edit__value {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  color: var(--text-muted);
  line-height: var(--lh-caption);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.swatch-edit__chevron {
  width: var(--space-16);
  height: var(--space-16);
  color: var(--text-muted);
  rotate: 0deg;
  transition: rotate 0.15s ease;
}
.swatch-edit.is-open .swatch-edit__chevron { rotate: 180deg; }
/* Drawer = the universal .reveal primitive; the variant adds only its
   ancestor-state opener. Padded body lives inside the bare collapser
   (vertical padding on .reveal__inner would floor the 0fr collapse). */
.swatch-edit.is-open > .reveal { grid-template-rows: 1fr; }
.swatch-edit__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-12);
  padding: var(--space-4) var(--space-8) var(--space-12);
}
/* Flush column of rows — gap 0 so the .swatch-edit + .swatch-edit
   hairlines sit between adjacent rows rather than floating in a parent
   flex gap (e.g. the composer's .setting-row__drawer, gap 12). */
.swatch-edit-list { display: flex; flex-direction: column; }

/* ── Bridge rules: legacy wrappers consuming the new swatch ──────
   After Phase 2 migration the inner .color-chip-swatch dot becomes
   a .swatch--xs (consuming --swatch-color), but the OUTER .color-
   chip wrapper still uses its own --chip-color custom property to
   tint its own pill background. These bridge rules pipe the
   wrapper's --chip-color into the inner swatch's --swatch-color so
   the migration is a class-rename only — no per-instance style
   rewrite needed. Plus the alpha-token preview chips get the
   correct dark/light backdrop ring on the inner dot. */
.color-chip .swatch--xs { --swatch-color: var(--chip-color); }
.color-chip--on-dark .swatch--xs {
  background: var(--chip-color);
  border: 1px solid rgba(255,255,255,0.15);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,0.05);
}
.color-chip--on-light .swatch--xs {
  background: var(--chip-color);
  border: 1px solid rgba(0,0,0,0.15);
  box-shadow: inset 0 0 0 1px rgba(0,0,0,0.04);
}

/* Theme cards (theme-composer) constrain the strip to 72 px wide —
   the four-stripe ladder reads as a brand-card signature there. The
   z-index + position pair lifts the strip over the .theme-card-select
   invisible overlay button (z-index:1) so clicks on the strip itself
   register as picks. Replaces the prior .theme-card .palette-strip
   rule (which is being deleted in Phase 3). */
.theme-card .swatch-strip {
  width: 72px;
  position: relative;
  z-index: 2;
}

/* Legacy compat */
.section-label { display: inline-block; font-size: var(--text-caption); line-height: var(--lh-caption); font-weight: var(--fw-bold); letter-spacing: var(--tracking-wider); text-transform: uppercase; color: var(--accent-secondary); margin-bottom: var(--space-16); opacity: 0.85; }
[data-theme="light"] .section-label { color: var(--accent-primary); }
.section-label::before { content: '// '; opacity: 0.5; }

.section-title {
  /* Display — clamps between H2 (2rem=32px) at small viewports
     and Display (3.5rem=56px) at desktop. Line-height --lh-display
     renders to 64px → --space-64. */
  font-size: clamp(var(--text-h2), 4.5vw, var(--text-display));
  line-height: var(--lh-display);
  letter-spacing: var(--tracking-tight);
  font-weight: var(--fw-extrabold);
  color: var(--text-primary);
  /* Documentation rhythm — title→desc is a paired-prose gap, tighter
     than desc→first-card. 20 px (was 24 px) is part of the cascade
     defined in design.md §13 → Documentation rhythm. */
  margin-bottom: var(--space-20);
}
.section-title.centered { text-align: center; }

.section-desc {
  /* Body — 1rem/1.5rem renders to 16/24 which sit on --space-16
     and --space-24 respectively.

     Color: `--text-secondary` (Apr 2026) — was `--text-muted` until
     this pass; the muted shade was *darker* than `.ds-doc-card__caption`
     (also a prose-mode description), inverting the visual grade.
     Same role = same colour: both descriptions read at secondary
     weight, distinguished from primary-text headings above them.

     Bottom margin: `var(--space-64)` (Apr 2026) — used to be
     undefined, leaving the gap between the section description and
     the first card to whatever the parent layout supplied. The 64 px
     is the widest pair in the documentation rhythm cascade
     (eyebrow→title 12 px, title→desc 20 px, desc→cards 64 px) and
     reads as "section header is finished, content begins". The
     existing `.ds-doc-subsection` rule overrides it when a chapter
     marker comes between desc and the first card. */
  font-size: var(--text-body);
  line-height: var(--lh-body);
  color: var(--text-secondary);
  max-width: 680px;
  margin-bottom: var(--space-64);
}
/* (When a chapter marker `.ds-doc-subsection` follows the section-desc
   directly, the existing rule downstream already sets the subsection's
   `margin-top: var(--space-40)`. With margin collapse the effective
   gap becomes max(64, 40) = 64 px — same generous breathing as
   desc → first card.) */
.section-divider {
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--border-brand), transparent);
  margin: 80px 0; position: relative; z-index: 2;
}
.gradient-text {
  background: linear-gradient(var(--accent-primary), var(--accent-primary)); /* TEMP solid — gradient text paused */
  -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
}

/* Hero-grade gradient typography — matches the hero H1 <span class="highlight">
   treatment. Use on section/card titles to echo the hero's brand shimmer.
   Heavy weight + negative tracking can clip the final glyph when clipping
   background to text, so padding-right extends the box past the last char. */
.text-gradient-hero {
  background: linear-gradient(var(--accent-primary), var(--accent-primary)); /* TEMP solid — gradient text paused */
  -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
  padding-right: 0.08em;
}

/* ── Mega display typography ────────────────────────────────
   Display-scale utilities for presentation decks, bold website
   statements, and single-word monuments. Tight line-heights
   (0.94–0.96), heavy weight (800/900), crunched tracking. NEVER
   use on body copy or anything with more than one or two lines.

   Pair any mega class with `.text-gradient-hero` (or the tuned
   `.text-gradient-mega`) to highlight a <span> or a whole block.

   Static vs fluid: the raw classes (e.g. `.text-statement`) use
   the static token size. Append `--fluid` (e.g.
   `.text-statement--fluid`) to get a `clamp()` that ramps from a
   smaller heading size on phones up to the full token at 1440px+.

   Word reveal: wrap animated text in `.statement-words` and
   give each word its own <span>. The wrapper sets each span to
   `inline-block` so `transform` animates without breaking lines;
   pass `--i` on each span to stagger a shared keyframe. */
.text-poster,
.text-poster--fluid,
.text-statement,
.text-statement--fluid,
.text-keynote,
.text-keynote--fluid,
.text-colossal,
.text-colossal--fluid {
  font-family: var(--font-sans, inherit);
  /* Deck-tier weight override. The rest of the large-type ladder
     (Hero / Display / H1) is 700; the 112–256px deck tier overrides
     to 800 because canvas-fill compositions go hollow at 700 — the
     letterforms read as outlines once they cross ~100px. Documented
     in design.md §03 → "Mega Display weight override". */
  font-weight: var(--fw-extrabold);
  text-wrap: balance;
  padding-right: 0.08em;  /* prevent final-glyph clip with background-clip:text */
}

.text-poster {
  font-size: var(--text-poster);
  line-height: var(--lh-poster);
  letter-spacing: var(--tracking-tighter);
}
.text-statement {
  font-size: var(--text-statement);
  line-height: var(--lh-statement);
  letter-spacing: var(--tracking-tighter);
}
.text-keynote {
  font-size: var(--text-keynote);
  line-height: var(--lh-keynote);
  letter-spacing: var(--tracking-crunch);
}
.text-colossal {
  font-size: var(--text-colossal);
  line-height: var(--lh-colossal);
  letter-spacing: var(--tracking-crunch);
}

/* Fluid variants — clamp(floor, fluid, ceiling). Floor keeps the
   word legible on a 390px phone; ceiling caps at the static token
   so nothing grows past the design spec on 4K monitors. */
.text-poster--fluid {
  font-size: clamp(var(--text-h1), 9vw, var(--text-poster));
  line-height: 1;
  letter-spacing: var(--tracking-tighter);
}
.text-statement--fluid {
  font-size: clamp(var(--text-display), 12vw, var(--text-statement));
  line-height: 1;
  letter-spacing: var(--tracking-tighter);
}
.text-keynote--fluid {
  font-size: clamp(var(--text-display), 16vw, var(--text-keynote));
  line-height: 0.96;
  letter-spacing: var(--tracking-crunch);
}
.text-colossal--fluid {
  font-size: clamp(var(--text-h1), 22vw, var(--text-colossal));
  line-height: 0.95;
  letter-spacing: var(--tracking-crunch);
}

/* Mega gradient — tuned for 112px+ type. Adds a wider hue path
   (180% horizontal span) and a vertical lean so the shimmer
   reads across a single word rather than compressing. */
.text-gradient-mega {
  background: linear-gradient(var(--accent-primary), var(--accent-primary)); /* TEMP solid — gradient text paused */
  background-size: 180% 100%;
  background-position: 0% 50%;
  -webkit-background-clip: text; -webkit-text-fill-color: transparent;
  background-clip: text;
  padding-right: 0.12em;  /* extra room at colossal sizes */
}

/* Statement word-reveal helper. Wrap each word in a <span>;
   use `--i` per span to stagger a shared animation.

     <h1 class="text-statement statement-words">
       <span style="--i:0">Any</span>
       <span style="--i:1">chain.</span>
       <span style="--i:2">Any</span>
       <span style="--i:3">asset.</span>
     </h1>

   Consumers attach keyframes — this ships the geometry only. */
.statement-words {
  display: inline-block;
}
.statement-words > span {
  display: inline-block;
  will-change: transform, opacity;
}
.statement-words > span + span { margin-left: 0.25em; }

/* Default reveal animation — opt in by adding `--reveal` to the
   wrapper. Staggered by `--i` on each word (0-indexed). Motion
   respects `prefers-reduced-motion`. */
.statement-words--reveal > span {
  opacity: 0;
  transform: translateY(0.4em);
  animation: statementWordReveal 0.7s cubic-bezier(0.2, 0.7, 0.1, 1) forwards;
  animation-delay: calc(var(--i, 0) * 90ms);
}
@keyframes statementWordReveal {
  to { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .statement-words--reveal > span {
    animation: none;
    opacity: 1;
    transform: none;
  }
}

/* Generic scroll-reveal */
@keyframes fadeUp {
  from { opacity: 0; transform: translateY(30px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Staggered card entry — larger travel distance than fadeUp; pair
   with a `transition-delay` cascade on child elements for the
   sequential reveal pattern used on pricing / plan cards. */
@keyframes cardFadeUp {
  0%   { opacity: 0; transform: translateY(40px); }
  100% { opacity: 1; transform: translateY(0); }
}


/* ══════════════════════════════════════════
   7. ACCENT CARDS — reusable pricing / feature cards
   ══════════════════════════════════════════ */

/* ── Base card ──
   Fill derives from `--surface-card` so the card follows theme + palette
   instead of a hardcoded white wash. `isolation: isolate` creates a new
   stacking context so the `backdrop-filter` blur kernel cannot pull the
   fixed nav (or other bright fixed elements) into the card fill as it
   scrolls past. Saturate dropped to 120% to avoid amplifying any bright
   pixel that does sneak through the backdrop. */
.accent-card {
  background: var(--surface-card);
  backdrop-filter: blur(40px) saturate(120%);
  -webkit-backdrop-filter: blur(40px) saturate(120%);
  border: 1px solid var(--border-subtle);
  border-radius: var(--card-radius);
  padding: 40px;
  display: flex; flex-direction: column;
  position: relative;
  isolation: isolate;
  transition: background 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;
}
/* Per-variant tone token — drives hover + active states so they track
   the variant's accent (primary / secondary / tertiary) without each
   variant duplicating its own :hover and :active rules. The light-theme
   base rule at the bottom of this block has higher specificity than
   per-variant :hover rules (attribute + 2 classes > 2 classes), so
   per-variant overrides can't beat it via specificity alone — routing
   through this variable is what makes hover/active variant-aware. */
.accent-card { --accent-card-tone: var(--accent-primary); }
.accent-card--secondary { --accent-card-tone: var(--accent-secondary); }
.accent-card--tertiary  { --accent-card-tone: var(--accent-tertiary); }

/* Hover — clean accent outline, no lift, no drop-shadow */
.accent-card:hover {
  border-color: var(--accent-card-tone);
}
/* :active press state — softened May 2026 to match the canonical UI-card
   direction (rim at 64% mixed into transparent, 4% accent fill). Dropped
   the prior 18 px additional halo glow so the variant's RESTING shadow
   (the colored halo that defines --primary / --secondary / --tertiary
   identity) stays visible through the press; the variant identity is
   preserved across rest → hover → active instead of being overridden.
   .accent-card has NO .is-active consumer — it's a marketing-CTA primitive,
   not a list-selection card; the click navigates rather than selects. */
.accent-card:active {
  background: color-mix(in oklch, var(--accent-card-tone) 4%, var(--surface-card));
  border-color: color-mix(in oklch, var(--accent-card-tone) 64%, transparent);
}

/* ── Primary variant — highlighted/recommended tier ── */
.accent-card--primary {
  border-color: color-mix(in oklch, var(--accent-primary) 30%, transparent);
  /* Glow — same 3-layer approach as `.ds-doc-menu-panel`. Reads as an
     accent-tinted halo in dark mode, softens to a colored drop shadow
     in light mode via the `[data-theme="light"]` override below. */
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.08),
    0 24px 64px -16px oklch(0% 0 0 / 0.55),
    0 8px 32px -6px color-mix(in oklch, var(--accent-primary) 28%, transparent);
}
.accent-card--primary:hover {
  border-color: var(--accent-primary-mid);
}

/* ── Secondary variant (teal highlight) ── */
.accent-card--secondary {
  border-color: color-mix(in oklch, var(--accent-secondary) 30%, transparent);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.08),
    0 24px 64px -16px oklch(0% 0 0 / 0.55),
    0 8px 32px -6px color-mix(in oklch, var(--accent-secondary) 28%, transparent);
}
.accent-card--secondary:hover {
  border-color: var(--accent-secondary);
}
/* :active inherits the base .accent-card:active recipe via --accent-card-tone
   (4% accent fill + 64%-transparent rim). The prior 2 px hard outer ring was
   retired May 2026 in the canonical-card softening — variant identity now
   lives in the resting halo, not in the press state. */
.accent-card--secondary .accent-card-chip {
  background: color-mix(in oklch, var(--accent-secondary) 12%, transparent);
  color: var(--accent-secondary);
}

/* ── Tertiary variant (outline style) ── */
.accent-card--tertiary {
  border-color: var(--border-subtle);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.08),
    0 24px 64px -16px oklch(0% 0 0 / 0.55),
    0 8px 32px -6px color-mix(in oklch, var(--accent-tertiary) 28%, transparent);
}
.accent-card--tertiary:hover {
  border-color: var(--accent-tertiary);
}
/* :active inherits the base .accent-card:active recipe via --accent-card-tone. */

/* ── Card inner elements ── */
.accent-card-chip {
  display: inline-block;
  font-size: var(--text-caption); font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-wide); text-transform: uppercase;
  padding: var(--space-4) var(--space-12); border-radius: var(--space-8);
  margin-bottom: var(--space-24); width: fit-content;
  font-family: var(--font-mono, 'Geist Mono', monospace);
}
.accent-card--primary .accent-card-chip {
  background: color-mix(in oklch, var(--accent-primary) 10%, transparent);
  color: var(--accent-primary-mid);
}
.accent-card--tertiary .accent-card-chip {
  background: color-mix(in oklch, var(--accent-tertiary) 10%, transparent);
  color: var(--accent-tertiary);
}
.accent-card-title {
  font-size: var(--text-h1); font-weight: var(--fw-extrabold); line-height: var(--lh-h1);
  color: var(--text-primary); letter-spacing: var(--tracking-tight);
  margin-bottom: var(--space-16); text-align: left;
  /* Hero-grade gradient typography — see `.text-gradient-hero` utility. */
  background: linear-gradient(var(--accent-primary), var(--accent-primary)); /* TEMP solid — gradient text paused */
  -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
  padding-right: 0.08em;
}
.accent-card-desc {
  font-size: var(--text-body); color: var(--text-muted);
  line-height: var(--lh-body); margin-bottom: var(--space-32); text-align: left;
}
.accent-card-features {
  list-style: none; padding: 0; margin: 0 0 var(--space-32);
  display: flex; flex-direction: column; gap: var(--stack-sm);
  flex: 1;
}
.accent-card-features li {
  display: flex; align-items: center; gap: var(--cluster-sm);
  font-size: var(--text-sm); color: var(--text-secondary);
  line-height: var(--lh-sm); text-align: left;
}
/* Check-bullet for each feature row uses the canonical .spot-icon
   primitive (compose .spot-icon.spot-icon--info.spot-icon--round in
   markup). Variant cards override --si-tone so the spot picks up the
   card's accent — primary stays on the brand --accent-primary that
   .spot-icon--info defaults to. */
.accent-card--secondary .accent-card-features .spot-icon { --si-tone: var(--accent-secondary); }
.accent-card--tertiary  .accent-card-features .spot-icon { --si-tone: var(--accent-secondary); }
.accent-card-cta { width: 100%; text-align: center; }

/* ── Accent card content alignment ── */
.accent-card { text-align: left; }
.accent-card-chip { text-align: left; }
.accent-card-features { text-align: left; }

/* Light theme */
[data-theme="light"] .accent-card {
  background: color-mix(in oklch, var(--surface-page) 72%, transparent);
  border-color: rgba(0,0,0,0.08);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.7),
    0 4px 24px rgba(0,0,0,0.05);
}
[data-theme="light"] .accent-card:hover {
  /* Mix with transparent (not #FFFFFF) so the page bg shows through the
     alpha while preserving the variant's hue at 100%. Same brand-pure
     pattern used on .brand-card:hover above. */
  background: color-mix(in oklch, var(--accent-card-tone) 6%, transparent);
  border-color: var(--accent-card-tone);
}
/* Light mode keeps the brand-pure mix (transparent base, not --surface-card
   — light's near-white --surface-card would drift secondary/tertiary tones
   toward LI.FI blue at 0 chroma; see CLAUDE.md → Variant-aware states). */
[data-theme="light"] .accent-card:active {
  background: color-mix(in oklch, var(--accent-card-tone) 4%, transparent);
  border-color: color-mix(in oklch, var(--accent-card-tone) 64%, transparent);
}
[data-theme="light"] .accent-card--primary {
  border-color: color-mix(in oklch, var(--accent-primary) 20%, transparent);
  /* Light mode — dark glow replaced by colored drop shadow. Ambient is
     softened (black at 0.08 vs 0.55 in dark), accent tint drops from 28%
     to 18% to avoid over-saturating against the light page. Matches
     `.ds-doc-menu-panel` light-mode shadow scheme. */
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.7),
    0 4px 24px rgba(0,0,0,0.05),
    0 0 0 1px color-mix(in oklch, var(--accent-primary) 8%, transparent),
    0 12px 28px -6px color-mix(in oklch, var(--accent-primary) 18%, transparent);
}
[data-theme="light"] .accent-card--secondary {
  border-color: color-mix(in oklch, var(--accent-secondary) 20%, transparent);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.7),
    0 4px 24px rgba(0,0,0,0.05),
    0 0 0 1px color-mix(in oklch, var(--accent-secondary) 8%, transparent),
    0 12px 28px -6px color-mix(in oklch, var(--accent-secondary) 18%, transparent);
}
[data-theme="light"] .accent-card--tertiary {
  border-color: rgba(0,0,0,0.08);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.7),
    0 4px 24px rgba(0,0,0,0.05),
    0 12px 28px -6px color-mix(in oklch, var(--accent-tertiary) 18%, transparent);
}

/* ── Reusable card glow/shadow utility ────────────────────────────────
   Opt-in class that layers the 3-part shadow recipe used by pricing
   cards and `.ds-doc-menu-panel` onto any card. Defaults to accent-primary;
   use the `--secondary`/`--tertiary` modifier for the other accents.

   Dark mode: deep ambient + accent-tinted halo reads as a glow.
   Light mode: softer ambient + lower-alpha accent reads as a colored
   drop shadow.

   Apply on a card with a solid or frosted background (`.card`,
   `.info-card`, `.theme-card`, etc.) — do not apply to fully transparent
   surfaces or the shadow will clip at the card edge.
     <div class="card card-glow">…</div>
     <div class="card card-glow--secondary">…</div>
     <div class="card card-glow--tertiary">…</div>
─────────────────────────────────────────────────────────────────────── */
.card-glow,
.card-glow--primary {
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.08),
    0 24px 64px -16px oklch(0% 0 0 / 0.55),
    0 8px 32px -6px color-mix(in oklch, var(--accent-primary) 28%, transparent);
}
.card-glow--secondary {
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.08),
    0 24px 64px -16px oklch(0% 0 0 / 0.55),
    0 8px 32px -6px color-mix(in oklch, var(--accent-secondary) 28%, transparent);
}
.card-glow--tertiary {
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.08),
    0 24px 64px -16px oklch(0% 0 0 / 0.55),
    0 8px 32px -6px color-mix(in oklch, var(--accent-tertiary) 28%, transparent);
}
[data-theme="light"] .card-glow,
[data-theme="light"] .card-glow--primary {
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.7),
    0 4px 24px rgba(0,0,0,0.05),
    0 12px 28px -6px color-mix(in oklch, var(--accent-primary) 18%, transparent);
}
[data-theme="light"] .card-glow--secondary {
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.7),
    0 4px 24px rgba(0,0,0,0.05),
    0 12px 28px -6px color-mix(in oklch, var(--accent-secondary) 18%, transparent);
}
[data-theme="light"] .card-glow--tertiary {
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.7),
    0 4px 24px rgba(0,0,0,0.05),
    0 12px 28px -6px color-mix(in oklch, var(--accent-tertiary) 18%, transparent);
}


/* ══════════════════════════════════════════
   7b. TILE — content-card primitive
   ══════════════════════════════════════════
   Icon + title + body. Replaces .feature-card and .enterprise-card
   (both kept as compat aliases via grouped selectors). Composable
   modifiers cover layout (flush / horizontal), size (sm / lg),
   and accent tint (accent2 / accent3). Parts use BEM-style child
   classes so templates for decks, landing pages, and in-UI reuse
   stay identical.

   Anatomy:
     .tile
       .tile-tag                                     — optional uppercase mono badge
       .spot-icon.spot-icon-{lg|xl|3xl}.spot-icon--info — canonical icon container; pick the size tier that matches the tile size (lg for .tile--sm, xl for default, 3xl for .tile--lg). Replaced the legacy .tile-icon May 2026.
       .tile-title                                   — h3-scale heading
       .tile-body                                    — sm-scale description
       .tile-meta                                    — caption-scale secondary line
       .tile-cta                                     — trailing link/button

   Modifiers:
     .tile--flush        — flat corners, for 1px-gap connected grids
     .tile--horizontal   — icon on the left, content stacked right
     .tile--sm / --lg    — compact / generous padding + icon
     .tile--accent2      — retint toward --accent-secondary
     .tile--accent3      — retint toward --accent-tertiary

   Containers:
     .tile-grid                — 24px gaps, standalone rounded tiles
     .tile-grid--flush         — 1px hairline gaps, shared outer radius
     .tile-grid--2/--3/--4     — explicit column count
   ══════════════════════════════════════════ */

/* ── .tile — marketing-surface card primitive
   ════════════════════════════════════════════════════════════════
   Canonical noun for marketing-tier card surfaces (homepage feature
   grids, accent-saturated promo blocks, scrollable sections that
   demand attention). Pairs with `.card` (UI-tier) by audience —
   tiles for marketing, cards for product. See design/components/surfaces.md →
   "Tile vs Card — audience-led naming" for the full rule.

   Both names share the elevation foundation (--surface-raised /
   --surface-card + --elev-N + faint hairline border) and
   diverge on density + accent intensity. Tiles deliberately wear
   a tinted fill (3 % --tile-accent) so the marketing splash reads
   from across the grid; cards stay quiet on --surface-raised.

   Legacy aliases — `.feature-card` and `.enterprise-card` remain
   first-class via the shared selector below so existing markup
   doesn't break. New code reaches for `.tile`.
   ════════════════════════════════════════════════════════════════ */
.tile,
.feature-card,
.enterprise-card {
  /* Dual-mode per-tile accent — public API for "card lights up in its
     own colour, with separate values per theme". Set inline:
       style="--tile-accent-light: oklch(...); --tile-accent-dark: oklch(...);"
     OR set --tile-accent directly for a single value across both
     themes. Either falls back to --accent-primary so plain tiles
     inherit the global brand. Promoted from the .brand-card
     --brand-c-light / --brand-c-dark contract (May 2026). */
  --tile-accent-light: var(--accent-primary);
  --tile-accent-dark:  var(--accent-primary);
  --tile-accent: var(--tile-accent-light);
  --accent-tint: var(--tile-accent);          /* bridge — feeds halo / tinted materials when used on this element */

  /* ── Sticky-active state tokens — reusable recipe (May 2026) ────
     Two tokens defined ON the tile family (not :root) so the
     inner var(--tile-accent) resolves per-element at compute time.
     Putting these at :root would freeze --tile-accent at the
     fallback (--accent-primary = brand blue), defeating per-card
     identity. Consumers (.tile.is-active / .feature-card.is-active /
     .enterprise-card.is-active and any future card-tier extension
     that opts into the recipe) pull these tokens directly.

     Recipe: the active state shares the hover bg-tint (6 % accent
     mix on --surface-card) and reinforces the selection with a
     soft 1 px accent ring (50 % alpha so it whispers, not shouts)
     and a stronger elevation (--elev-3 instead of hover's --elev-2).
     The lift is the primary signal — sibling cards keep their
     hover/rest shadow, so the active one visibly floats above
     the rest. The ring quietly confirms the accent identity.
     No accent glow: it competed with the ring + lift.

     Why `in oklab` for the bg? OKLCH's hue-as-angle weighted
     average drags the result hue toward --surface-card's blue
     undertone (h 268°) — Mabel's green (h 145°), Finn's cyan
     (h 200°), and brand blue all collapse to "generic blue" at
     low mix percentages. OKLab is the Cartesian sibling: same
     perceptual uniformity, mixes via (a, b) coordinates that
     don't drag when one input is near-neutral. Hue is preserved
     even at 6 % mix.

     INTENTIONALLY EXCLUDED from this recipe — these are not
     surface-card-based and have legitimately different visual
     contracts: .brand-card.is-active (transparent-base override),
     .theme-card.is-active (surface-raised base), .seg-item /
     .ds-doc-tab-nav-item.is-active (quieter pill controls),
     .swatch--interactive.is-active (selection ring).
     ─────────────────────────────────────────────────────────── */
  --state-bg-active:     color-mix(in oklab, var(--tile-accent) 6%, var(--surface-card));
  --state-border-active: color-mix(in oklch, var(--tile-accent) 50%, transparent);

  display: flex; flex-direction: column; gap: var(--space-16);
  /* Paper material by default — wired via :where(.tile, .feature-card,
     .enterprise-card) above. Override per-instance with data-material
     ("halo" for brand-identity cards, "tinted" for accent-forward
     cards, "glass" for floating-feel tiles). State styles below
     (:hover, :active, .is-active) layer accent overrides on top of
     the resolved --mat-* values. */
  background: var(--mat-bg);
  border: var(--mat-border);
  border-radius: var(--card-radius);
  padding: var(--space-40) var(--space-32);
  box-shadow: var(--mat-shadow), var(--mat-highlight);
  backdrop-filter: var(--mat-backdrop);
  -webkit-backdrop-filter: var(--mat-backdrop);
  position: relative; overflow: hidden;
  transition:
    background 0.18s ease,
    border-color 0.18s ease,
    box-shadow 0.18s ease,
    opacity 0.8s,
    transform 0.8s;
}
[data-theme="dark"] .tile,
[data-theme="dark"] .feature-card,
[data-theme="dark"] .enterprise-card {
  --tile-accent: var(--tile-accent-dark);
}
.tile:hover,
.feature-card:hover,
.enterprise-card:hover {
  /* Hover overlays a faint accent wash on top of the resolved card
     surface and bumps elevation via the material's hover-shadow token.
     Uses --surface-card as the mix base (theme-aware) so the hover
     reads correctly in both modes. The shadow now comes from
     --mat-shadow-hover so it follows whichever material the tile is
     wearing — frosted → --elev-2, halo → --elev-2, glass → glass shadow
     unchanged. May 2026 hover-choreography migration. */
  background: color-mix(in oklch, var(--tile-accent) 6%, var(--surface-card));
  box-shadow: var(--mat-shadow-hover);
}
.tile:active,
.feature-card:active,
.enterprise-card:active {
  background: color-mix(in oklch, var(--tile-accent) 9%, var(--surface-card));
  box-shadow: var(--elev-1),
              0 0 0 2px color-mix(in oklch, var(--tile-accent) 24%, transparent);
}

/* ── Sticky-active state — standard pattern for any tile/card that
   carries a persistent "selected" state (e.g. the active theme card
   in the brand catalog, an active filter chip, an active item in a
   settings list). Distinct from `:active` (mouse-down momentary) —
   `.is-active` is sticky.

   The glow uses --tile-accent, so cards that override --tile-accent
   locally (like `.brand-card` mapping it to a per-card --brand-c)
   light up in their own colour automatically. The 18px blur fits
   comfortably inside the typical 24px grid gap so the halo never
   crowds neighbouring cards.

   Recipe (May 2026 retune). Active = "hovered + ringed". The bg
   matches hover's 6 % accent tint exactly (so a hovered card and
   a sticky-selected card share visual weight on the page), and
   the full-strength accent border is the sole selection signal.
   Elevation comes from --mat-shadow-hover. No separate accent
   glow — it competed with the thin border and pushed the state
   into "shouting" territory. Two reusable tokens
   (--state-bg-active / --state-border-active) live on the tile
   family rule above; see that comment block for the OKLab vs
   OKLCH rationale and the opt-out list.

   Hierarchy (paper material — other materials wrap the same elev
   tiers with their own surface character, e.g. glass adds backdrop
   blur, halo adds a brand-bloom shadow layer):
     rest      0% accent, --mat-shadow       (=--elev-1)   resting on the page
     hover     6% accent, --mat-shadow-hover (=--elev-2)   slight lift
     :active   9% accent + 24% accent ring                 momentary press
     is-active 6% accent + 50% ring (1 px) + --elev-3      sticky selection, lifted

   Design intent + when to use is documented in design.md §13. ── */
.tile.is-active,
.feature-card.is-active,
.enterprise-card.is-active {
  background: var(--state-bg-active);
  /* Use inset box-shadow instead of an actual border so the active
     state doesn't cause layout shift on any material whose resting
     --mat-border is 0-width (glass, and any material that opts out
     of a resting hairline). A real border would push siblings in
     the grid by 1 px on each side when one card activates. Inset
     shadow paints inside the border-box with no impact on layout.
     The 1 px stays thin and reads as a soft accent ring (50 % alpha
     via --state-border-active); the real "selected" signal is the
     elevation step from --elev-2 to --elev-3, which lifts the active
     card above its hovered/rest siblings. */
  box-shadow: var(--elev-3),
              inset 0 0 0 1px var(--state-border-active);
}

/* Suppress browser default :focus outline. UA stylesheets (Chrome,
   Safari, Firefox) paint a blue / system-accent ring on :focus
   regardless of input modality. Setting outline: none on :focus and
   then re-adding the custom outline via :focus-visible below keeps
   keyboard accessibility intact while killing the stray blue ring
   that mouse-click activation was leaving behind on active cards. */
.tile:focus,
.feature-card:focus,
.enterprise-card:focus {
  outline: none;
}

/* Custom :focus-visible — the keyboard-nav focus indicator (and any
   case where the browser heuristic decides focus should be shown).
   Two-px outline in the full-strength accent reads as a "darker
   version" of the soft 50 % accent ring the active card already
   wears — same colour identity, just opaque. Applies to any .tile
   in any state, so a focused inactive card also signals its accent. */
.tile:focus-visible,
.feature-card:focus-visible,
.enterprise-card:focus-visible {
  outline:        2px solid var(--tile-accent);
  outline-offset: 2px;
}

/* Scroll-reveal animation — opt-in via .is-hidden → .is-visible or
   the legacy .visible state shared with feature-card/enterprise-card. */
.tile.is-hidden,
.feature-card.is-hidden,
.enterprise-card.is-hidden { opacity: 0; transform: translateY(30px); }
.tile.is-visible, .tile.visible,
.feature-card.visible,
.enterprise-card.visible { animation: fadeUp 0.8s forwards; }

/* ── .destination-card — navigation / hub card (extends .tile)
   ════════════════════════════════════════════════════════════════
   A destination chooser: heading, a 16:9 preview frame (a theme-
   swappable screenshot or any media), a short bullet list, and a
   full-width justified CTA. Composes .tile — surface, hover / active
   accent wash, focus ring, and the --tile-accent system all come for
   free; bind the accent per instance via the tile API
   (--tile-accent-light / --tile-accent-dark). Heading uses .tile-title,
   the list type uses .tile-body. Built for hub / lobby / landing index
   pages; first consumer is the Launchpad (index.html).

   CORNER MATH runs OUTWARD per the nested-radius rule (outer = inner +
   pad): the --button-radius anchor (16px — the lg-tier button radius) is
   the SEED — it sets the preview frame + CTA radius (the CTA re-asserts
   the seed, since the text-button default is now a tier tighter at 12);
   the card radius DERIVES as min(seed + padding, --space-32) — concentric
   up to the 32px ceiling, then flat, so a large padding doesn't over-round
   the card (default pad 32 → seed 16 + 32 = 48, clamped to 32). The ceiling
   is the OUTWARD twin of the nested-radius rule's inward 0px floor (see
   that rule). Change --destination-card-pad and the radius re-derives,
   still capped.
   ════════════════════════════════════════════════════════════════ */
.destination-card {
  --destination-card-pad: var(--space-32);
  /* OUTWARD concentric with an over-round ceiling — see the corner-math note above. */
  --destination-card-radius: min(calc(var(--button-radius) + var(--destination-card-pad)), var(--space-32));
  border-radius: var(--destination-card-radius);
  padding: var(--destination-card-pad);
  gap: var(--stack-sm);                       /* internal rhythm — DS vertical-stack family */
  text-decoration: none; color: var(--text-primary);
}

/* Preview slot — transparent passthrough; the frame is the only edge treatment. */
.destination-card__media { display: block; }

/* Preview frame — neutral-hairline surface the media fills edge to edge. Radius
   = the button seed (16px). At the default 32px padding the card radius hits its
   --space-32 ceiling, so the card corner is tighter than strictly-concentric with
   this frame — the intentional over-round cap (see the corner-math note above).
   --surface-card is the fallback fill if the media fails to load. Sized by its
   content (the 16:9 shot); give it an explicit aspect-ratio if you mount non-shot
   media with no intrinsic height. */
.destination-card__frame {
  position: relative; display: block;
  border-radius: var(--button-radius);
  border: 1px solid var(--border-subtle);
  background: var(--surface-card);
  overflow: hidden;
}

/* Theme-swappable preview image — mount two (one per mode); the cascade shows
   the one matching the active theme. The width/height attrs set the intrinsic
   ratio; height:auto lets aspect-ratio drive the rendered box. */
.destination-card__shot {
  display: block; width: 100%; height: auto;
  aspect-ratio: 16 / 9; object-fit: cover; object-position: top center;
}
.destination-card__shot--dark { display: none; }
[data-theme="dark"] .destination-card__shot--light { display: none; }
[data-theme="dark"] .destination-card__shot--dark { display: block; }

/* Bullet list — short "what's here" summary. Type from .tile-body; layout local. */
.destination-card__list { margin: 0; padding-left: var(--space-16); list-style: disc; }
.destination-card__list li + li { margin-top: var(--space-4); }

/* CTA — compose .btn-block .btn-with-icon .btn-justify on a .btn-* for the
   label-leading / arrow-trailing footer button. margin-top:auto on the foot
   pins it to the bottom so CTAs align across cards of differing height.
   pointer-events:none keeps the whole card the single link; the arrow slides a
   touch on card hover. Radius is pinned to the --button-radius seed (16) so the
   CTA stays concentric with the preview frame — the text-button default is now
   one tier tighter (12), so we re-assert the seed here rather than inherit it. */
.destination-card__foot { margin-top: auto; }
.destination-card__action { height: var(--space-48); padding-block: 0; pointer-events: none; border-radius: var(--button-radius); }
.destination-card__action svg { transition: transform .2s ease; }
.destination-card:hover .destination-card__action svg { transform: translateX(3px); }

/* ── .card — UI-surface card primitive
   ════════════════════════════════════════════════════════════════
   Canonical noun for product / dashboard / signed-in card surfaces.
   Quieter than `.tile` (no accent fill — pure surface elevation),
   smaller padding, tighter radius. Reach for `.card` when the
   surface holds dense data or controls (KPIs, settings rows, form
   sections, picker grids); reach for `.tile` when it holds a
   marketing message that needs to be felt from across the page.

   Both share the elevation foundation. Existing UI-tier classes
   (`.stat-card`, `.form-card`, `.theme-card`, `.action-card`,
   `.action-card`) are specialised variants — they keep their
   names for back-compat and consume the same recipe. New UI
   surfaces should reach for `.card` directly.

   Modifiers compose:
     .card--sm      tighter — pairs with .input--sm / .btn-sm
     .card--lg      looser  — pairs with .input--lg / .btn-lg
     .card--accent  accent-tinted fill (small dose; for marketing
                    leakage into a UI surface, not a brand splash)
   ════════════════════════════════════════════════════════════════ */
.card {
  display: flex;
  flex-direction: column;
  gap: var(--space-12);
  padding: var(--space-20) var(--space-24);
  /* Paper material — wired via :where(.card) above. The component
     consumes --mat-* so swapping to data-material="halo" / "glass" /
     "tinted" / "gradient" works without re-authoring the CSS. */
  background: var(--mat-bg);
  border: var(--mat-border);
  border-radius: var(--card-radius);
  box-shadow: var(--mat-shadow), var(--mat-highlight);
  transition: box-shadow .18s ease, background-color .12s ease,
              border-color .12s ease, transform .18s ease;
}
.card--sm {
  padding: var(--space-12) var(--space-16);
  border-radius: var(--card-radius);
  gap: var(--space-8);
}
.card--lg {
  padding: var(--space-32) var(--space-40);
  border-radius: var(--card-radius);
  gap: var(--space-16);
}
.card--accent {
  /* 3 % accent tint on the canonical card tier (--surface-card via
     Paper material), so accent variants sit at the same surface tier
     as their neutral siblings — just brand-leaning. May 2026
     ui-card unification: was layered on --surface-raised. */
  background: color-mix(in oklch, var(--accent-primary) 3%, var(--surface-card));
}
/* Hover only when the card is interactive — opt in via [data-tap] or
   wrap in a <button>/<a>. Static cards don't respond to hover. */
.card[data-tap="true"]:hover,
button.card:hover,
a.card:hover {
  box-shadow: var(--elev-2);
}
.card[data-tap="true"] { cursor: pointer; }
button.card,
a.card {
  font: inherit;
  text-align: left;
  cursor: pointer;
  text-decoration: none;
  color: inherit;
}


/* .card-select / .card-select-grid retired May 2026 v17 — promoted in v14
   from .playground-rail__card-select* to a universal primitive, then never
   adopted outside the catalog. Superseded by .option-card (preview + title +
   description) which the rail and the rest of the system already use. See
   CLAUDE.md → Primitive retirement is atomic — no aliases. */


/* ══════════════════════════════════════════════════════════════════════
   OPTION CARD — .option-card / .option-card-grid (universal)
   ══════════════════════════════════════════════════════════════════════
   Horizontal single-select tile — wireframe preview on the left, title
   + 1-line description on the right. Use for picking one option from N
   where each option benefits from a caption (drill-in settings, "pick a
   mode" panels, onboarding flows, layout / variant / mode / height
   pickers).

   Markup:
     <div class="option-card-grid">
       <button type="button" class="option-card is-active">
         <span class="option-card__preview" aria-hidden="true">…SVG…</span>
         <span class="option-card__body">
           <span class="option-card__title">Auto</span>
           <span class="option-card__description">Lets the widget grow with content</span>
         </span>
       </button>
       <button type="button" class="option-card">…</button>
     </div>

   Customization (per consumer):
     --option-card-grid-cols  default repeat(1, 1fr); set to repeat(2, 1fr)
                              for compact 2-col layouts in wider hosts.
     --option-card-tone       default var(--accent-primary); override at
                              the grid level for accent-secondary /
                              accent-tertiary themed pickers (follows the
                              "Variant-aware states — route through a
                              --*-tone token" CLAUDE.md rule).
     --option-card-preview-size  default var(--space-56) (56 px); the
                              preview slot is a square at this size.

   Single-select is the contract — consumer toggles .is-active on
   exactly one card via JS. The primitive doesn't enforce this.

   Composition with the canonical UI card recipe: --surface-card +
   --elev-1, 1 px transparent border at rest that flips to a softened
   accent rim on active. No halo glow, no layout shift. The Hover
   transitions doctrine (ease 0.2s on bg / color / border / shadow,
   bouncy transform on lift) lives in the .btn-* family — this card
   is not a button-shaped primitive, so it ships its own scoped
   transitions on the surface tokens only.
*/
.option-card-grid {
  --option-card-grid-cols: 1fr;
  --card-gap: var(--gap-card-sm);
  display: grid;
  grid-template-columns: var(--option-card-grid-cols);
  gap: var(--card-gap);
}

.option-card {
  --option-card-tone: var(--accent-primary);
  --option-card-preview-size: var(--space-56);
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-12);
  padding: var(--space-12);
  width: 100%;
  background: var(--surface-card);
  border: 1px solid transparent;
  border-radius: var(--space-12);
  color: var(--text-primary);
  font-family: var(--font-sans);
  text-align: left;
  cursor: pointer;
  box-shadow: var(--elev-1);
  transition: background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
}
.option-card:hover {
  background: color-mix(in oklch, var(--option-card-tone) 3%, var(--surface-card));
  box-shadow: var(--elev-2);
}
.option-card:active,
.option-card.is-active {
  background: color-mix(in oklch, var(--option-card-tone) 4%, var(--surface-card));
  border-color: color-mix(in oklch, var(--option-card-tone) 64%, transparent);
  box-shadow: var(--elev-2);
}
.option-card:focus-visible {
  outline: 2px solid var(--option-card-tone);
  outline-offset: -3px;
  border-radius: inherit;
}

.option-card__preview {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--option-card-preview-size);
  height: var(--option-card-preview-size);
  border-radius: var(--space-8);
  background: color-mix(in oklch, var(--text-primary) 5%, transparent);
  color: color-mix(in oklch, var(--text-primary) 50%, transparent);
  transition: background 0.2s ease, color 0.2s ease;
}
.option-card__preview > svg {
  width: 100%;
  height: 100%;
  display: block;
}
.option-card.is-active .option-card__preview {
  background: color-mix(in oklch, var(--option-card-tone) 10%, transparent);
  color: var(--option-card-tone);
}

.option-card__body {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;
}
.option-card__title {
  font-size: var(--text-sm);
  font-weight: var(--fw-semibold);
  color: var(--text-primary);
  line-height: var(--lh-sm);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.option-card__description {
  font-size: var(--text-caption);
  font-weight: var(--fw-regular);
  color: var(--text-muted);
  line-height: var(--lh-caption);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* .option-card--expandable (+ __main / __chevron / __drawer family)
   retired June 2026 (the standardization sweep) — zero product consumers
   after the Theme Composer redesign migrated the Custom-corners card to
   a --tile + sibling .reveal drawer. Successors: .setting-row--expand
   (collapsible SECTION) · .option-card--tile + .reveal (configurable
   CHOICE). */

/* ── Tile variant — .option-card--tile ─────────────────────────────
   Icon-top compact tile for dense choice grids (2–4 across via the
   grid's --option-card-grid-cols). The preview becomes a full-width
   band (the 64×40 glyphs read at their native aspect), the title
   centers below it, and the description is suppressed — consumers
   surface the ACTIVE choice's description as a single .muted caption
   under the grid instead (state-reactive, less repeated noise).
   Tone / hover / active / focus recipes are inherited from the base
   card untouched. June 2026 — the Theme Composer redesign. */
.option-card--tile {
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-8);
  padding: var(--space-8);
}
.option-card--tile .option-card__preview {
  width: 100%;
  height: var(--space-40);
}
.option-card--tile .option-card__body {
  align-items: center;
  text-align: center;
}
.option-card--tile .option-card__title {
  font-size: var(--text-caption);
  font-weight: var(--fw-medium);
}
.option-card--tile .option-card__description { display: none; }


/* .accordion (+ __header / __title / __summary / __chevron / __body)
   retired June 2026 — folded into the .setting-row family as the
   .setting-row--expand variant (same head-summary-chevron-over-.reveal
   shape, on the standard setting-row card chrome) so the system carries
   ONE "row with a label, a live value, and an affordance" primitive.
   Wiring moved shared.js → initAccordions() → initSettingRowExpand(). */


/* ══════════════════════════════════════════════════════════════════════
   REVEAL — .reveal (the universal grid-rows drawer)
   ══════════════════════════════════════════════════════════════════════
   THE layout-shift-free expand/collapse mechanism, encoded ONCE: a grid
   whose single row animates 0fr → 1fr, with a BARE collapser child.
   June 2026 standardization sweep — consolidated four hand-rolled copies
   (.accordion__drawer · .theme-editor-drawer · .option-card__drawer ·
   portal's .network-row__drawer). The .ui-quote-card detail slot stays
   bespoke by design (it choreographs margin/border/stagger beyond rows).

   Anatomy + state:
     .reveal           — the grid; collapsed at rest
     .reveal.is-open   — self-state opener (consumers that toggle the
                         element directly, e.g. the Theme Composer's
                         Custom-corners drawer)
     ancestor openers  — consumers driving state from a parent add ONE
                         rule: `.host.is-open > .reveal` /
                         `.host[data-state="x"] > .reveal`
                         { grid-template-rows: 1fr; } (.setting-row--expand
                         + the Portal network row do this). State binding
                         belongs to the consumer — same split as
                         .drawer-handle (primitive owns chrome/mechanics,
                         consumer owns positioning/state).
     .reveal__inner    — BARE collapser: min-height 0 + overflow hidden
                         ONLY. Vertical padding here floors the 0fr
                         collapse at the padding height (the grid-rows
                         accordion trap — CLAUDE.md); content padding
                         goes on a nested body element. Child shadows
                         need that body's gutter (overflow stays hidden
                         permanently). */
.reveal {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.28s ease;
}
.reveal.is-open { grid-template-rows: 1fr; }
.reveal__inner {
  min-height: 0;
  overflow: hidden;
}
@media (prefers-reduced-motion: reduce) {
  .reveal { transition: none; }
}


/* ══════════════════════════════════════════════════════════════════════
   THEME EDITOR — editor-local layout glue (LifiThemeEditor)
   ══════════════════════════════════════════════════════════════════════
   The shared LifiThemeEditor (theme-editor.js) mounts in five surfaces
   (FAB popover · enterEditView host · catalog demo · Portal modal ·
   playground rail). June 2026 redesign: the editor body is a stack of
   .setting-row--expand section cards (the collapsible setting-row
   variant) in a .setting-stack--tight, whose controls are
   .slider-field--compact units, .option-card--tile grids, and the
   .color-field — all universal. What remains HERE is only the editor's
   own layout glue: the mode seg, the swatch-strip picker rows, the
   active-choice caption, the custom-corners drawer, and the grouped
   derived-token preview.

   RETIRED in the redesign (atomic, no aliases): .theme-editor-label
   (section heads are now .setting-row__head), .theme-editor-slider
   (→ .slider-field--compact), .theme-editor-gaps / -surface / -sem
   (→ .setting-row__drawer), .tc-chip (→ .theme-editor-preview dots),
   the editor's .ds-doc-control-row usage (tier-1 catalog-meta class —
   was a naming-tier violation on a product surface), and
   .theme-editor-sections (→ .setting-stack.setting-stack--tight; the
   short-lived .accordion section shell folded into .setting-row--expand
   in the same June 2026 pass). */

/* Mode seg — full-width Light/Dark toggle pinned above the sections. */
.theme-editor-mode {
  display: flex;
  width: 100%;
  margin-bottom: var(--space-12);
}

/* Swatch-strip picker row — the re-pointing selector for accents
   (Colors) and roles (Semantic states). Dots are .swatch--sm circles;
   the active one wears .is-active (ring + check). The trailing name
   (sans) + optional hex (mono) identify the selection. */
.theme-editor-strip {
  display: flex;
  align-items: center;
  gap: var(--space-8);
  padding: var(--space-4) var(--space-4) var(--space-8);
}
.theme-editor-strip__name {
  flex: 1 1 auto;
  min-width: 0;
  text-align: right;
  font-size: var(--text-sm);
  font-weight: var(--fw-medium);
  color: var(--text-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Active-choice caption under a tile grid — one state-reactive line
   carrying the ACTIVE tile's description (tiles suppress their own). */
.theme-editor-active-desc {
  padding: 0 var(--space-4);
  margin: 0;
}

/* Typography section (features.font) — the brand-font .dropdown fills the
   drawer's width so the trigger reads as the section's single control:
   label left, chevron right. The panel pairs via --match-trigger (JS sets
   the panel width to the trigger's). Menu rows preview each family via
   inline font-family on .menu-item-title (theme-editor.js renderFontBody). */
.theme-editor-font { display: block; }
.theme-editor-font .dropdown__trigger {
  width: 100%;
  justify-content: space-between;
}
/* Two labeled font fields (Sans + Mono) stacked in the Typography drawer. */
.theme-editor-typo { display: flex; flex-direction: column; gap: var(--space-16); }
.theme-editor-typo-field { display: flex; flex-direction: column; gap: var(--space-6); }
.theme-editor-typo-field .form-label { margin: 0; }

/* Sub-zone divider inside a section drawer (palette ↔ canvas contrast). */
.theme-editor-divider {
  border-top: 1px solid var(--border-subtle);
  margin: var(--space-4) 0;
}

/* Theme Composer — Manual colour mode (the Auto/Manual seg + the override
   panel). The seg sits above the seed rows; the override panel is hidden in
   Auto via the wrapper's data-color-mode (JS toggles it). */
.theme-editor-colormode { margin-bottom: var(--space-12); }
.theme-editor-colors[data-color-mode="auto"] .theme-editor-overrides { display: none; }
.theme-editor-overrides {
  margin-top: var(--space-16);
  display: flex;
  flex-direction: column;
  gap: var(--space-16);
}
.theme-editor-ov-intro {
  margin: 0;
  font-size: var(--text-caption);
  line-height: var(--lh-caption);
  color: var(--text-muted);
}
.theme-editor-ov-group { display: flex; flex-direction: column; gap: var(--space-4); }
.theme-editor-ov-grouplabel {
  font-size: var(--text-micro);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-faint);
}
/* Pinned override — the value reads in accent (vs muted "derived"). */
.swatch-edit.is-pinned .swatch-edit__value { color: var(--accent-primary); }
.theme-editor-ov-reset {
  appearance: none; -webkit-appearance: none;
  border: 0;
  background: transparent;
  align-self: flex-start;
  padding: 0;
  font-family: var(--font-sans);
  font-size: var(--text-caption);
  color: var(--accent-primary-mid);
  cursor: pointer;
}
.theme-editor-ov-reset:hover { text-decoration: underline; }

/* Custom-corners drawer — the universal .reveal primitive (self-state:
   the editor toggles .is-open on the element). .theme-editor-drawer is
   a pure-composition MARKER on the reveal (no CSS of its own — names
   the drawer for grep + the __body namespace); only the padded body
   carries editor rules. Mechanics retired June 2026 → .reveal. */
.theme-editor-drawer__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-16);
  padding: var(--space-8) var(--space-4) var(--space-12);
}

/* Pill toggle nested inside the button-radius slider — sits below the tick
   row. Reuses .slider-label-row anatomy; center-align (a switch has no
   text baseline, unlike the label + numeric readout pairs). */
.corners-pill-row {
  align-items: center;
  margin-top: var(--space-8);
  padding-top: var(--space-12);
}

/* Derived-token preview — grouped, labeled, click-to-copy .swatch--xs
   dots (replaces the anonymous .tc-chip strip). */
.theme-editor-preview {
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
  margin-top: var(--space-12);
  padding-top: var(--space-12);
  border-top: 1px solid var(--border-subtle);
}
.theme-editor-preview__group {
  display: flex;
  align-items: center;
  gap: var(--space-8);
}
.theme-editor-preview__label {
  flex: 0 0 auto;
  width: var(--space-64);
  font-family: var(--font-sans);
  font-size: var(--text-micro);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-loose);
  text-transform: uppercase;
  color: var(--text-faint);
}
.theme-editor-preview__dots {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-wrap: wrap;
}

/* Theme vignette — the mini mock above the dot groups: a recessed canvas
   holding one card (text specimen + primary button). EVERY colour is painted
   inline by theme-editor.js → syncVignette from deriveTokens() output — the
   editor body sits outside the scoped canvas, so live var() reads would show
   the chrome's theme (the resolvedSemTone reasoning). CSS owns geometry only;
   the transparent border slots reserve the JS-painted hairlines (kill the
   paint, keep the slot). The card's mini cast shadow is fixed neutral —
   elevation reads against both modes' canvases at this scale. ~74px tall:
   inside the 64–80 budget for the narrowest (FAB, ~340px content) mount. */
.theme-editor-vignette {
  display: flex;
  padding: var(--space-12);
  border: 1px solid transparent;
  border-radius: var(--space-12);
}
.theme-editor-vignette__card {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-12);
  min-width: 0;
  padding: var(--space-12);
  border: 1px solid transparent;
  border-radius: var(--space-8);
  box-shadow: 0 1px 2px oklch(0% 0 0 / 0.10), 0 1px 1px oklch(0% 0 0 / 0.06);
}
.theme-editor-vignette__text {
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
  min-width: 0;
}
.theme-editor-vignette__title {
  font-size: var(--text-sm);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-normal);
  line-height: 1;
}
.theme-editor-vignette__bars {
  display: flex;
  align-items: center;
  gap: var(--space-4);
}
.theme-editor-vignette__bar {
  height: var(--space-4);
  border-radius: var(--space-2);
}
.theme-editor-vignette__bar:first-child { width: var(--space-56); }
.theme-editor-vignette__bar:last-child  { width: var(--space-32); }
.theme-editor-vignette__btn {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  height: var(--space-24);
  padding-inline: var(--space-12);
  border-radius: var(--space-8);
  font-size: var(--text-micro);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-normal);
  line-height: 1;
}


/* ══════════════════════════════════════════════════════════════════════
   BRAND CARD — composite component for brand/protocol profile cards
   ══════════════════════════════════════════════════════════════════════
   Use this when a card needs to display a brand identity at a glance:
   logo / name, category, color swatches, narrative description, and a
   primary action. Built ON TOP OF the universal `.tile` primitive — the
   shell, hover, and active styling are inherited; only the brand-
   specific subcomponents (header layout, chip grid, source link, full-
   width action) live in this section.

   Reusability: not just a competitive-analysis primitive. Anywhere you
   need a "brand profile in card form" — partner showcase, ecosystem
   directory, integration directory, even an internal team-credits
   page — this composite fits. Apr 2026 v4 it ships in
   design-system/competitors.html; future surfaces just compose the
   same parts.

   Composition (markup contract):

     <article class="tile brand-card" style="
         --brand-c-light:   oklch(...) ;
         --brand-c-dark:    oklch(...) ;
         --brand-c-on-light: #0a0a0a;     // contrast-aware text
         --brand-c-on-dark:  #ffffff;
       ">
       <div class="brand-card-header">
         <div class="eyebrow-word">{category}</div>
         <a class="brand-card-title-link" href="{site}" target="_blank">
           <h3 class="tile-title">{name}</h3>
           <span class="ms brand-card-title-link__icon">open_in_new</span>
         </a>
       </div>
       <div class="brand-card-chips">
         <div class="brand-chip">
           <span class="swatch swatch--strip" style="--swatch-color:{hex}"></span>
           <div class="brand-chip-hex">{hex}</div>
           <div class="brand-chip-label">{label}</div>
         </div>
         …repeat 2-3×…
       </div>
       <p class="tile-body">
         {narrative}
         <a class="brand-card-source-link" href="{src}">{srcName}<span class="ms">open_in_new</span></a>
       </p>
       <div class="brand-card-foot">
         <button class="btn-secondary btn-sm btn-with-icon" data-apply="{id}">
           <svg>✓</svg> <span>Preview theme</span>
         </button>
       </div>
     </article>

   Per-card brand identity (the bit that makes selected-state colourful):
   The active state lights up in the protocol's OWN colour, not the
   global --accent-primary. The card's inline custom properties carry
   the OKLCH primary for both modes plus a contrast-aware text colour
   for filled buttons (white on dark brand fills, near-black on bright
   ones like LayerZero lime or Jupiter gold). Mapping --tile-accent →
   --brand-c is the bridge: the SHARED `.tile.is-active` pattern paints
   bg / border / glow from --tile-accent, so each card lights up in its
   OWN brand identity automatically. NO duplicated active CSS lives
   below — this composite reuses the standard tile-active pattern.
   ══════════════════════════════════════════════════════════════════════ */

/* Shell — extends `.tile`. Override gap (20px between header / chips /
   body / foot) and bind the per-card brand colour to --tile-accent so
   the shared active-state pattern picks up the right hue. */
.brand-card {
  /* Map the brand-card markup contract (--brand-c-light / --brand-c-dark
     inline styles) onto the canonical .tile dual-mode accent API. The
     `.tile` rule above resolves --tile-accent from --tile-accent-light
     in light theme and --tile-accent-dark in dark theme; .brand-card
     just feeds the per-card brand colours into those slots. --brand-c
     stays as a contrast-aware sibling (used by the foot button fill),
     bound to the resolved --tile-accent. */
  --tile-accent-light: var(--brand-c-light, var(--accent-primary));
  --tile-accent-dark:  var(--brand-c-dark,  var(--accent-primary));
  --brand-c:    var(--tile-accent);
  --brand-c-on: var(--brand-c-on-light, var(--text-on-accent, #fff));
  gap: var(--space-20);
}
[data-theme="dark"] .brand-card {
  --brand-c-on: var(--brand-c-on-dark, var(--text-on-accent, #fff));
}

/* Hover + active state bg — mix with transparent, NOT with --surface-card.
   Reason: --surface-page (which --surface-card resolves to in light mode)
   is oklch(100% 0 268) — pure white but with stored hue 268. CSS color-mix
   in OKLCH interpolates that stored hue even when chroma is 0, so mixing
   --tile-accent with --surface-card drifts the painted hue toward 268
   (LI.FI blue) regardless of the brand's actual hue. Mixing with
   transparent lets the page bg show through the alpha while preserving
   the brand hue at 100%. Both states use the same brand-pure recipe;
   alpha differs (6% hover / 8% active) to match the canonical .tile
   recipe's contrast steps. */
.brand-card:hover {
  background: color-mix(in oklch, var(--tile-accent) 6%, transparent);
}
.brand-card.is-active {
  background: color-mix(in oklch, var(--tile-accent) 8%, transparent);
}

/* Header — eyebrow + title stacked vertically. The title is the link to
   the brand's website (an `<a>` wraps the h3 + a hover-revealed
   open_in_new icon). Hit target = the title itself; no separate corner
   icon. */
.brand-card-header {
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
  margin: 0;
}
.brand-card-header .eyebrow-word {
  margin: 0;
}
/* Brand-card eyebrow tracks the card's brand identity instead of the
   generic --accent-primary that the global .eyebrow-word rule paints.
   --brand-c is theme-aware via the --tile-accent cascade — picks up
   --brand-c-light in light theme, --brand-c-dark in dark. The light-
   theme variant is qualified with [data-theme="light"] so it beats the
   duplicate [data-theme="light"] .eyebrow-word rule that lives later
   in this file (specificity tie → source order wins). */
.brand-card-header .eyebrow-word { color: var(--brand-c); }
[data-theme="light"] .brand-card-header .eyebrow-word { color: var(--brand-c); }

/* Title link — undecorated wrapper around the h3 + hover icon. Title
   stays in its native colour at rest (no "blue link" treatment); the
   icon fades in on hover/focus to signal interactivity. */
.brand-card-title-link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-8);
  text-decoration: none;
  color: inherit;
  width: fit-content;
  max-width: 100%;
}
.brand-card-title-link .tile-title {
  margin: 0;
  transition: color 0.15s ease;
}
.brand-card-title-link__icon {
  opacity: 0;
  font-size: var(--text-body-xl);
  line-height: 1;
  color: var(--text-muted);
  transition: opacity 0.15s ease, color 0.15s ease;
  flex-shrink: 0;
}
.brand-card-title-link:hover .brand-card-title-link__icon,
.brand-card-title-link:focus-visible .brand-card-title-link__icon {
  opacity: 1;
  color: var(--brand-c);
}
.brand-card-title-link:hover .tile-title,
.brand-card-title-link:focus-visible .tile-title {
  color: var(--brand-c);
}

/* Title scale — bigger than default `.tile-title` (h3 24px). Use h2
   (32px) for strong card-level identity at a glance — designers
   scanning a multi-card grid recognise the brand from typography
   alone. */
.brand-card .tile-title {
  font-size: var(--text-h2);
  line-height: var(--lh-h2);
  letter-spacing: var(--tracking-tight);
}

/* ── Brand chip grid + chip — column of color tile + hex + label, used
   inside .brand-card to surface the brand's identity palette. The inner
   color tile is the unified .swatch primitive in --strip shape (scoped
   below to 56 px height + 12 px radius for the brand-chip context). May
   2026 v11 — replaces the prior .brand-chip-swatch self-contained CSS
   rule that hardcoded a colored block with no swatch integration. */
.brand-card-chips {
  display: flex;
  gap: var(--space-8);
  margin: 0;
}
.brand-chip {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
  min-width: 0;
}
/* Scope the unified .swatch--strip to 56 px height + 12 px radius when
   it sits inside a .brand-chip. Replaces the prior .brand-chip-swatch
   class; the markup is now <span class="swatch swatch--strip"
   style="--swatch-color: HEX"></span>. */
.brand-chip .swatch--strip {
  width: 100%;
  height: var(--space-56);
  border-radius: var(--space-12);
  box-shadow: inset 0 0 0 1px var(--alpha-border-subtle);
  flex: 0 0 auto;
}
.brand-chip-hex {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-loose);
  color: var(--text-secondary);
  text-align: left;
  text-transform: uppercase;
}
.brand-chip-label {
  font-size: var(--text-micro);
  font-family: var(--font-mono);
  color: var(--text-muted);
  text-align: left;
  letter-spacing: var(--tracking-loose);
  text-transform: uppercase;
}

/* Body min-height — keeps grid rows aligned across cards with different
   description lengths. Tweak only if all cards in a grid use the same
   shorter/longer copy band. */
.brand-card .tile-body { min-height: var(--space-64); }

/* Inline source link — appended to the body paragraph. Worded link, no
   "Source:" prefix or divider; reads as "<description> <name> ↗". */
.brand-card-source-link {
  color: var(--brand-c);
  text-decoration: none;
  font-weight: var(--fw-medium);
  white-space: nowrap;
  transition: color 0.15s ease;
}
.brand-card-source-link:hover {
  text-decoration: underline;
  text-underline-offset: 2px;
}
.brand-card-source-link .ms {
  font-size: var(--text-caption);
  vertical-align: -1px;
  margin-left: 0;
}

/* Foot — single primary action, full-width. `.btn-secondary` fills the
   row; in the active state the button switches to a brand-coloured
   filled fill via --brand-c with contrast-aware text via --brand-c-on. */
.brand-card-foot {
  margin-top: auto;
  padding-top: var(--space-16);
}
.brand-card .brand-card-foot .btn-secondary {
  width: 100%;
  justify-content: center;
  white-space: nowrap;
}
.brand-card .brand-card-foot .btn-secondary svg {
  width: var(--space-12);
  height: var(--space-12);
  flex-shrink: 0;
}
.brand-card.is-active .brand-card-foot .btn-secondary {
  background: var(--brand-c);
  color: var(--brand-c-on);
  border-color: transparent;
}
.brand-card.is-active .brand-card-foot .btn-secondary:hover {
  background: color-mix(in oklch, var(--brand-c) 88%, black);
  color: var(--brand-c-on);
}

/* ══════════════════════════════════════════════════════════════════
   .card-avatar + .card-avatar-grid — avatar quick-pick tile family
   ══════════════════════════════════════════════════════════════════
   Mini-card that pairs an avatar (chain mark, token logo, brand
   glyph, persona, wallet — any tier-1 visual identity) with the
   canonical UI card recipe. Square as a standalone tile; inside a
   .card-avatar-grid it stretches to fill its column (height stays
   on the ladder, so a stretched tile is a centered-mark rectangle).
   Renders typically at the top of a picker as a quick-pick strip,
   showing the first N items in the collection's canonical order plus
   a `.card-avatar--more` overflow tile that opens the full drill.

   Generalised from the original `.chain-shortcut` (May 2026) so a
   token picker, persona grid, wallet picker, or brand cloud can
   compose the same primitive with a different avatar source. The
   class name follows the host-first compound convention (parallel
   to `.chip-avatar`) — see CLAUDE.md → Compound class naming.

   Composition:
     .card-avatar-grid               CSS Grid (auto-fit fill + cap)
       .card-avatar                  tile (hosts an .avatar)
         <.avatar avatar--circle …>     mark / logo / glyph
       .card-avatar.is-active        selected
       .card-avatar.card-avatar--cluster   multi-mark 2×2 mini-grid
         <.card-avatar__cluster>        cluster wrapper
       .card-avatar.card-avatar--more     "+N" overflow tile

   Sizes — pair the grid + tile in lockstep. The tile dictates the
   inner avatar size via descendant selector, so consumers just
   write <img class="avatar avatar--circle">:
     .card-avatar-grid--sm + .card-avatar--sm   48 / 32
     (default)                                  56 / 40
     .card-avatar-grid--lg + .card-avatar--lg   64 / 48

   States — canonical UI card recipe (May 2026), mirrors
   .theme-card / .option-card / .ui-quote-card:
     rest:        --surface-card + 1 px transparent border + --elev-1
     :hover:      3% accent tint + --elev-2
     .is-active:  4% accent fill + 64%-transparent accent rim + --elev-2
     No halo glow, no layout shift.
   ══════════════════════════════════════════════════════════════════ */

/* Fill-to-width grid, with a 1–2-tile cap so a sparse config doesn't
   balloon. The default is auto-fit + minmax(cell, 1fr): it packs as
   many ≥-cell columns as fit (count driven by the cell MIN, since the
   1fr max is indefinite), then stretches every column to share the
   row — so the grid always meets both container edges and few tiles
   (3–5) stretch into wide centered-mark rectangles. `--card-avatar-cell`
   is the MINIMUM column width (the square tile size); a stretched tile
   keeps its ladder height and reads as a centered-mark rectangle. Gap
   stays on the card-gap ladder.

   The cap is the :has() rule below — see its comment for the why. */
.card-avatar-grid {
  --card-avatar-cell: var(--space-56);
  --card-avatar-cell-max: var(--space-120);
  --card-gap: var(--gap-card-sm);
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(var(--card-avatar-cell), 1fr));
  gap: var(--card-gap);
}
.card-avatar-grid--sm { --card-avatar-cell: var(--space-48); }
.card-avatar-grid--lg { --card-avatar-cell: var(--space-64); }

/* 1–2 tiles only — cap each at --card-avatar-cell-max and center the
   row, so a lone mark doesn't stretch across the full panel. This is
   a SEPARATE rule (not a cap baked into the default minmax) because
   auto-fit sizes its column COUNT from the track's max sizing function
   when that max is a definite length: minmax(cell, 120px) would yield
   only ~2 columns at the picker width and break the 3+ tile fill.
   With a definite max here the cap binds, and because the tracks are
   now content-sized (not 1fr) the leftover collects at the container
   level for justify-content to center — the inter-tile gap stays on
   the --gap-card ladder. Content-driven via :has() (no opt-in needed),
   per CLAUDE.md → Auto-detect content-driven state via :has(). Escape
   hatch for unbounded fill: set --card-avatar-cell-max: 1fr. */
.card-avatar-grid:not(:has(> :nth-child(3))) {
  grid-template-columns: repeat(auto-fit, minmax(var(--card-avatar-cell), var(--card-avatar-cell-max)));
  justify-content: center;
}

/* Inside the grid the tile fills its (≥ cell) column — height stays
   on the ladder via --card-avatar-size, so a stretched tile reads as
   a rectangle with the mark centered (the avatar slot is unchanged).
   Standalone tiles keep the square `width: --card-avatar-size` from
   the base rule below. (0,2,0) beats the base (0,1,0). */
.card-avatar-grid > .card-avatar { width: auto; }

.card-avatar {
  --card-avatar-size:   var(--space-56);
  --card-avatar-mark: var(--space-40);
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--card-avatar-size);
  height: var(--card-avatar-size);
  padding: 0;
  background: var(--surface-card);
  border: 1px solid transparent;
  border-radius: var(--card-radius);
  color: var(--text-primary);
  font-family: var(--font-sans);
  cursor: pointer;
  box-shadow: var(--elev-1);
  transition: background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
}
.card-avatar:hover {
  background: color-mix(in oklch, var(--accent-primary) 3%, var(--surface-card));
  box-shadow: var(--elev-2);
}
.card-avatar:active,
.card-avatar.is-active {
  background: color-mix(in oklch, var(--accent-primary) 4%, var(--surface-card));
  border-color: color-mix(in oklch, var(--accent-primary) 64%, transparent);
  box-shadow: var(--elev-2);
}
.card-avatar:focus-visible {
  outline: 2px solid var(--accent-primary);
  outline-offset: -3px;
  border-radius: inherit;
}

/* The tile dictates the inner avatar size — consumer writes
   <img class="avatar avatar--circle"> and the descendant rule
   (specificity 0,2,0) overrides the .avatar base --avatar-size
   (0,1,0). Size pairing is automatic. */
.card-avatar .avatar { --avatar-size: var(--card-avatar-mark); }

/* Sizes — pair with the Sizing Ladder (sm 48 / default 56 / lg 64).
   Padding stays 8 px on every tier: (size − avatar) / 2 = 8. */
.card-avatar--sm {
  --card-avatar-size:   var(--space-48);
  --card-avatar-mark: var(--space-32);
}
.card-avatar--lg {
  --card-avatar-size:   var(--space-64);
  --card-avatar-mark: var(--space-48);
}

/* Overflow variant — same chrome, "+N" label instead of an avatar.
   Click typically wires to open the full drill of the underlying
   collection (chain picker, token picker, etc.). Font size scales
   with tile size. */
.card-avatar--more {
  font-size: var(--text-body);
  font-weight: var(--fw-semibold);
  color: var(--text-secondary);
  letter-spacing: var(--tracking-tight);
}
.card-avatar--sm.card-avatar--more { font-size: var(--text-sm); }
.card-avatar--lg.card-avatar--more { font-size: var(--text-body-lg); }

/* Cluster variant — composite-mark affordance. Same chrome as the
   default tile, but the avatar slot hosts a 2 × 2 mini-grid of
   four representative marks instead of a single avatar — reads as
   a single composite identity (e.g. "all networks" on a chain
   picker, "all stablecoins" or "all majors" on a token picker, a
   curated multi-asset bundle, etc.). Consumer owns the semantic
   meaning via aria-label + click handler. The 4-px inner gap
   keeps the four marks reading as one shape; minis resolve to
   sm 14 / default 18 / lg 22 — off the 4-grid but on the 2-px
   sub-grid (acceptable for sub-shape geometry where the composite
   outline carries the affordance). */
.card-avatar--cluster .card-avatar__cluster {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-4);
  width:  var(--card-avatar-mark);
  height: var(--card-avatar-mark);
}
.card-avatar--cluster .card-avatar__cluster .avatar {
  --avatar-size: calc((var(--card-avatar-mark) - var(--space-4)) / 2);
  width:  var(--avatar-size);
  height: var(--avatar-size);
}

/* ══════════════════════════════════════════════════════════════════
   .identity-tile — tile-family identity-card composition
   ══════════════════════════════════════════════════════════════════
   Identity tile for any subject — personas, contributors, partner
   POCs, customer profiles. Renders an avatar + role eyebrow +
   display name, with optional bio, metric footer, named metadata
   blocks, and tag set. Composes on top of .tile (extends the
   surface, hover, active, and per-card --tile-accent contract).

   Two layouts:
     .identity-tile                Compact — head + bio + foot.
                                  For 3-up / 4-up subject grids.
     .identity-tile--detail        Expanded — 3-col head with
                                  subtitle + actions, 2-col body
                                  of named blocks. Single hero
                                  subject view.

   Anatomy:
     .tile.identity-tile
       .identity-tile-head         — avatar + identity stack
         <.avatar variant>
         .identity-tile-id
           .eyebrow-word          — role / category
           .identity-tile-name     — display name (h3 / h2 scale)
           .identity-tile-subtitle — optional muted line (detail)
         .identity-tile-actions    — optional, detail variant only
       .tile-body                 — bio paragraphs (compact)
       .identity-tile-body         — 2-col block grid (detail)
         .identity-tile-block      — eyebrow + content + tags
           .eyebrow-code
           <prose / list / composition>
           .identity-tile-tags     — chip wrap
       .identity-tile-foot         — bottom-pinned single stat (label + value)
         .identity-tile-foot-label — regular-case muted label
         .identity-tile-foot-val   — extrabold sans value (h2)

   Promoted May 2026 from page-local .personas-* slots in
   design-system/personas.html. The persona-domain content
   blocks (wallets list, balances table, override row) stay
   page-local — only the shape-of-an-identity-card primitive
   lives here. ══════════════════════════════════════════════ */

/* Dual-host reset. .identity-tile is rendered on a <button> (clickable
   persona / org pickers) as often as on an <article>/<div> (catalog demos,
   the detail view). A <button> host leaks UA chrome: centered text, the
   Arial UA font, buttontext colour, and the default cursor — which is why a
   page that forgot a `button.tile` reset (partners.html) rendered the org
   cards centred + Arial while personas.html looked right. Defend it on the
   component so it's correct on EVERY host, no per-page reset required. .tile
   already owns the surface (bg / border / radius / padding / width), so only
   the type + alignment + cursor leak needs covering — same dual-host
   discipline as .menu-item / .side-nav__item, scoped to the properties .tile
   doesn't set. font-family: inherit (NOT the `font` shorthand) so the
   per-slot font-size / weight rules below still stand. */
.identity-tile {
  text-align: left;
  font-family: inherit;
  color: inherit;
}
button.identity-tile,
a.identity-tile {
  cursor: pointer;
}

/* ── Compact layout: chip banner + photo + head (name) + bio + foot ─ */
/* .identity-tile-head holds the name (June 2026: the tier chip moved OUT of the
   head to become the card's FIRST element, above the photo — see the .chip rule
   below). align-items: stretch lets the name (a block h3) fill the width while
   its text stays left. The detail variant overrides this with a 3-col grid head
   (avatar + identity + actions). */
.identity-tile-head {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-12);
}

.identity-tile-id {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}

/* Bumped to h2 scale for the photo-first compact card (May 2026).
   Selector is .identity-tile .identity-tile-name (specificity 0,2,0)
   to win over the .tile h3 base rule at (0,1,1). The detail
   variant has its own `.identity-tile--detail .identity-tile-name`
   override below. */
.identity-tile .identity-tile-name {
  font-size: var(--text-h2);
  line-height: var(--lh-h2);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-snug);
  color: var(--text-primary);
  margin: 0;
}

/* ── .identity-tile-photo — hero portrait that fills the card width ─
   For the compact "photo-first" persona/teammate card. Sits as a direct
   child of .tile.identity-tile so the existing 32px horizontal padding
   provides the margins on the photo. Square aspect, rounded corners
   one tier softer than the card itself (.tile uses 20px → photo uses 16px),
   object-fit: cover so transparent-PNG portraits crop nicely. The implicit
   background uses --tile-accent so the alpha channel of a transparent
   PNG reveals the per-card brand colour. */
.identity-tile-photo {
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border-radius: var(--space-16);
  background: var(--tile-accent, var(--alpha-dim));
}

/* Compact variant: balance the tile padding so top / sides / bottom
   read as equal margins around the photo (the .tile default has 40 top
   and 32 sides, which makes the photo's top margin look heavier than
   its side margins on a photo-first card). */
.identity-tile:not(.identity-tile--detail) {
  padding: var(--space-32);
}

/* The tier chip is a full-width banner sitting at the TOP of the card, as its
   FIRST element — above the photo / avatar (June 2026: moved out of
   .identity-tile-head to be a direct child of the card; markup order = visual
   order via the .tile flex column, so no `order` override is needed). width:
   100% + centered label make it span the card edge-to-edge. Paints with the
   host card's --tile-accent so the chip and the plate-revealed photo background
   share one tone. Descendant selector (not direct child) so it works whether
   the chip is a direct card child or inside a future wrapper. */
.identity-tile:not(.identity-tile--detail) .chip {
  width: 100%;
  justify-content: center;
  background: color-mix(in oklch, var(--tile-accent) 14%, transparent);
  color: var(--tile-accent);
}
[data-theme="light"] .identity-tile:not(.identity-tile--detail) .chip {
  background: color-mix(in oklch, var(--tile-accent) 12%, transparent);
}

/* Compact body sits at --text-body (16px) — one tier above the .tile-body
   default of --text-sm (14px) which would be lost under the h2 name,
   and one tier below the earlier --text-body-lg bump that read too eager
   next to the 32px name. */
.identity-tile:not(.identity-tile--detail) > .tile-body {
  font-size: var(--text-body);
  line-height: var(--lh-body);
  color: var(--text-muted);
}

/* .identity-tile-lead — the persona's headline one-liner from the data
   model's `subtitle` field, sitting directly under the name+chip row
   and before the longer summary paragraph. Same size+line-height as
   the body (.tile-body) but in --text-primary so it reads as the lead
   instead of fading into the supporting prose below. Apply alongside
   .tile-body so the type sizing stays in one place. Selector mirrors
   the compact-body scope so specificity (0,3,0) ties the muted .tile-body
   rule above; source order wins. */
.identity-tile:not(.identity-tile--detail) > .identity-tile-lead {
  color: var(--text-primary);
}

.identity-tile-subtitle {
  font-size: var(--text-body);
  line-height: var(--lh-body);
  color: var(--text-muted);
  margin: var(--space-8) 0 0;
}

/* Foot — a single labelled stat (June 2026): a big extrabold sans value
   (.identity-tile-foot-val) above a caption-sized muted label
   (.identity-tile-foot-label). Left-aligned vertical stack, pinned to the
   bottom of the tile via margin-top: auto. No separator. Value-on-top is a
   column-reverse of the label-first DOM — safe here (no justify-content to
   invert, align is cross-axis, spans aren't focusable) and it keeps the
   screen-reader reading order label→value. Persona + org share this foot. */
.identity-tile-foot {
  display: flex;
  flex-direction: column-reverse;
  align-items: flex-start;
  gap: var(--space-4);
  padding-top: var(--space-16);
  margin-top: auto;
}
/* Label — a calm caption-sized regular-case muted caption sitting UNDER the
   value, NOT an uppercase mono eyebrow. */
.identity-tile-foot-label {
  font-size: var(--text-caption);
  line-height: var(--lh-caption);
  letter-spacing: var(--tracking-normal);
  text-transform: none;
  font-weight: var(--fw-regular);
  color: var(--text-muted);
}
/* Value — h2 (32) extrabold sans with tabular figures and lh 1 so the box is
   cap-height tall — sized to read as a supporting foot stat, not a hero number.
   (.identity-tile-metric — the old mono-h3 persona value — was retired here
   June 2026; persona + org now share this one value treatment.) */
.identity-tile-foot-val {
  font-size: var(--text-h2);
  line-height: 1;
  letter-spacing: var(--tracking-tighter);
  font-weight: var(--fw-extrabold);
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}

/* ── .identity-tile--detail — expanded hero subject layout ── */
.identity-tile--detail .identity-tile-head {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: var(--space-24);
  align-items: center;
  padding-bottom: var(--space-24);
  margin-bottom: var(--space-24);
  border-bottom: 1px solid var(--border-subtle);
}
@media (max-width: 720px) {
  .identity-tile--detail .identity-tile-head {
    grid-template-columns: auto 1fr;
    gap: var(--space-16);
  }
  .identity-tile--detail .identity-tile-actions { grid-column: 1 / -1; }
}

/* The detail variant is a STATIC panel (an <article>, not a picker tile),
   so it opts out of .tile's interactive :hover / :active accent wash —
   reset both back to the resting material values. (:focus / .is-active
   never apply: the article isn't focusable and never gets selected.) */
.identity-tile--detail:hover,
.identity-tile--detail:active {
  background: var(--mat-bg);
  box-shadow: var(--mat-shadow), var(--mat-highlight);
}
/* Light mode needs a higher-specificity twin: page.css's
   `[data-theme="light"] .tile:not(.brand-card):hover` (0,4,0) re-tints the
   bg + border on hover and out-specifies the theme-agnostic rule above
   (0,2,0) — the variant-aware-states trap. Three classes + the theme
   attribute reach (0,5,0) to beat it regardless of source order, and
   re-assert the resting bg + border so the panel stays inert in light. */
[data-theme="light"] .tile.identity-tile.identity-tile--detail:hover,
[data-theme="light"] .tile.identity-tile.identity-tile--detail:active {
  background: var(--mat-bg);
  border: var(--mat-border);
}

.identity-tile--detail .identity-tile-name {
  font-size: var(--text-h2);
  line-height: 1;
  font-weight: var(--fw-extrabold);
  letter-spacing: var(--tracking-tight);
}

.identity-tile-body {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-32);
}
@media (max-width: 900px) {
  .identity-tile-body { grid-template-columns: 1fr; gap: var(--space-24); }
}

.identity-tile-block {
  display: flex;
  flex-direction: column;
  gap: var(--space-12);
}

.identity-tile-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

/* ── .identity-tile--org — partner-organization variant ──────────────
   Person-LED org card: a large leader portrait + a small company-logo
   .avatar-badge (the .avatar-tandem), instead of the persona variant's lone
   full-width hero photo. Same compact chrome (.tile surface, head, lead, body,
   pinned foot); only the avatar treatment + the foot's two-metric row differ.
   The h3 is the ORG name, the .identity-tile-lead names the leader + role, the
   body is the org blurb, and the foot carries a single Volume stat in the same
   label+value treatment as the persona foot.
   Canonical consumer: design-system/partners.html. */
.identity-tile--org .identity-tile-avatar {
  margin-bottom: var(--space-4);
}
/* The org foot is now the SAME single-stat label+value stack as the persona
   foot (June 2026 — the old horizontal two-metric row + the integrations
   count were retired; the label/value treatment lives in .identity-tile-foot
   / -foot-label / -foot-val above, shared by both variants). */

/* ── 8q0. WALLET MOUNT — .ui-wallet-mount ────────────────────────────
   Product-surface state wrapper that hosts both wallet states in one
   slot. Shows a "Connect wallet" CTA when no persona is active; swaps to
   the canonical `.list-item` identity card (a single-item `.list--cards`)
   showing the active persona when one is. State is driven by
   `data-state="empty" | "connected"` set
   by the consumer's mount renderer (playground: swap.js → initWalletMount;
   Portal: portal.js → wallet mount); CSS handles the show/hide.

   Promoted from swap.css → styles.css (June 2026) when the Portal footer
   became the 2nd consumer — both surfaces load styles.css, so the mount is
   shared, not duplicated. Visual styling is entirely owned by its children
   (the button + the `.list-item` identity card), so the mount
   itself is layout-neutral and drops into any column / footer / header.

   Markup:
     <div class="ui-wallet-mount" data-state="empty">
       <div class="ui-wallet-mount__empty">
         <button class="btn-primary" data-action="open-wallet-modal">Connect wallet</button>
       </div>
       <div class="ui-wallet-mount__connected" data-mount-slot="connected"></div>
     </div>
   ──────────────────────────────────────────────────────────────────── */
.ui-wallet-mount {
  display: block;
  width: 100%;
}
.ui-wallet-mount__empty,
.ui-wallet-mount__connected {
  display: none;
}
.ui-wallet-mount[data-state="empty"] .ui-wallet-mount__empty {
  display: block;
}
.ui-wallet-mount[data-state="connected"] .ui-wallet-mount__connected {
  display: block;
}
/* Connect button fills its column. The button-tier classes carry their own
   height + chrome; this rule only pins the width. */
.ui-wallet-mount__empty > .btn-primary {
  width: 100%;
}
/* Connected status card spans the mount edge-to-edge — flush left/right, so it
   matches the full-width empty-state Connect button + the Support/Docs row above
   it. The status card composes .list--cards (for the card surface); here we drop
   only its horizontal shadow-gutter padding (scoped override on the mount — NOT
   the shared .list--cards default, which keeps its gutter for stacked lists).
   Vertical gutter stays so the card's --elev-1 shadow has room. */
.ui-wallet-mount__connected > .list--cards {
  padding-inline: 0;
}

/* ── 8q. PERSONA / IDENTITY CONNECT CARD — retired June 2026 ──────
   The bespoke .persona-connect-card primitive was retired and folded into
   the canonical .list-item + .list--cards composition (lead avatar / body
   name + address / trailing action or balance) — the same primitive as the
   modal's provider + ecosystem rows. Per-row tier colour routes through
   --list-card-tone on .list--cards; a prominent balance uses
   .list-item__trail--value. No aliases (CLAUDE.md → "Primitive retirement
   is atomic"). See: wallet-connect.js → statusCardHTML / personaListHTML;
   design/components/lists.md → Connect / identity row.
   ──────────────────────────────────────── */


/* ── Parts ─────────────────────────────────────────────────── */
.tile-tag {
  display: inline-flex; align-items: center;
  font-family: var(--font-mono, 'Geist Mono', monospace);
  font-size: var(--text-caption); font-weight: var(--fw-bold);
  line-height: var(--lh-caption);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  padding: var(--space-4) var(--space-12); border-radius: var(--r-full);
  background: color-mix(in oklch, var(--tile-accent) 12%, transparent);
  color: color-mix(in oklch, var(--tile-accent) 35%, white);
  width: fit-content;
  margin: 0;
}
[data-theme="light"] .tile-tag {
  color: color-mix(in oklch, var(--tile-accent) 65%, black);
}

/* May 2026: `.tile-icon` removed from this multi-selector — every
   `.tile` now uses the canonical `.spot-icon.spot-icon-xl.spot-icon--info`
   instead of a per-tile-family icon recipe. `.feature-icon` and
   `.enterprise-card-icon` remain on this rule for marketing surfaces;
   they'll migrate to `.spot-icon-xl` in a follow-up sweep so every
   icon-tile in the system speaks the same primitive. */
.feature-icon,
.enterprise-card-icon {
  width: 52px; height: 52px; border-radius: 12px;
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0; margin: 0;
  background: color-mix(in oklch, var(--tile-accent) 8%, transparent);
  border: 1px solid color-mix(in oklch, var(--tile-accent) 12%, transparent);
  color: var(--tile-accent);
}
.feature-icon svg,
.enterprise-card-icon svg { width: var(--space-24); height: var(--space-24); }
.feature-icon img,
.enterprise-card-icon img { width: 100%; height: 100%; object-fit: contain; }

.tile-title,
.tile h3, .feature-card h3, .enterprise-card h3 {
  font-size: var(--text-h3);
  line-height: var(--lh-h3);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-snug);
  color: var(--text-primary);
  margin: 0;
}

.tile-body,
.tile p, .feature-card p, .enterprise-card p {
  font-size: var(--text-sm);
  line-height: var(--lh-sm);
  color: var(--text-muted);
  margin: 0;
}

.tile-meta {
  font-size: var(--text-caption);
  line-height: var(--lh-caption);
  font-family: var(--font-mono, 'Geist Mono', monospace);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-faint);
  margin: 0;
}

.tile-cta {
  margin-top: auto;
  font-size: var(--text-sm);
  font-weight: var(--fw-semibold);
  color: var(--tile-accent);
  text-decoration: none;
  display: inline-flex; align-items: center; gap: var(--space-8);
  transition: color 0.15s ease, gap 0.2s ease;
}
.tile-cta::after {
  /* Trailing arrow = the library Lucide arrow-right (icons/arrow-right.svg)
     rendered as a currentColor mask, matching the .btn-with-icon::after recipe.
     Replaces the former Unicode → (U+2192) Figtree glyph, which rendered
     thinner/smaller and didn't match the library icon. Sized at 1.25em so it
     tracks the --text-sm label (vs. .btn-with-icon's fixed --space-20 md tier);
     currentColor inherits the CTA's --tile-accent in both themes. */
  content: '';
  width: 1.25em; height: 1.25em; flex-shrink: 0;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14'/%3E%3Cpath d='m12 5 7 7-7 7'/%3E%3C/svg%3E") center / contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14'/%3E%3Cpath d='m12 5 7 7-7 7'/%3E%3C/svg%3E") center / contain no-repeat;
  transition: transform 0.2s ease;
}
.tile-cta:hover { gap: var(--space-12); }
.tile-cta:hover::after { transform: translateX(2px); }

/* ── Size modifiers ────────────────────────────────────────── */
.tile--sm {
  padding: var(--space-24); border-radius: var(--card-radius);
  gap: var(--space-8);
}
.tile--sm .tile-title { font-size: var(--text-body); line-height: var(--lh-body); }
/* (Icon size cascade dropped May 2026: tiles now use the canonical
   `.spot-icon` family, where the size tier is explicit per consumer
   — `.spot-icon-lg` for sm tiles, `.spot-icon-xl` for default tiles,
   `.spot-icon-3xl` for lg tiles. Author the size on the icon, not
   the wrapper.) */

.tile--lg {
  padding: var(--space-48) var(--space-40); border-radius: var(--card-radius);
  gap: var(--space-24);
}
.tile--lg .tile-title { font-size: var(--text-h2); line-height: var(--lh-h2); }

/* ── Layout modifiers ──────────────────────────────────────── */
.tile--flush { border-radius: 0; }

.tile--horizontal {
  flex-direction: row; align-items: flex-start; gap: var(--space-24);
}
/* (`.tile--horizontal .tile-icon { margin: 0 }` removed May 2026 —
   `.spot-icon` already has no margin set, so the override was a
   no-op for the new icon primitive.) */
.tile--horizontal > div:last-child,
.tile--horizontal .tile-body-group {
  display: flex; flex-direction: column; gap: var(--space-8);
  flex: 1; min-width: 0;
}

/* ── Accent tint modifiers — swap the --tile-accent token so
   border, icon bg, tag, and cta color all shift together. ──── */
.tile--accent2 { --tile-accent: var(--accent-secondary); }
.tile--accent3 { --tile-accent: var(--accent-tertiary); }

/* ── Gradient background variants ──────────────────────────
   Brand-grounded gradients from the --grad-brand token family.
   Subtle variants (--grad-brand / --grad-rich / --grad-radial)
   keep palette-aware text readable by staying at low alpha.
   Bold variant fills with the full brand gradient and forces
   white text for hero / feature-callout emphasis.
   ──────────────────────────────────────────────────────── */

/* Subtle brand gradient — low-alpha blue → teal wash at 135°.
   Mirrors the signature brand gradient but keeps the tile legible. */
.tile--grad-brand {
  background: linear-gradient(135deg,
    color-mix(in oklch, var(--accent-primary) 10%, transparent) 0%,
    color-mix(in oklch, var(--accent-secondary) 10%, transparent) 100%);
  border-color: color-mix(in oklch, var(--accent-primary) 16%, transparent);
}
.tile--grad-brand:hover {
  background: linear-gradient(135deg,
    color-mix(in oklch, var(--accent-primary) 14%, transparent) 0%,
    color-mix(in oklch, var(--accent-secondary) 14%, transparent) 100%);
  border-color: var(--accent-primary-mid);
}

/* Rich 4-stop brand gradient — subtler cousin of --grad-brand-rich
   with low alpha. Pulls all three accents through the gradient. */
.tile--grad-rich {
  background: linear-gradient(135deg,
    color-mix(in oklch, var(--accent-primary) 12%, transparent) 0%,
    color-mix(in oklch, var(--accent-secondary) 8%, transparent) 50%,
    color-mix(in oklch, var(--accent-tertiary) 10%, transparent) 100%);
  border-color: color-mix(in oklch, var(--accent-primary) 18%, transparent);
}
.tile--grad-rich:hover {
  background: linear-gradient(135deg,
    color-mix(in oklch, var(--accent-primary) 16%, transparent) 0%,
    color-mix(in oklch, var(--accent-secondary) 12%, transparent) 50%,
    color-mix(in oklch, var(--accent-tertiary) 14%, transparent) 100%);
  border-color: var(--accent-primary-mid);
}

/* Radial — ambient glow anchored in the top-right corner.
   Keeps most of the tile neutral so text stays crisp. */
.tile--grad-radial {
  background:
    radial-gradient(ellipse 85% 65% at 100% 0%,
      color-mix(in oklch, var(--accent-primary) 18%, transparent) 0%,
      transparent 70%),
    color-mix(in oklch, var(--accent-primary) 3%, transparent);
  border-color: color-mix(in oklch, var(--accent-primary) 14%, transparent);
}
.tile--grad-radial:hover {
  background:
    radial-gradient(ellipse 85% 65% at 100% 0%,
      color-mix(in oklch, var(--accent-primary) 24%, transparent) 0%,
      transparent 70%),
    color-mix(in oklch, var(--accent-primary) 6%, transparent);
  border-color: var(--accent-primary-mid);
}

/* Bold — full-saturation brand gradient fill with white text.
   Reserved for hero / feature-callout tiles where the tile itself
   is the visual moment, not just a container. */
.tile--grad-bold {
  background: var(--grad-brand);
  border-color: transparent;
  color: #fff;
}
.tile--grad-bold .tile-title,
.tile--grad-bold .tile-body,
.tile--grad-bold .tile-meta { color: #fff; }
.tile--grad-bold .tile-body { opacity: 0.88; }
/* Gradient-fill tiles invert the spot-icon to white-on-white-tint so
   the icon stays visible against the saturated brand wash. Targets
   the canonical `.spot-icon` (and the legacy `.tile-icon` alias for
   any out-of-tree consumer mid-migration). */
.tile--grad-bold .spot-icon,
.tile--grad-bold .tile-icon {
  background: rgba(255,255,255,0.16);
  color: #fff;
}
.tile--grad-bold .tile-tag {
  background: rgba(255,255,255,0.18);
  color: #fff;
}
.tile--grad-bold .tile-cta { color: #fff; }
.tile--grad-bold:hover {
  background: var(--grad-brand);
  filter: brightness(1.06);
  border-color: transparent;
}

/* Bold, rich 4-stop variant — the full --grad-brand-rich as fill */
.tile--grad-bold-rich {
  background: var(--grad-brand-rich);
  border-color: transparent;
  color: #fff;
}
.tile--grad-bold-rich .tile-title,
.tile--grad-bold-rich .tile-body,
.tile--grad-bold-rich .tile-meta { color: #fff; }
.tile--grad-bold-rich .tile-body { opacity: 0.88; }
.tile--grad-bold-rich .spot-icon,
.tile--grad-bold-rich .tile-icon {
  background: rgba(255,255,255,0.16);
  color: #fff;
}
.tile--grad-bold-rich .tile-tag {
  background: rgba(255,255,255,0.18);
  color: #fff;
}
.tile--grad-bold-rich .tile-cta { color: #fff; }
.tile--grad-bold-rich:hover {
  background: var(--grad-brand-rich);
  filter: brightness(1.06);
  border-color: transparent;
}

/* Light-mode overrides — page.css's [data-theme="light"] .tile rule
   loads AFTER styles.css and has equal specificity (0,1,1,0), so it
   wins against .tile--grad-* at (0,0,1,0). We bump specificity here
   by doubling the class (.tile.tile--grad-*) to reach 0,1,2,0. */
[data-theme="light"] .tile.tile--grad-brand {
  background: linear-gradient(135deg,
    color-mix(in oklch, var(--accent-primary) 10%, transparent) 0%,
    color-mix(in oklch, var(--accent-secondary) 10%, transparent) 100%);
  border-color: color-mix(in oklch, var(--accent-primary) 20%, transparent);
}
[data-theme="light"] .tile.tile--grad-rich {
  background: linear-gradient(135deg,
    color-mix(in oklch, var(--accent-primary) 12%, transparent) 0%,
    color-mix(in oklch, var(--accent-secondary) 8%, transparent) 50%,
    color-mix(in oklch, var(--accent-tertiary) 10%, transparent) 100%);
  border-color: color-mix(in oklch, var(--accent-primary) 22%, transparent);
}
[data-theme="light"] .tile.tile--grad-radial {
  background:
    radial-gradient(ellipse 85% 65% at 100% 0%,
      color-mix(in oklch, var(--accent-primary) 18%, transparent) 0%,
      transparent 70%),
    color-mix(in oklch, var(--accent-primary) 3%, transparent);
  border-color: color-mix(in oklch, var(--accent-primary) 18%, transparent);
}
[data-theme="light"] .tile.tile--grad-bold {
  background: var(--grad-brand);
  border-color: transparent;
}
[data-theme="light"] .tile.tile--grad-bold-rich {
  background: var(--grad-brand-rich);
  border-color: transparent;
}

/* ── Tile grid containers ──────────────────────────────────── */
.tile-grid,
.enterprise-grid {
  --card-gap: var(--gap-card-xl);
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--card-gap);
}
.tile-grid--2 { grid-template-columns: repeat(2, 1fr); }
.tile-grid--3 { grid-template-columns: repeat(3, 1fr); }
.tile-grid--4 { grid-template-columns: repeat(4, 1fr); }

/* Flush — 1px hairline gap, shared outer radius via overflow:hidden.
   Works for any row/column count without first/last/nth corner hacks.
   Aliased to the legacy .features-grid and .feature-grid for compat. */
.tile-grid--flush,
.features-grid,
.feature-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1px; /* hairline rule between cards — read as a separator, not spacing */
  background: color-mix(in oklch, var(--accent-primary) 10%, transparent);
  border: 1px solid color-mix(in oklch, var(--accent-primary) 10%, transparent);
  border-radius: 20px;
  overflow: hidden;
}
.feature-grid { grid-template-columns: repeat(2, 1fr); }
.tile-grid--flush .tile,
.features-grid .feature-card,
.feature-grid .feature-card {
  border-radius: 0; border: none;
}

/* Responsive — collapse multi-column grids to 1 column at small vw */
@media (max-width: 768px) {
  .tile-grid,
  .tile-grid--flush,
  .enterprise-grid,
  .features-grid,
  .feature-grid { grid-template-columns: 1fr; }
}


/* ══════════════════════════════════════════
   8. HERO GRADIENT — accent-tinted top fade for hero sections
   ══════════════════════════════════════════
   Layers over the page ambient background (body::before / body::after).
   Must stay transparent-based — an opaque linear-gradient would mask the
   top + right ambient glows within the hero region. Fades from an
   accent-primary tint at the top through transparent at the bottom so
   the body ambient keeps showing through.
   ══════════════════════════════════════════ */
/* `.hero-gradient` removed (Apr 2026): hero modules now ship as a
   single reusable component (`.doc-header` / `.uc-hero` / etc.) on a
   flat canvas. The body-level `body::before` ambient halo carries any
   atmospheric light. No per-hero gradient class, no `.grad` span on
   titles — every team renders the same hero, no decoration drift. */

/* ══════════════════════════════════════════
   8b. SEARCH BAR — .search (legacy, visual-only)
   ══════════════════════════════════════════
   Static styled input — magnifier glyph + token padding + glass
   backdrop. Three sizes: `.search--sm` / default / `.search--lg`.
   No JS-driven behaviour anymore; the catalog uses it as a visual
   primitive in the "Search bar" card only.

   The page-wide search role moved to .cmd-palette in commit d4b9f92
   (centred ⌘K overlay with scope pills + grouped results + keyboard
   nav). The .search--sticky modifier (sticky-on-scroll FAB) and
   .search-results dropdown were retired Apr 2026 — both had zero
   production consumers. See the retirement note below for what was
   removed.

   Anatomy (current, visual-only):
     .search
       .search-icon   — leading search glyph
       .search-input  — native <input type="search">

   Size modifiers:
     .search--sm      — sidebars + table headers, tighter padding
     .search--lg      — hero placement, larger padding + heading font
   ══════════════════════════════════════════ */
/* ── Default tier — 40 height, font 14, icon 16, padding-x 24, gap 12.
   Aligned to the canonical sm/default/lg/xl ladder (heights step +8,
   labels step through the typography scale 12/14/16/18). Heights pinned
   via `height:` per the Sizing Ladders rule; flex `align-items: center`
   vertically centers the icon + input. May 2026 ladder alignment —
   previously default lived at 50px / 16 (off-ladder); the catalog Rules
   pane "skip the ladder" exception was retired in the same commit. */
.search {
  position: relative;
  display: flex; align-items: center;
  gap: var(--space-12);
  width: 100%; max-width: 640px;
  height: var(--space-40);
  padding: 0 var(--space-24);
  /* Pill radius — every tier stays a capsule. */
  border-radius: var(--r-full);
  /* Form-field family chrome — same --field-bg* token ladder used by
     .input, .dropdown__trigger, .search-trigger. Slightly darker than
     --mat-bg / paper material so the bar reads as a "fielded" input
     instead of a card-tier surface. Transparent border so the fill
     carries the chrome; hover/focus-within bump the fill, focus-within
     adds an accent ring on top. Apr 2026 — migrated from paper-material
     (--mat-bg + --mat-border) to align visually with .search-trigger,
     which the catalog sidebar uses as the canonical search entry. */
  background: var(--field-bg);
  border: 1px solid transparent;
  transition:
    background 0.18s ease,
    box-shadow 0.18s ease;
}
.search:hover {
  background: var(--field-bg-hover);
}
.search:focus-within {
  background: var(--field-bg-focus);
  box-shadow: 0 0 0 3px color-mix(in oklch, var(--accent-primary) 20%, transparent);
}

.search-icon {
  flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--text-muted);
  transition: color 0.18s ease;
}
.search:focus-within .search-icon { color: var(--accent-primary-mid); }
/* Default tier — 16 px icon. Sibling tiers override below. */
.search-icon svg { width: var(--space-16); height: var(--space-16); }

.search-input {
  flex: 1; min-width: 0;
  background: transparent;
  border: none; outline: none; padding: 0;
  color: var(--text-primary);
  font-family: var(--font-sans);
  /* Default tier — --text-sm (14) matches the ladder's default label.
     line-height: 1 keeps the input's intrinsic height tight so the
     pinned container height does the vertical centering via flex. */
  font-size: var(--text-sm);
  line-height: 1;
  letter-spacing: var(--tracking-normal);
  /* Fade the input + its placeholder in/out as the sticky bar
     collapses to a circle — keeps the crossfade smooth. */
  transition: opacity 0.18s ease 0.04s;
}
.search-input::placeholder { color: var(--text-muted); }
/* Hide the UA clear "x" — we handle clear via Escape */
.search-input::-webkit-search-cancel-button { -webkit-appearance: none; }

/* Small tier — 32 height, font 12 (--text-caption), icon 12, padding-x 16,
   gap 8. Dense rails, sidebars, table-header filters. Icon ladder steps
   +4 across the tiers: 12 / 16 / 20 / 24. */
.search--sm {
  height: var(--space-32);
  padding: 0 var(--space-16);
  gap: var(--space-8);
}
.search--sm .search-icon svg { width: var(--space-12); height: var(--space-12); }
.search--sm .search-input {
  font-size: var(--text-caption);
}

/* Large tier — 48 height, font 16 (--text-body), icon 20, padding-x 24,
   gap 12. Hero placement, comfortable rows, primary picker surfaces. */
.search--lg {
  height: var(--space-48);
  padding: 0 var(--space-24);
  gap: var(--space-12);
  max-width: 720px;
}
.search--lg .search-icon svg { width: var(--space-20); height: var(--space-20); }
.search--lg .search-input {
  font-size: var(--text-body);
}

/* X-Large tier — 56 height, font 18 (--text-body-lg), icon 24, padding-x 32,
   gap 16. Banner / statement placements, deck-scale hero search. May 2026. */
.search--xl {
  height: var(--space-56);
  padding: 0 var(--space-32);
  gap: var(--space-16);
  max-width: 800px;
}
.search--xl .search-icon svg { width: var(--space-24); height: var(--space-24); }
.search--xl .search-input {
  font-size: var(--text-body-lg);
}

/* (Sticky variant + Results dropdown CSS retired Apr 2026 — both
   components had zero production consumers; .cmd-palette replaced
   the hero search use case. ~233 lines removed: .search--sticky,
   .search-results, .search-result, .search-empty, related
   light-theme overrides + @keyframes dsSearchSlideIn + mobile media.) */


/* ══════════════════════════════════════════════════════════════════
   8c. DATA VISUALIZATION — .stat-card, .chart-*, .sparkline …
   ══════════════════════════════════════════════════════════════════
   A first-pass data-viz kit grounded in the existing token system:
   typography comes from the --text-· / --lh-· / --tracking-· scale,
   spacing from --space-·, colors from --series-1..8 (palette-aware
   for 1 & 2) and the --success / --danger / --warn semantic trio.
   Every component is theme-aware (dark + light) via the base tokens.

   Components in this block:
     .stat-card                  — KPI card (eyebrow + value + delta)
     .chip-delta-pos / -neg /    — delta chip (Chips family — see below)
       -neutral
     .sparkline             — tiny inline SVG trend
     .progress              — linear progress / segmented progress
     .bar-chart             — SVG categorical bars + per-row identity
     .line-chart            — SVG line / area chart wrapper
     .donut                 — SVG donut with center label
     .gauge                 — semi-circular 0→100 gauge
     .heatmap               — CSS-grid intensity matrix
     .chart-legend          — swatch + label + value rows
     .chart-tooltip         — floating hover panel (shadcn-style)
     .chart-grid/axis/tick  — shared axis typography + grid-lines
   ══════════════════════════════════════════════════════════════ */

/* ── Shared chart scaffolding ─────────────────────────────────
   Typography + grid-line primitives every chart component can
   compose. Axis labels use the mono font at caption size so they
   read like measurements, not body text. Grid lines stay at 6%
   ink so they never compete with the data series. */
.chart {
  position: relative;
  width: 100%;
}
.chart-title {
  font-size: var(--text-sm);
  line-height: var(--lh-sm);
  font-weight: var(--fw-semibold);
  color: var(--text-primary);
  letter-spacing: var(--tracking-tight);
}
.chart-subtitle {
  font-size: var(--text-caption);
  line-height: var(--lh-caption);
  color: var(--text-muted);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  font-weight: var(--fw-semibold);
}
.chart-tick {
  /* Axis tick labels read as normal UI text, not mono "measurement chrome":
     default sans family, caption size, medium weight, normal tracking, and a
     readable palette ink (muted, not faint). */
  font-family: var(--font-sans);
  font-size: var(--text-caption);
  font-weight: var(--fw-medium);
  line-height: 1;
  fill: var(--text-muted);
  color: var(--text-muted);
  letter-spacing: var(--tracking-normal);
}
.chart-grid-line {
  stroke: color-mix(in oklch, var(--text-primary) 6%, transparent);
  stroke-width: 1;
  stroke-dasharray: 2 4;
  fill: none;
}
.chart-axis-line {
  stroke: color-mix(in oklch, var(--text-primary) 15%, transparent);
  stroke-width: 1;
  fill: none;
}
/* Axis title — one tier above .chart-tick. Use for rotated y-axis
   labels ("Volume", "Open Interest", "Date range") and x-axis-title
   captions. Caption-size uppercase sans, secondary ink (not faint) so
   it reads as a LABEL, not a measurement.
   Typical SVG: <text class="chart-axis-title" transform="rotate(-90 cx cy)">…</text> */
.chart-axis-title {
  font-family: var(--font-sans);
  font-size: var(--text-caption);
  font-weight: var(--fw-semibold);
  line-height: var(--lh-caption);
  fill: var(--text-muted);
  color: var(--text-muted);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}

/* ── .chart-threshold ─────────────────────────────────────────
   Horizontal reference line drawn across the plot area — used to
   mark a mean, target, or benchmark ("$1B average", "Goal: 100K
   users", "Break-even"). Composes inside ANY .bars SVG (or area
   chart) as a sibling of the bars themselves; not a leaderboard
   modifier — it's a reusable atom that pairs with any chart
   primitive that has gridlines.

   Markup (paired):
     <line class="chart-threshold" x1="70" y1="N" x2="780" y2="N"/>
     <text class="chart-threshold-label" x="780" y="N">$1B avg</text>

   The label uses text-anchor: end and sits at the right edge of
   the plot, just above the line, so it never collides with bars.
   For inside-plot labels (rare), override text-anchor on the
   consumer side and offset y by a few pixels above the line.

   Visual contract: dashed 4-4 stroke at 30% --text-primary mix —
   reads as quieter than .chart-axis-line (15% solid) so the line
   doesn't compete with the data, but louder than .chart-grid-line
   (6% dashed 2-4) so it reads as a deliberate reference, not as
   another tick line. */
.chart-threshold {
  stroke: color-mix(in oklch, var(--text-primary) 30%, transparent);
  stroke-width: 1;
  stroke-dasharray: 4 4;
  fill: none;
}
.chart-threshold-label {
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  font-weight: var(--fw-medium);
  fill: var(--text-faint);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  text-anchor: end;
}

/* ── .stat-card ─────────────────────────────────────────────────
   KPI card. Three canonical slots: eyebrow label, hero number,
   footer (label + delta or sparkline). Uses the same surface +
   radius language as .tile so stats and tiles can share a grid. */
.stat-card {
  --stat-card-accent: var(--accent-primary);
  display: flex;
  flex-direction: column;
  /* Inter-slot rhythm — 12px (June 2026 refresh, was 4px) gives the big h1
     value room to breathe under the label. The flex gap is uniform, so it
     also opens value→sparkline/footer. --sm keeps its own tighter 4px. */
  gap: var(--space-12);
  padding: var(--space-24);
  border-radius: var(--card-radius);
  /* Paper material — wired via :where(.stat-card) above. Component
     reads from the resolved --mat-* API; the per-theme bg pivot
     (dark = 24% OKLCH, light = var(--surface-page)) lives in --surface-card,
     which paper material consumes. Override per-instance with
     data-material to wear glass / tinted / gradient. */
  background: var(--mat-bg);
  border: var(--mat-border);
  box-shadow: var(--mat-shadow), var(--mat-highlight);
  min-width: 0;
  position: relative;
  overflow: hidden;
}
.stat-card-eyebrow {
  /* Label treatment (June 2026) — the metric name reads as a calm 16px
     muted label, not an uppercase mono eyebrow. The slot class stays
     `-eyebrow` for now (slot identity); a rename to `.stat-card-label`
     is a pending follow-up — it collides with the footer
     `.stat-card-label`, which would move to `.stat-card-caption` per the
     content-vocabulary convention. Do that as one atomic sweep once the
     card design settles. */
  font-size: var(--text-body);
  line-height: var(--lh-body);
  letter-spacing: var(--tracking-normal);
  text-transform: none;
  color: var(--text-muted);
  font-weight: var(--fw-regular);
}
.stat-card-value {
  /* Default sits at h2-lg (36px) extrabold — a confident KPI number that
     stays calm in a dense grid. --sm drops to h3 (24); --lg bumps to
     display (56). (June 2026 stat-card refresh: default stepped h2 → h1 →
     h2-lg across iterations; weight stays extrabold throughout.)

     Line-height is a unitless 1 (Apr 2026) — KPI numbers sit on one line
     (`white-space: nowrap` below) so leading would only loosen the vertical
     rhythm. lh:1 makes the value box exactly cap-height tall. Tracking pairs
     with size: tighter at the default, crunch at --lg below. */
  font-size: var(--text-h2-lg);
  line-height: 1;
  letter-spacing: var(--tracking-tighter);
  font-weight: var(--fw-extrabold);
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
  min-width: 0;
  /* Numbers must never break mid-digit — no `overflow-wrap: anywhere`
     here. Overflow is handled by the grid (stats collapse to fewer
     columns at smaller breakpoints) or by dropping to --sm. */
  white-space: nowrap;
}
.stat-card-unit {
  /* Tightened Apr 2026: 0.5em → 0.45em, margin-left 0.15em → 0.1em.
     The unit reads as a clear suffix glued to its number rather than
     a separate token. Still tabular-friendly (the number itself owns
     `font-variant-numeric: tabular-nums`); the unit inherits baseline. */
  font-size: 0.45em;
  font-weight: var(--fw-semibold);
  color: var(--text-muted);
  letter-spacing: var(--tracking-normal);
  margin-left: 0.1em;
  vertical-align: baseline;
}
.stat-card-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-16);
  margin-top: var(--space-8);
}
.stat-card-label {
  font-size: var(--text-sm);
  line-height: var(--lh-sm);
  color: var(--text-muted);
}

/* ── New sub-elements (Apr 2026) — three additions to support the
   Tailwind UI Stats variants:
     .stat-card-icon  — leading icon next to (or above) the eyebrow
     .stat-card-prev  — "from <prior value>" comparison chip in the footer
     .stat-card-desc  — supporting description block below the value
   All three are optional and compose with existing .stat-card-* slots. */

/* Leading icon — accent-tinted 32×32 tile above the eyebrow. For the
   compact vertical layout. For the horizontal `--row` brand-icon KPI
   pattern, use the canonical `.spot-icon` family directly (typically
   `.spot-icon-xl`) instead of this slot — keeps the icon recipe
   shared with the rest of the system. */
.stat-card-icon {
  width: var(--space-32);
  height: var(--space-32);
  border-radius: var(--space-8);
  background: color-mix(in oklch, var(--stat-card-accent) 14%, transparent);
  color: var(--stat-card-accent);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  margin-bottom: var(--space-8);
}
.stat-card-icon svg {
  width: var(--space-16);
  height: var(--space-16);
}
[data-theme="light"] .stat-card-icon {
  background: color-mix(in oklch, var(--stat-card-accent) 10%, transparent);
}

/* Row layout — flips the card to `[icon] [text-cluster]` horizontal.
   Pair with the existing `.spot-icon` family (`.spot-icon-xl` for the
   canonical brand-icon KPI row). Wrap the right column in
   `.stat-card-text` so the label + value-row group as one cluster.
   The label is tuned down here: in dense KPI rows the base 16px muted
   label steps to 14px secondary so it pairs tighter with the bold
   value beside it. Case/family/tracking come from the base rule. */
.stat-card--row {
  flex-direction: row;
  align-items: flex-start;
  gap: var(--space-16);
  padding: var(--space-20);
}
.stat-card-text {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  flex: 1;
  min-width: 0;
}
.stat-card--row .stat-card-eyebrow {
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

/* Inline value + delta — value and delta sit on a single baseline,
   delta scaled down so it reads as supporting context to the value.
   Used inside `.stat-card-text` for the row layout. The chip-delta
   inside this row drops its chip background — pure text + arrow,
   no pill — because the row is dense and the pill would compete
   with the value. Targets all three tones explicitly so future
   chip-delta tones must opt in. */
.stat-card-value-row {
  display: flex;
  align-items: baseline;
  gap: var(--space-8);
  flex-wrap: wrap;
}
.stat-card-value-row .stat-card-value {
  font-size: var(--text-h2-lg);
  line-height: 1;
  letter-spacing: var(--tracking-tighter);
}
.stat-card-value-row .chip-delta-pos,
.stat-card-value-row .chip-delta-neg,
.stat-card-value-row .chip-delta-neutral {
  /* One chip tier up from the former --md 14px → the --lg tier's 16px
     (June 2026 refresh), so the delta keeps pace with the larger h1
     value. Stays frameless — no pill — per the rules below. */
  font-size: var(--text-body);
  font-weight: var(--fw-medium);
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0;
  background: transparent;
  border-radius: 0;
}

/* Prior-period chip — "from 70,946" muted comparison. Sits in the
   footer next to or instead of a chip-delta. */
.stat-card-prev {
  font-size: var(--text-caption);
  line-height: var(--lh-sm);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
.stat-card-prev-value {
  color: var(--text-secondary);
  font-weight: var(--fw-medium);
}

/* Description block — multi-line supporting text below the value.
   Sits between the value and footer; no margin so the .stat-card's
   gap controls spacing. */
.stat-card-desc {
  font-size: var(--text-sm);
  line-height: var(--lh-md);
  color: var(--text-muted);
}

/* Size variants */
.stat-card--sm { padding: var(--space-16); gap: var(--space-4); }
.stat-card--sm .stat-card-value {
  font-size: var(--text-h3);
  line-height: 1;
}
.stat-card--lg .stat-card-value {
  /* Hero KPI moment — display (56), well above the h2-lg (36) default so
     --lg reads as a clear marquee number. Bump tracking from
     --tracking-tighter (-4%) to --tracking-crunch (-5%) so the digits hug
     closer and read more confident. */
  font-size: var(--text-display);
  line-height: 1;
  letter-spacing: var(--tracking-crunch);
}
/* Tone variants — tint the border + inner halo to match the series */
.stat-card--pos {
  border-color: color-mix(in oklch, var(--success) 28%, transparent);
  box-shadow: 0 0 0 1px color-mix(in oklch, var(--success) 8%, transparent);
}
.stat-card--neg {
  border-color: color-mix(in oklch, var(--danger) 28%, transparent);
  box-shadow: 0 0 0 1px color-mix(in oklch, var(--danger) 8%, transparent);
}

/* ── Accent tint modifiers — swap the --stat-card-accent token so
   the radial-gradient variant and any accent-dependent treatment
   shift together. Use alongside --grad-* below. ───────────── */
.stat-card--accent2 { --stat-card-accent: var(--accent-secondary); }
.stat-card--accent3 { --stat-card-accent: var(--accent-tertiary); }

/* ── Visual-background variants — mirror the .tile language
   so stats and tiles can live in the same grid and feel like
   one family. All variants are purely decorative — they don't
   alter the component's slot structure or interaction model.
   Use sparingly; over-using gradient stats flattens the data
   hierarchy (every metric looks equally important).
   ──────────────────────────────────────────────────────── */

/* --glass — frosted backdrop-blur panel. Use over ambient glows,
   on photo-backed heroes, or when the stat sits on a busy surface.
   Pairs naturally with --accent2/--accent3 via the border tint. */
.stat-card--glass {
  background: color-mix(in oklch, var(--glass-base, #13151D) 68%, transparent);
  border-color: color-mix(in oklch, var(--stat-card-accent) 22%, transparent);
  -webkit-backdrop-filter: blur(24px);
  backdrop-filter: blur(24px);
  box-shadow:
    inset 0 0 0 1px color-mix(in oklch, white 6%, transparent),
    0 1px 0 0 color-mix(in oklch, white 4%, transparent);
}

/* --grad-brand — subtle 2-stop blue → teal wash at 135°.
   Text stays fully legible; the gradient is an atmosphere, not a fill. */
.stat-card--grad-brand {
  background: linear-gradient(135deg,
    color-mix(in oklch, var(--accent-primary) 10%, transparent) 0%,
    color-mix(in oklch, var(--accent-secondary) 10%, transparent) 100%);
  border-color: color-mix(in oklch, var(--accent-primary) 16%, transparent);
}

/* --grad-rich — 3-stop low-alpha gradient pulling all three accents. */
.stat-card--grad-rich {
  background: linear-gradient(135deg,
    color-mix(in oklch, var(--accent-primary) 12%, transparent) 0%,
    color-mix(in oklch, var(--accent-secondary) 8%, transparent) 50%,
    color-mix(in oklch, var(--accent-tertiary) 10%, transparent) 100%);
  border-color: color-mix(in oklch, var(--accent-primary) 18%, transparent);
}

/* --grad-radial — ambient glow anchored top-right. Shifts with
   --stat-card-accent, so combine with --accent2/--accent3 for per-card hue. */
.stat-card--grad-radial {
  background:
    radial-gradient(ellipse 85% 65% at 100% 0%,
      color-mix(in oklch, var(--stat-card-accent) 18%, transparent) 0%,
      transparent 70%),
    color-mix(in oklch, var(--stat-card-accent) 3%, transparent);
  border-color: color-mix(in oklch, var(--stat-card-accent) 14%, transparent);
}

/* --grad-bold — full-saturation brand gradient fill with white
   text. Reserved for the single hero KPI on a page — the stat
   itself is the visual moment, not just a container. Pair with
   one solid neighbour so the bold card reads as the anchor. */
.stat-card--grad-bold {
  background: var(--grad-brand);
  border-color: transparent;
  color: #fff;
}
.stat-card--grad-bold .stat-card-eyebrow,
.stat-card--grad-bold .stat-card-label { color: rgba(255,255,255,0.72); }
.stat-card--grad-bold .stat-card-value { color: #fff; }
.stat-card--grad-bold .stat-card-unit { color: rgba(255,255,255,0.72); }

/* --grad-bold-rich — 4-stop rich brand gradient fill. Ultra-hero. */
.stat-card--grad-bold-rich {
  background: var(--grad-brand-rich);
  border-color: transparent;
  color: #fff;
}
.stat-card--grad-bold-rich .stat-card-eyebrow,
.stat-card--grad-bold-rich .stat-card-label { color: rgba(255,255,255,0.72); }
.stat-card--grad-bold-rich .stat-card-value { color: #fff; }
.stat-card--grad-bold-rich .stat-card-unit { color: rgba(255,255,255,0.72); }

/* Bold-variant delta chips — white-on-white-translucent (Apr 2026).
   The original design kept semantic green/red on bold under the
   premise "contrast against the gradient is acceptable" — but the
   14% success/danger tint vanishes against the saturated brand
   gradient and the green/red text fails contrast. Two equally bad
   alternatives — saturating the chip (steals focus from the value)
   or outlining it (visually anemic next to the heroic value) —
   pushed the design to a third path: drop semantic colour on bold
   and let the +/- prefix + arrow direction (▲/▼) carry sentiment.
   Same pattern Stripe and Linear use on gradient hero KPIs. The
   chip stays a clear, lifted pill; the value stays the anchor. */
.stat-card--grad-bold .chip-delta-pos,
.stat-card--grad-bold .chip-delta-neg,
.stat-card--grad-bold .chip-delta-neutral,
.stat-card--grad-bold-rich .chip-delta-pos,
.stat-card--grad-bold-rich .chip-delta-neg,
.stat-card--grad-bold-rich .chip-delta-neutral {
  background: rgba(255, 255, 255, 0.18);
  color: #fff;
  /* Arrow inherits white via currentColor — no override needed. */
}

/* Light-mode specificity bump — page.css's [data-theme="light"] .stat-card
   rules load after styles.css; double the class to reach (0,2,0) so
   variants win. Mirrors the pattern used by .tile--grad-* above. */
[data-theme="light"] .stat-card.stat-card--glass {
  background: color-mix(in oklch, white 72%, transparent);
  border-color: color-mix(in oklch, var(--stat-card-accent) 22%, transparent);
}
[data-theme="light"] .stat-card.stat-card--grad-brand {
  background: linear-gradient(135deg,
    color-mix(in oklch, var(--accent-primary) 10%, transparent) 0%,
    color-mix(in oklch, var(--accent-secondary) 10%, transparent) 100%);
  border-color: color-mix(in oklch, var(--accent-primary) 22%, transparent);
}
[data-theme="light"] .stat-card.stat-card--grad-rich {
  background: linear-gradient(135deg,
    color-mix(in oklch, var(--accent-primary) 12%, transparent) 0%,
    color-mix(in oklch, var(--accent-secondary) 8%, transparent) 50%,
    color-mix(in oklch, var(--accent-tertiary) 10%, transparent) 100%);
  border-color: color-mix(in oklch, var(--accent-primary) 22%, transparent);
}
[data-theme="light"] .stat-card.stat-card--grad-radial {
  background:
    radial-gradient(ellipse 85% 65% at 100% 0%,
      color-mix(in oklch, var(--stat-card-accent) 18%, transparent) 0%,
      transparent 70%),
    color-mix(in oklch, var(--stat-card-accent) 4%, transparent);
  border-color: color-mix(in oklch, var(--stat-card-accent) 18%, transparent);
}
/* Bold variants read the same in both modes — background is the full
   brand gradient, text is always white. No light-mode override needed. */

/* ── Chip — universal pill primitive ─────────────────────────────
   Base + tones + sizes + modifiers, all in one family. Replaces the
   former .chip-pill (small monospace card-tier label) and .scope-chip
   (large dot-led proposal pill) — both folded in as compositions of
   the same chip primitive at different sizes / tones / modifiers.

   Anatomy:
     <span class="chip chip-accent1">             — default · filled
     <span class="chip chip-accent1 chip--md">   — md / lg / xl size
     <span class="chip chip-accent1 chip--outline"> — outline modifier
     <span class="chip chip-accent1">             — icon-left
       <svg class="chip-icon">…</svg> Active
     </span>
     <span class="chip chip-accent2">             — icon-right (or both)
       Open <svg class="chip-icon">…</svg>
     </span>

   The .chip-delta-* family below extends this with semantic +/− tones
   tuned for numeric content (tabular-nums + arrow slot). It composes
   with the same chip--md/lg/xl size scale as the rest of the family —
   no chip-delta-scoped size variants. */
/* Heights step +8 on the spacing grid (24 / 32 / 40 / 48), one tier
   under the button ladder. Padding is horizontal-only — `min-height`
   drives vertical so the chip locks to the grid regardless of how
   the line-height token resolves. align-items: center vertically
   centers the label inside the available space. */
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-4);
  padding: 0 var(--space-12);
  min-height: var(--space-24);
  font-family: var(--font-sans);                     /* defensive — <button> UA default is Arial; render brand Figtree if ever given a text label / ::after glyph */
  border: 1px solid transparent;
  border-radius: var(--r-full);
  font-size: var(--text-caption);
  line-height: var(--lh-caption);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-normal);
  white-space: nowrap;
}

/* Tones — filled (default). Each tone sets bg + color from a single
   palette / semantic token at 12% bg + full-strength text. Light-mode
   override at the bottom of this block tunes bg opacity. */
.chip-accent1 { background: color-mix(in oklch, var(--accent-primary) 12%, transparent);   color: var(--accent-primary); }
.chip-accent2 { background: color-mix(in oklch, var(--accent-secondary) 12%, transparent); color: var(--accent-secondary); }
.chip-accent3 { background: color-mix(in oklch, var(--accent-tertiary) 12%, transparent);  color: var(--accent-tertiary); }
.chip-success { background: color-mix(in oklch, var(--success) 12%, transparent);          color: var(--success); }
.chip-danger  { background: color-mix(in oklch, var(--danger) 12%, transparent);           color: var(--danger); }
.chip-warn    { background: color-mix(in oklch, var(--warn) 12%, transparent);             color: var(--warn); }
.chip-info    { background: color-mix(in oklch, var(--info) 12%, transparent);             color: var(--info); }
.chip-neutral { background: color-mix(in oklch, var(--text-primary) 8%, transparent);      color: var(--text-primary); }
/* Muted — the quiet sibling of neutral: same grey fill family, but a notch
   lighter and muted-ink text (--text-muted) so it reads as de-emphasized /
   inactive (terminal states, secondary metadata). Outline + bare compose via
   currentColor, so .chip-muted.chip--outline paints a soft grey border. */
.chip-muted   { background: color-mix(in oklch, var(--text-primary) 6%, transparent);      color: var(--text-muted); }

/* Chrome-axis modifiers — `.chip--outline` and `.chip--bare`. The two
   are mutually exclusive on the chrome axis (filled / outline / bare).

   Both are anchored under `.chip` (`.chip.chip--outline` / `.chip.chip--bare`)
   so their specificity is (0,2,0) — needed to beat the late-declared
   delta-tone rules (`.chip-delta-pos` etc., at (0,1,0)) which otherwise
   would re-paint the bg by source order. Without the bump, bare on a
   delta chip silently keeps its tone-tinted fill (latent .chip--outline
   bug, surfaced May 2026 when adding .chip--bare).

   Outline — drops the bg, paints the border in `currentColor` so it
   picks up the tone automatically. Composes with any tone:
   `<span class="chip chip-accent1 chip--outline">`. */
.chip.chip--outline {
  background: transparent;
  border-color: currentColor;
}

/* Bare — drops every chrome dimension (bg, border, padding,
   min-height). The chip reads as inline tone-coloured text.

   Canonical use case: frameless `.chip-delta-*` for inline price-readouts
   where the chip sits beside muted time labels and shouldn't read as a
   separate UI element ("↑0.13% 5m  ↑0.08% 1h  ↓0.67% 24h"). Composes
   with any tone — `chip-success` / `chip-danger` / `chip-accent1` etc.
   — for inline status indicators on dense rows.

   Cascade with size modifiers: bare zeroes chrome (padding + min-height)
   at every tier, but the tier's `font-size`, `line-height`, and `gap`
   pass through. `.chip.chip-delta-pos.chip--md.chip--bare` renders at
   14 px tone-coloured text with an 8 px arrow gap, no chrome around it.

   Border is dropped explicitly (`border: 0`, not just
   `border-color: transparent`) because bare chips never host an
   `.is-active` ring — the slot is dead weight and would push the chip
   off baseline against the surrounding text. */
.chip.chip--bare {
  background: transparent;
  border: 0;
  padding: 0;
  min-height: 0;
  gap: var(--space-2);
}

/* Bare-chip gap tracks type-density at every tier. Filled chips step
   gap 4 / 8 / 8 / 8 across default / md / lg / xl — that spacing reads
   well when there's chrome around the chip. Bare drops the chrome, so
   the icon-to-value gap should hug tighter: 2 / 4 / 4 / 4. The same
   visual relationship between icon-size and gap, scaled down one tier
   to compensate for the missing padding. */
.chip.chip--md.chip--bare,
.chip.chip--lg.chip--bare,
.chip.chip--xl.chip--bare {
  gap: var(--space-4);
}

/* Icon slot — leading or trailing SVG glyph. Position is determined
   by DOM order: place the SVG before the text for icon-left, after
   for icon-right (or use both — Lucide stroke-2 outline glyphs read
   well at every size). Color flows through `color: inherit` so the icon
   always picks up the chip's tone. Sizes step with the chip tier so the
   glyph stays proportional to the text.

   The `.chip` qualifier lifts specificity to (0,2,0) so a container that
   styles trailing svgs (e.g. `.list-item__trail svg` greys + 20px-sizes
   them) can't override the chip's own icon colour/size. .chip-icon is only
   ever used inside a .chip, so this changes nothing standalone. */
.chip .chip-icon {
  width: var(--space-12);
  height: var(--space-12);
  flex-shrink: 0;
  color: inherit;
}
.chip--md .chip-icon { width: var(--space-16); height: var(--space-16); }
.chip--lg .chip-icon { width: var(--space-16); height: var(--space-16); }
.chip--xl .chip-icon { width: var(--space-20); height: var(--space-20); }

/* Window slot — supplementary muted text inside a chip. The chip's
   tone color sets the value's color via inheritance; .chip-window
   breaks that inheritance with its own muted ink, signalling that the
   trailing text is annotation, not value.

   Canonical use: bare delta chips for inline price-readouts
   ("↑0.07% 5m") where the time-interval sits next to the value as
   annotation. Reusable for any chip composition that needs a secondary
   muted label inside the chip's box — token chips with "Active", chain
   badges with chain count, status chips with timestamp, etc.

   No font-size — inherits the chip's tier scale so window text grows
   with the chip across default / md / lg / xl. Weight is one tier
   below the value (medium 500 vs the chip's semibold 600) so the
   value still leads visually. Position relative to siblings is owned
   by the chip family's gap (2px bare, 4 / 8 / 8 / 8 filled) — no
   margin on .chip-window itself. */
.chip-window {
  color: var(--text-muted);
  font-weight: var(--fw-medium);
}

/* Sizes — default / md / lg / xl. Each tier steps the type up the
   scale and bumps padding to match. Weight escalates 600 → 700 at
   lg/xl so larger chips read as a heavier statement. The same scale
   covers tone chips, scope-chip use cases (xl + dot), pricing-tier
   labels (former chip-pill: md + outline), and delta indicators.

     default  12px · 4×12 pad  · 4 gap  · ~22px tall
                  → stat-card footers, dense rows, status pills
     --md     14px · 8×16 pad  · 8 gap  · ~30px tall
                  → larger tile footers, card metadata
     --lg     16px · 8×24 pad  · 8 gap  · ~40px tall (700 wt)
                  → hero tile chips, marketing accent labels
     --xl     20px · 12×24 pad · 8 gap  · ~52px tall (700 wt)
                  → proposal scope chips, banner / print heroes

   12px is the floor — chips rendered below it lose legibility on
   2× retina print export (11px Figma → ~5pt at US Letter).
   Print-safe by design across all four tiers. */
.chip--md {
  padding: 0 var(--space-16);
  min-height: var(--space-32);
  gap: var(--space-8);
  font-size: var(--text-sm);
  line-height: var(--lh-sm);
}
.chip--lg {
  padding: 0 var(--space-24);
  min-height: var(--space-40);
  gap: var(--space-8);
  font-size: var(--text-body);
  line-height: var(--lh-body);
  font-weight: var(--fw-bold);
}
.chip--xl {
  padding: 0 var(--space-24);
  min-height: var(--space-48);
  gap: var(--space-8);
  font-size: var(--text-body-lg);
  line-height: var(--lh-body-lg);
  font-weight: var(--fw-bold);
}

/* Light-mode bg-opacity tuning — the 12% tint reads slightly heavier
   on white surfaces; soften to 10% (8% → 6% for neutral) so chips
   feel grounded without overpowering the row. Scoped with
   :not(.chip--outline):not(.chip--bare) so the chrome-axis modifiers
   (single class, specificity 10) still win over the attribute+class
   light-mode selector — without the :not(), light-mode tone backgrounds
   would re-paint over the outline / bare variants' transparent fill. */
[data-theme="light"] .chip-accent1:not(.chip--outline):not(.chip--bare) { background: color-mix(in oklch, var(--accent-primary) 10%, transparent); }
[data-theme="light"] .chip-accent2:not(.chip--outline):not(.chip--bare) { background: color-mix(in oklch, var(--accent-secondary) 10%, transparent); }
[data-theme="light"] .chip-accent3:not(.chip--outline):not(.chip--bare) { background: color-mix(in oklch, var(--accent-tertiary) 10%, transparent); }
[data-theme="light"] .chip-success:not(.chip--outline):not(.chip--bare) { background: color-mix(in oklch, var(--success) 10%, transparent); }
[data-theme="light"] .chip-danger:not(.chip--outline):not(.chip--bare)  { background: color-mix(in oklch, var(--danger) 10%, transparent); }
[data-theme="light"] .chip-warn:not(.chip--outline):not(.chip--bare)    { background: color-mix(in oklch, var(--warn) 10%, transparent); }
[data-theme="light"] .chip-info:not(.chip--outline):not(.chip--bare)    { background: color-mix(in oklch, var(--info) 10%, transparent); }
[data-theme="light"] .chip-neutral:not(.chip--outline):not(.chip--bare) { background: color-mix(in oklch, var(--text-primary) 6%, transparent); }
[data-theme="light"] .chip-muted:not(.chip--outline):not(.chip--bare)   { background: color-mix(in oklch, var(--text-primary) 5%, transparent); }

/* ── Delta chips — semantic +/− tones with tabular-nums ──────────────
   Used to show % change or absolute delta next to a metric. Same
   visual recipe as .chip-success / .chip-danger / .chip-neutral, but
   with `font-variant-numeric: tabular-nums` so digits align in dense
   tables.

     <span class="chip chip-delta-pos">
       <svg class="chip-icon" …>arrow-up</svg> +12.4%
     </span>
     <span class="chip chip--md chip-delta-neg">−2.1%</span>

   Size scale comes from the chip family — no delta-scoped size variants.
   The arrow uses `.chip-icon` (the generic chip-family icon slot), which
   scales 12 / 16 / 16 / 20 px across the size tiers automatically.

   Migration note (May 2026, v19): `.chip-delta-arrow` retired. The
   delta-scoped arrow slot was an exact duplicate of `.chip-icon` (same
   sizes, same flex-shrink) carrying its own 10×10 filled-triangle path.
   Migrated to `.chip-icon` + inlined Lucide `arrow-up.svg` /
   `arrow-down.svg` at stroke-width 2 so deltas source their iconography
   from the DS icon library like every other chip. Old name appears in
   imagery.md spec history and this migration-note only. */
.chip-delta-pos     { background: color-mix(in oklch, var(--success) 14%, transparent); color: var(--success); font-variant-numeric: tabular-nums; }
.chip-delta-neg     { background: color-mix(in oklch, var(--danger) 14%, transparent);  color: var(--danger);  font-variant-numeric: tabular-nums; }
.chip-delta-neutral { background: color-mix(in oklch, var(--text-primary) 8%, transparent); color: var(--text-muted); font-variant-numeric: tabular-nums; }


/* ── .chip-edit — inline-editable chip primitive ────────────────────────
   A chip that toggles between a static label (the "click to edit" cue)
   and an editable input. Click the label → swaps to input mode with the
   input focused + selected; Enter / Escape / blur → swaps back to the
   label. Composes any chip tone (.chip-neutral, .chip-success, etc.);
   the tone applies to both label and edit states.

   The label IS the affordance — its copy ("Custom", "Set limit",
   "Edit name") tells the user the chip is editable, replacing the older
   pencil-icon pattern. A button-rooted label gives focus + keyboard
   activation for free.

   Anatomy:
     .chip-edit                   — wrapper (compose with .chip + tone)
     .chip-edit__label            — button. Visible at rest; hidden in edit mode
     .chip-edit__field            — visible only in edit mode (hidden at rest)
     .chip-edit__field__input     — the editable input. Hugs content via
                                    field-sizing: content; 5ch fallback for
                                    older browsers
     .chip-edit__field__suffix    — optional unit-mark ("%", "px", "/sec")

   Wiring: data-chip-edit on the root opts in. shared.js → initChipEdit()
   binds click on the label to activate, and Enter / Escape / blur on the
   input to deactivate. Consumers wire their own input event handlers
   (data-chip-edit-input) for value semantics — the primitive itself is
   value-agnostic.

   Catalog: design-system/index.html#chip-edit
   ----------------------------------------------------------------------- */
.chip-edit {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  cursor: text;
  /* No min-width — the chip hugs content so the resting state reads as
     a peer of any other .chip in a row (Market / +1% / Custom all look
     like one family). The slight width change when the input takes over
     the label is acceptable; the alternative (locking a min-width based
     on the label) makes the editable chip visibly wider than its preset
     siblings at rest, which broke the family read. */
  transition: background-color 120ms ease;
}

/* Edit-state highlight — bg only. No outline / focus ring; the user-
   facing design intent is "chip, not button," and the outline was
   reading as button chrome. The deeper bg (color-mix currentColor 22%)
   is the sole edit-state cue — visible enough that the chip stands
   apart from its preset siblings at rest, quiet enough not to compete
   with the chip's tone tier.

   Note the layered selector — pure specificity bump. The light-mode
   chip-tone override `[data-theme="light"] .chip-neutral:not(.chip
   --outline)` has specificity (0, 3, 0), and lives later in the
   stylesheet than this rule. A naive `.chip-edit:focus-within` (0, 2, 0)
   loses outright; `.chip.chip-edit:focus-within` (0, 3, 0) ties but
   loses by source-order. Adding `[data-chip-edit]` takes the selector to
   (0, 4, 0) which wins unconditionally. Same general lesson — any
   primitive that wants to override a theme-scoped tone rule needs
   specificity above (0, 3, 0). */
.chip.chip-edit:focus-within[data-chip-edit] {
  background: color-mix(in oklch, currentColor 22%, transparent);
}

.chip-edit__label {
  /* `appearance: none` strips the user-agent button chrome (raised on
     Safari/Mac, OS-level focus rings, font drift). Without it, the
     label reads as a "default button" rather than a chip-styled clickable
     element. The rest restores inherit-from-chip semantics — the label
     looks identical to a static .chip's text content. No hover affordance
     by design: the chip's overall hover feedback (the standard chip-tone
     hover) is enough, and a label-scoped underline read as a competing
     visual layer rather than reinforcing editability. */
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
  border: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
  padding: 0;
  margin: 0;
  outline: none;
}

.chip-edit__field {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

/* `hidden` attribute → display:none. Necessary because the rule above
   sets `display: inline-flex`, which would otherwise win over the user-
   agent stylesheet's `[hidden] { display: none }`. Same pattern needed
   for any flex container that toggles visibility via [hidden]. */
.chip-edit__field[hidden] {
  display: none;
}

.chip-edit__input {
  width: 5ch;
  field-sizing: content;
  border: 0;
  background: transparent;
  font: inherit;
  color: inherit;
  font-variant-numeric: tabular-nums;
  text-align: center;
  outline: none;
  padding: 0;
  appearance: textfield;
  -moz-appearance: textfield;
}

.chip-edit__suffix {
  opacity: 0.75;
}


/* ── .chip-avatar — standalone leading-avatar pill primitive ────────────
   Independent primitive (not a `.chip` modifier as of May 2026 v2) for
   avatar-led identities: token + chain (.avatar-tandem), single chain,
   partner, wallet. Its CSS is fully decoupled from the `.chip` family
   so the two can drift without affecting each other.

   The avatar slot consumes `--avatar-size` from the chip-avatar root;
   inheritance cascades through `.avatar-tandem` (wrapper), inner
   `.avatar` (width/height), `.avatar-badge` (40% of size), and the
   tandem's cutout-mask radial-gradient geometry. One variable → every
   avatar primitive sized correctly.

   Markup:
     <span class="chip-avatar chip-avatar-neutral chip-avatar--lg">
       <span class="avatar-tandem">
         <img class="avatar avatar--circle" src="…" alt="">
         <img class="avatar-badge avatar-badge--br" src="…" alt="">
       </span>
       USDC
     </span>

   The consumer DOESN'T size the inner avatars (no `.avatar--sm`, no
   `.avatar-tandem--lg`) — the chip-avatar tier governs through
   inheritance. If the consumer DOES add a size modifier on the inner
   avatar / tandem, it wins (local var wins over inherited).

   Interactive use — render the host as <button>. The button.chip-avatar
   rule below adds cursor + tone-aware hover.

   Size ladder — canonical sm / default / lg / xl per CLAUDE.md's
   Component Sizing Ladders. Heights step on the 4 px grid; the default
   tier is the canonical swap-widget size (40 px) — May 2026 v3 raised
   the default from 24 px to 40 px after the 24 px tier proved too
   small for tandem badges (6.4 px badge below practical legibility):
     --sm      32px tall → avatar 20  → badge 8
     default   40px tall → avatar 28  → badge 11.2   ← canonical
     --lg      48px tall → avatar 32  → badge 12.8
     --xl      56px tall → avatar 36  → badge 14.4

   Single avatars (no badge) work at every tier; tandem badges read
   confidently from --sm and up.

   Padding is asymmetric per tier — avatar sits flush-ish on the left
   (4 / 8 / 12 / 16) while the right pad tightens around the label so
   the chip-avatar "hugs" its content. Right pad ≈ chip's nominal
   horizontal padding ÷ 1.5 — enough breathing room for the label but
   not so loose the pill feels airy. */
.chip-avatar {
  /* Layout — default tier is the canonical 40 px / 28 px-avatar size. */
  display: inline-flex;
  align-items: center;
  gap: var(--space-8);
  padding: 0 var(--space-16) 0 var(--space-8);
  min-height: var(--space-40);
  border: 1px solid transparent;
  border-radius: var(--r-full);

  /* Typography */
  font-family: inherit;
  font-size: var(--text-body);
  line-height: var(--lh-body);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-normal);
  white-space: nowrap;

  /* Avatar size cascade — re-pinned per size modifier below. */
  --avatar-size: var(--space-28);
}

/* Tones — filled by default (color-mix recipe). Mirrors the chip
   family's tone vocabulary 1:1 but independently styled. Tweaks here
   do NOT affect standard .chip-neutral / .chip-accent* consumers. */
.chip-avatar-neutral { background: color-mix(in oklch, var(--text-primary) 8%, transparent);     color: var(--text-primary); }
.chip-avatar-accent1 { background: color-mix(in oklch, var(--accent-primary) 12%, transparent);  color: var(--accent-primary); }
.chip-avatar-accent2 { background: color-mix(in oklch, var(--accent-secondary) 12%, transparent); color: var(--accent-secondary); }
.chip-avatar-accent3 { background: color-mix(in oklch, var(--accent-tertiary) 12%, transparent); color: var(--accent-tertiary); }
.chip-avatar-success { background: color-mix(in oklch, var(--success) 12%, transparent);         color: var(--success); }
.chip-avatar-warn    { background: color-mix(in oklch, var(--warn) 12%, transparent);            color: var(--warn); }
.chip-avatar-danger  { background: color-mix(in oklch, var(--danger) 12%, transparent);          color: var(--danger); }

/* Outline modifier — drops bg, border tracks the tone via currentColor. */
.chip-avatar--outline {
  background: transparent;
  border-color: currentColor;
}

/* Sizes — --sm / default / --lg / --xl. Each tier re-pins:
   • --avatar-size (cascades down to avatar primitives)
   • padding (asymmetric left/right per the hug rule above)
   • min-height + gap + font-size/weight (typography ladder) */
.chip-avatar--sm {
  --avatar-size: var(--space-20);
  padding: 0 var(--space-12) 0 var(--space-4);
  min-height: var(--space-32);
  gap: var(--space-8);
  font-size: var(--text-sm);
  line-height: var(--lh-sm);
  font-weight: var(--fw-bold);
}
.chip-avatar--lg {
  --avatar-size: var(--space-32);
  padding: 0 var(--space-20) 0 var(--space-12);
  min-height: var(--space-48);
  gap: var(--space-8);
  font-size: var(--text-body-lg);
  line-height: var(--lh-body-lg);
  font-weight: var(--fw-bold);
}
.chip-avatar--xl {
  --avatar-size: var(--space-36);
  padding: 0 var(--space-24) 0 var(--space-16);
  min-height: var(--space-56);
  gap: var(--space-12);
  font-size: var(--text-h3);
  line-height: var(--lh-h3);
  font-weight: var(--fw-bold);
}

/* Light-mode tone-opacity tuning — 12% reads heavier on white surfaces;
   soften to 10% (8% → 6% for neutral). Scoped with
   :not(.chip-avatar--outline) so the outline modifier still wins. */
[data-theme="light"] .chip-avatar-neutral:not(.chip-avatar--outline) { background: color-mix(in oklch, var(--text-primary) 6%, transparent); }
[data-theme="light"] .chip-avatar-accent1:not(.chip-avatar--outline) { background: color-mix(in oklch, var(--accent-primary) 10%, transparent); }
[data-theme="light"] .chip-avatar-accent2:not(.chip-avatar--outline) { background: color-mix(in oklch, var(--accent-secondary) 10%, transparent); }
[data-theme="light"] .chip-avatar-accent3:not(.chip-avatar--outline) { background: color-mix(in oklch, var(--accent-tertiary) 10%, transparent); }
[data-theme="light"] .chip-avatar-success:not(.chip-avatar--outline) { background: color-mix(in oklch, var(--success) 10%, transparent); }
[data-theme="light"] .chip-avatar-warn:not(.chip-avatar--outline)    { background: color-mix(in oklch, var(--warn) 10%, transparent); }
[data-theme="light"] .chip-avatar-danger:not(.chip-avatar--outline)  { background: color-mix(in oklch, var(--danger) 10%, transparent); }

/* Borderless modifier — no chrome at rest; the tone fill REVEALS on hover,
   so the pill silhouette (its actual footprint) only appears on interaction.
   Same "ghost" pattern as .btn-borderless / .seg--borderless: it suppresses
   the resting tone bg + border and leaves the interactive recipe untouched —
   the existing button.chip-avatar:hover (currentColor @ 14%) / :active (@ 20%)
   rules below paint the tone wash exactly as a filled chip-avatar would, so
   the reveal needs no extra rule. Pairs with a <button> host (the hover IS
   the affordance); on a static <span> it's simply chrome-less.

   Specificity — the base rule (0,0,1,0) is declared after the per-tone resting
   rules so it suppresses them in dark mode; the [data-theme="light"] sibling
   ties the per-tone light overrides at (0,0,3,0) and wins by later source
   order. Both sit BEFORE the hover rules, so hover (also (0,0,3,0) in light,
   (0,0,2,0) in dark) wins the reveal. Mirrors the --locked specificity shape. */
.chip-avatar--borderless {
  background: transparent;
  border-color: transparent;
}
[data-theme="light"] .chip-avatar.chip-avatar--borderless {
  background: transparent;
}

/* Placeholder-prompt typography — when the chip carries .avatar--placeholder
   or .avatar-tandem--placeholder, the label reads as a PROMPT ("Select")
   instead of a value ("USDC"). Keep the bold weight so the label mirrors
   the typographic SHAPE of a real token symbol (same pill silhouette
   designers scan against); only shift ink primary → muted so the chip
   doesn't compete with neighbouring real chips. Auto-detected via :has() —
   markup stays clean; the moment a real avatar binds (src lands, placeholder
   spans swap to <img>), the selector stops matching and the chip restores
   full primary ink. Override beats per-tone color via source order at the
   same specificity. */
.chip-avatar:has(.avatar--placeholder),
.chip-avatar:has(.avatar-tandem--placeholder) {
  color: var(--text-muted);
}

/* ── Locked — read-only "fixed value, shown for context" state ─────────
   DISTINCT from a disabled CONTROL (the system's :disabled / [aria-disabled]
   → uniform 0.4-opacity dim, see the .btn-* disabled family). A locked
   chip-avatar shows a committed value the user can SEE but can't change in
   this context — the canonical case is the Modify-limit-order modal's pair
   (modifying an order can't re-pick the tokens). Recipe: drop the tone fill
   → transparent + a quiet hairline outline (the same --border-subtle every
   card outline uses, so the locked pill reads as inert structure), mute the
   label ink, and go fully inert (pointer-events: none). The AVATAR stays at
   full opacity on purpose — the whole point is to show the locked token
   legibly; the transparent fill + muted label + dead cursor carry the
   "can't change this" signal without hurting recognition.

   The base .chip-avatar reserves `border: 1px solid transparent`, so painting
   border-color here costs zero layout shift. The light-mode background needs a
   specificity bump to beat the per-tone light overrides above (which sit at
   (0,0,3,0) via their :not() qualifier) — same shape as the hover / selected
   light-mode siblings; .chip-avatar.chip-avatar--locked ties at (0,0,3,0) and
   wins by later source order. Native button:disabled is folded in so a real
   <button class="chip-avatar" disabled> gets the same read-only treatment. */
.chip-avatar--locked,
button.chip-avatar:disabled {
  background: transparent;
  border-color: var(--border-subtle);
  color: var(--text-muted);
  pointer-events: none;
  cursor: default;
}
[data-theme="light"] .chip-avatar.chip-avatar--locked,
[data-theme="light"] button.chip-avatar:disabled {
  background: transparent;
}

/* ── Interactive states — when chip-avatar is rendered as <button> ────
   Matches the `.list-item--interactive` recipe used on the token list
   yesterday so chip-avatars and list rows feel like one system: tone-
   deepened hover + active, accent-primary outline on focus, palette-
   aware selected state. The hover/active deepen via `currentColor`
   mix (14% / 20%) rather than the neutral `--overlay-hover` token —
   chip-avatar already has a tone bg at rest, so a flat overlay would
   wash the tone out. The currentColor pattern works for every tone
   with one rule, no per-tone overrides.

   Specificity discipline — the light-mode tone-opacity overrides
   above carry specificity (0,0,3,0) because of the `:not(--outline)`
   qualifier, which beats a plain `.chip-avatar:hover` (0,0,2,0). To
   keep hover / active / selected visible in light mode, each rule
   gets a `[data-theme="light"]`-prefixed sibling that ties at
   (0,0,3,0) and wins via source order. Dark mode doesn't have the
   per-tone override, so the base rule wins there at (0,0,2,0). */
button.chip-avatar {
  cursor: pointer;
  transition: background-color .12s ease;
}
:where(button).chip-avatar:hover,
[data-theme="light"] :where(button).chip-avatar:hover {
  background: color-mix(in oklch, currentColor 14%, transparent);
}
:where(button).chip-avatar:active,
[data-theme="light"] :where(button).chip-avatar:active {
  background: color-mix(in oklch, currentColor 20%, transparent);
}
:where(button).chip-avatar:focus-visible {
  outline: 2px solid color-mix(in oklch, var(--accent-primary) 50%, transparent);
  outline-offset: 2px;
}

/* ── Selected — persistent "currently picked" state ────────────────────
   System-wide default matching `.list-item--selected`: accent-primary
   12% bg + primary text colour, so the selected hue follows the active
   palette (LI.FI 1.0 → blue, Jumper 2.0 → its primary, etc.). Applies
   to both naming conventions:
     .chip-avatar--selected    — BEM modifier (catalog, marketing)
     .chip-avatar.is-selected  — state class (JS toggles this; product)
   Light-mode variants bump specificity for the same reason as the
   hover rules above. Tying with hover at (0,0,3,0) but later in source
   means selected wins when a cursor hovers a selected chip — exactly
   the behaviour the token-list selected state relies on. */
.chip-avatar--selected,
.chip-avatar.is-selected,
[data-theme="light"] .chip-avatar--selected,
[data-theme="light"] .chip-avatar.is-selected {
  background: color-mix(in oklch, var(--accent-primary) 12%, transparent);
  color: var(--text-primary);
}


/* ── Interactive states — when .chip is rendered as <button> or <a[href]> ──
   Mirrors the .chip-avatar interactive recipe: cursor + tone-aware
   hover/active + accent focus outline. `:where()` keeps specificity
   low (0,0,1,1) so the per-tone resting rules win, and the
   [data-theme="light"] sibling ties with the light-mode tone overrides
   at (0,0,3,1) and wins via source order — same shape as chip-avatar.

   Interactive hosts are <button> and <a[href]>. A static <span class="chip">
   (the catalog default for labels) gets no cursor and no hover — chips
   are chrome by default, interactive only when wrapped in a host element
   that means it. `text-decoration: none` cancels the user-agent <a>
   underline so the chip's tone reads cleanly through link defaults. */
:where(button, a[href]).chip {
  cursor: pointer;
  transition: background-color .12s ease;
  font-family: inherit;
  text-decoration: none;
}
:where(button, a[href]).chip:hover,
[data-theme="light"] :where(button, a[href]).chip:hover {
  background: color-mix(in oklch, currentColor 14%, transparent);
}
:where(button, a[href]).chip:active,
[data-theme="light"] :where(button, a[href]).chip:active {
  background: color-mix(in oklch, currentColor 20%, transparent);
}
:where(button, a[href]).chip:focus-visible {
  outline: 2px solid color-mix(in oklch, var(--accent-primary) 50%, transparent);
  outline-offset: 2px;
}

/* ── .sparkline ────────────────────────────────────────────
   Tiny inline SVG trend. Default 80×24 reads inside a stat card
   footer or a table cell; --sm (56×18) slots into dense tables,
   --lg (120×36) works beside a hero number. Color binds to the
   current text color via `currentColor`, so tone modifiers are
   as simple as `color: var(--success)`. */
.sparkline {
  display: inline-block;
  width: 80px;
  height: 24px;
  color: var(--accent-primary);
  vertical-align: middle;
}
.sparkline--sm { width: var(--space-56); height: var(--space-20); }
.sparkline--lg { width: var(--space-120); height: var(--space-32); }
.sparkline--pos { color: var(--success); }
.sparkline--neg { color: var(--danger); }
.sparkline svg {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}
.sparkline-line {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linejoin: round;
  stroke-linecap: round;
}
.sparkline-area {
  fill: color-mix(in oklch, currentColor 18%, transparent);
  stroke: none;
}
.sparkline-dot {
  fill: currentColor;
}

/* ── .progress ────────────────────────────────────────────
   Linear progress bar. Drive the fill via the `--value` custom
   property (0–100). Semantic variants swap the fill color to
   success / warn / danger — useful for capacity meters. */
.progress {
  position: relative;
  width: 100%;
  height: var(--space-8);
  background: color-mix(in oklch, var(--text-primary) 10%, transparent);
  border-radius: var(--r-full);
  overflow: hidden;
}
.progress-fill {
  height: 100%;
  width: calc(var(--value, 0) * 1%);
  background: var(--accent-primary);
  border-radius: inherit;
  transition: width 0.4s ease;
}
.progress--sm { height: var(--space-4); }
.progress--lg { height: var(--space-12); }
.progress--xl { height: var(--space-16); }
.progress--success .progress-fill { background: var(--success); }
.progress--warn    .progress-fill { background: var(--warn); }
.progress--danger  .progress-fill { background: var(--danger); }
.progress--info    .progress-fill { background: var(--info); }
/* Segmented — multi-stop track. Each child is a fill with its own
   width / color. Good for showing breakdowns like usage per tier. */
.progress--segmented {
  display: flex;
  gap: var(--space-4);
  background: transparent;
}
.progress--segmented .progress-fill {
  width: auto;
  flex: var(--weight, 1) 0 0;
  border-radius: var(--space-4);
}

/* Inline — bar on the left, numeric readout on the right, on ONE row.
   The dense meter for table cells, list rows, and compact dashboards
   (vs. the value-above-bar stacked layout, which suits wider panels).
   The bar flexes to fill; the value hugs the right and reserves a stable
   min-width so the row doesn't reflow as --value animates. Compose any
   .progress size / tone inside — the value type pairs with the bar tier
   via :has() (sm → caption · default → sm · lg → body · xl → body-lg).
   Promoted May 2026 from the page-local .ow-fill in the Orders table. */
.progress-inline {
  display: flex;
  align-items: center;
  gap: var(--space-8);
}
.progress-inline .progress {
  flex: 1 1 auto;
  min-width: 0;
}
.progress-value {
  flex: 0 0 auto;
  min-width: 4ch;
  text-align: right;
  font-size: var(--text-sm);
  line-height: 1;
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}
.progress-inline:has(.progress--sm) .progress-value { font-size: var(--text-caption); }
.progress-inline:has(.progress--lg) .progress-value { font-size: var(--text-body); }
.progress-inline:has(.progress--xl) .progress-value { font-size: var(--text-body-lg); }

/* ── .progress-ring ────────────────────────────────────────
   Circular progress — the linear .progress's circular sibling.
   Single 0–100 value rendered as an SVG arc on top of a quiet
   track. Same vocabulary as .progress: sm / default / lg / xl
   sizes, semantic tones, gradient variants, and an optional
   center label slot.

   Why SVG (not conic-gradient): conic-gradient draws a clean
   pie but no rounded line cap — the arc's leading edge cuts
   square at the gradient stop. Stroke-based arcs get the
   rounded cap and play nicely with brand-gradient strokes via
   a <linearGradient> def.

   The trick that keeps the math simple: pathLength="100" on
   the <circle> normalises the path's total length to 100 —
   stroke-dasharray is just 100 and stroke-dashoffset is
   (100 - --value). No π, no per-size dasharray recompute.

   Markup:
     <div class="progress-ring" style="--value: 72;">
       <svg class="progress-ring__svg" viewBox="0 0 100 100" aria-hidden="true">
         <circle class="progress-ring__track" cx="50" cy="50" r="45" pathLength="100"/>
         <circle class="progress-ring__fill"  cx="50" cy="50" r="45" pathLength="100"/>
       </svg>
       <div class="progress-ring__label">72%</div>
     </div>

   `--value` drives the arc. The label slot is optional — drop
   any content (percentage, glyph, avatar) or omit entirely. */
.progress-ring {
  --size: 80px;
  --stroke-w: 8;
  --value: 0;
  --track-color: color-mix(in oklch, var(--text-primary) 10%, transparent);
  --fill-color: var(--accent-primary);

  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--size);
  height: var(--size);
  flex-shrink: 0;
}
.progress-ring__svg {
  width: 100%;
  height: 100%;
  transform: rotate(-90deg);
}
.progress-ring__track,
.progress-ring__fill {
  fill: none;
  stroke-width: var(--stroke-w);
}
.progress-ring__track { stroke: var(--track-color); }
.progress-ring__fill {
  stroke: var(--fill-color);
  stroke-linecap: round;
  stroke-dasharray: 100;
  stroke-dashoffset: calc(100 - var(--value, 0));
  transition: stroke-dashoffset 0.4s ease;
}

/* Sizes — match the sm / default / lg / xl ladder.
   stroke-w is tuned per size for a visually consistent stroke
   weight (the value is in viewBox units 0–100, so larger sizes
   take a smaller stroke-w to net the same painted pixels). */
.progress-ring--sm { --size: 48px;  --stroke-w: 10;  }
/* default = 80px / stroke-w 8 — set on the base block above */
.progress-ring--lg { --size: 96px;  --stroke-w: 7;   }
.progress-ring--xl { --size: 128px; --stroke-w: 5.5; }

/* Semantic tones */
.progress-ring--success { --fill-color: var(--success); }
.progress-ring--warn    { --fill-color: var(--warn); }
.progress-ring--danger  { --fill-color: var(--danger); }
.progress-ring--info    { --fill-color: var(--info); }

/* Optional center label. Empty by default. Designers compose
   anything: a percentage, a glyph, an avatar. */
.progress-ring__label {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font: 600 var(--text-body) / 1 var(--font-sans);
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
  text-align: center;
  pointer-events: none;
}
.progress-ring--sm .progress-ring__label { font-size: var(--text-caption); }
.progress-ring--lg .progress-ring__label { font-size: var(--text-body-lg); }
.progress-ring--xl .progress-ring__label { font-size: var(--text-h3); }

/* ── .progress-ring-inline ─────────────────────────────────
   Ring on the left, numeric readout on the right, on ONE row —
   the circular sibling of .progress-inline. The dense, compact
   meter for table cells, list rows, and tight columns where a
   full-width bar wastes horizontal space: the ring is content-
   width (no flex-fill), so the whole component hugs its content
   and the column narrows.

   Sizes pair a small ring Ø with a label tier — and the label
   ladder is IDENTICAL to .progress-inline's value ladder
   (caption 12 · sm 14 · body 16 · body-lg 18), so a ring-inline
   reads at the same value size as the bar-inline it replaces.
   Ring Ø steps +4 on the 4-grid: sm 16 · default 20 · lg 24 · xl 28
   — a compact range tuned for dense inline contexts (table cells,
   rows), not the display sizes of .progress-ring. stroke-w (viewBox
   units) is tuned per tier so the painted arc stays a visually
   consistent ~3 px across the ladder.

   Reuses .progress-value for the readout and the bare .progress-
   ring (with its --value / tone modifiers) for the arc — this
   primitive only owns the inline layout + the small-ring ladder.

   Markup:
     <span class="progress-ring-inline progress-ring-inline--lg">
       <span class="progress-ring progress-ring--success" style="--value: 100;">
         <svg class="progress-ring__svg" viewBox="0 0 100 100" aria-hidden="true">
           <circle class="progress-ring__track" cx="50" cy="50" r="45" pathLength="100"/>
           <circle class="progress-ring__fill"  cx="50" cy="50" r="45" pathLength="100"/>
         </svg>
       </span>
       <span class="progress-value">100%</span>
     </span> */
.progress-ring-inline {
  --pri-ring: 16px;
  --pri-stroke: 16;
  --pri-label: var(--text-caption);
  display: inline-flex;
  align-items: center;
  gap: var(--space-8);
}
.progress-ring-inline .progress-ring {
  --size: var(--pri-ring);
  --stroke-w: var(--pri-stroke);
}
.progress-ring-inline .progress-value {
  font-size: var(--pri-label);
  min-width: 0;
}
/* Three-tier ladder. The default was 20px / --text-sm until June 2026,
   when it shrank to 16 / --text-caption to fix a glyph-vs-ring vertical
   miscentering that read as off-balance at the 20 px tier (digit-heavy
   labels like "37%" rendered with their bbox center aligned to the ring
   center, but the digit MASS sat ~0.5–1 px low against the larger ring).
   At 16 the ring is snug against the digit cap-height so the label and
   ring read as one composed unit. `--sm` was retired in the same sweep —
   it had been 16 + --text-caption (now identical to the base). */
.progress-ring-inline--lg { --pri-ring: 24px; --pri-stroke: 13; --pri-label: var(--text-body); }
.progress-ring-inline--xl { --pri-ring: 28px; --pri-stroke: 11; --pri-label: var(--text-body-lg); }

/* ── .steps ──────────────────────────────────────────────────
   Multi-step navigation / wizard primitive. Renders the user's
   current position in a flow. Seven variants share one state
   API — every item carries data-step-state="complete | current
   | upcoming" — so swapping variants is a wrapper-class change,
   never a per-item rewrite.

   Variants:
     .steps--simple          N horizontal bars + "Step N of M"
     .steps--bullets         Horizontal row of dots, connected
     .steps--bullets-text    Vertical: dot · title · description
     .steps--circles         Horizontal numbered circles + lines
     .steps--circles-text    Top-edge accent + label + title cells
     .steps--panels          Boxed steps with top accent edge
     .steps--panels-border   Boxed steps with left accent edge

   State semantics:
     complete  — finished step; filled accent
     current   — active step; ring / outline accent
     upcoming  — pending; quiet outline / muted

   Per-variant tone: set --steps-tone on the outer .steps element
   (or on the page root) to retint the whole flow. Defaults to
   --accent-primary so steps follow the live brand palette. */
.steps {
  --steps-tone: var(--accent-primary);
  --steps-track: color-mix(in oklch, var(--text-primary) 12%, transparent);
}

/* ── .steps--simple — N bars + caption ───────────────────── */
.steps--simple {
  display: flex;
  flex-direction: column;
  gap: var(--space-12);
}
.steps--simple .steps__bars {
  display: flex;
  gap: var(--space-4);
}
.steps--simple .steps__item {
  flex: 1;
  height: var(--space-4);
  border-radius: var(--r-full);
  background: var(--steps-track);
  transition: background 0.2s ease;
}
.steps--simple .steps__item[data-step-state="complete"],
.steps--simple .steps__item[data-step-state="current"] {
  background: var(--steps-tone);
}
.steps--simple .steps__label {
  font: 600 var(--text-caption) / 1 var(--font-mono);
  text-transform: uppercase;
  letter-spacing: var(--tracking-loose);
  color: var(--steps-tone);
}

/* ── .steps--bullets — horizontal dots, connected ──────────
   Items compose .status-dot (the 12-px status-dot primitive);
   this block owns the layout (gap, connector geometry) only.
   The dots themselves are styled by .status-dot[data-step-state]. */
.steps--bullets {
  display: flex;
  align-items: center;
  gap: var(--space-4);
}
.steps--bullets .steps__connector {
  flex: 1 1 auto;
  height: 2px;
  background: var(--steps-track);
  min-width: var(--space-16);
}
.steps--bullets .steps__connector[data-step-state="complete"] {
  background: var(--steps-tone);
}

/* ── .steps--bullets-text — vertical: dot + title + desc ──
   The bullet column hosts a .status-dot primitive; this block
   owns the grid layout, vertical connector line, and text colour. */
.steps--bullets-text {
  display: flex;
  flex-direction: column;
}
.steps--bullets-text .steps__item {
  display: grid;
  grid-template-columns: var(--space-24) 1fr;
  column-gap: var(--space-16);
  position: relative;
  padding-bottom: var(--space-24);
}
.steps--bullets-text .steps__item:last-child { padding-bottom: 0; }
.steps--bullets-text .status-dot {
  margin-top: var(--space-4);
  margin-left: var(--space-4);
  position: relative;
  z-index: 1;
}
.steps--bullets-text .steps__item:not(:last-child)::before {
  content: "";
  position: absolute;
  left: calc(var(--space-4) + 5px);
  top: var(--space-20);
  bottom: 0;
  width: 2px;
  background: var(--steps-track);
}
.steps--bullets-text .steps__item[data-step-state="complete"]:not(:last-child)::before {
  background: var(--steps-tone);
}
.steps--bullets-text .steps__title {
  font: 600 var(--text-body) / 1.3 var(--font-sans);
  color: var(--text-primary);
}
.steps--bullets-text .steps__item[data-step-state="upcoming"] .steps__title {
  color: var(--text-muted);
}
.steps--bullets-text .steps__desc {
  margin-top: var(--space-4);
  font: 400 var(--text-sm) / 1.5 var(--font-sans);
  color: var(--text-secondary);
}

/* ── .steps--circles — numbered circles + connectors ──────
   The disc geometry is .spot-icon spot-icon--round; the state
   visual is .spot-icon[data-step-state="…"]. This block owns
   the row layout (gap), the connector lines, and per-disc
   typography overrides (mono number caption). */
.steps--circles {
  display: flex;
  align-items: center;
  gap: var(--space-4);
}
.steps--circles .steps__item {
  font: 600 var(--text-caption) / 1 var(--font-mono);
}
.steps--circles .steps__connector {
  flex: 1 1 auto;
  height: 2px;
  background: var(--steps-track);
  min-width: var(--space-16);
}
.steps--circles .steps__connector[data-step-state="complete"] {
  background: var(--steps-tone);
}
.steps--circles .steps__check {
  width: var(--space-16);
  height: var(--space-16);
}

/* ── .steps--circles-inline — numbered disc + INLINE label + connector ───
   The labeled, compact sibling of .steps--circles, for multi-step modal /
   wizard headers (the canonical breadcrumb: circle + label + connector).
   Each .steps__item pairs a numbered disc (.spot-icon spot-icon--round,
   scoped down to 24 px) WITH an inline title; a flexible .steps__connector
   spans the gap. Where .steps--circles is disc-only and .steps--circles-text
   uses a top-accent bar (no disc), this variant carries both. data-step-state
   lives on BOTH the .steps__item (for label colour) and the inner .spot-icon
   (for the disc visual) — same duplicate-the-state pattern as
   .steps--bullets-text's status-dot. */
.steps--circles-inline {
  display: flex;
  align-items: center;
  gap: var(--space-8);
}
.steps--circles-inline .steps__item {
  display: inline-flex;
  align-items: center;
  gap: var(--space-8);
  flex: 0 0 auto;
}
/* Compact 24 px disc — scoped override on THIS variant, never the shared
   .spot-icon default (stays 32 px). The mono number reads from this rule. */
.steps--circles-inline .spot-icon {
  width: var(--space-24);
  height: var(--space-24);
  font: 600 var(--text-caption) / 1 var(--font-mono);
}
.steps--circles-inline .steps__check {
  width: var(--space-12);
  height: var(--space-12);
}
.steps--circles-inline .steps__title {
  font: 600 var(--text-sm) / 1 var(--font-sans);
  color: var(--text-primary);
  white-space: nowrap;
}
.steps--circles-inline .steps__item[data-step-state="upcoming"] .steps__title {
  font-weight: var(--fw-regular);
  color: var(--text-muted);
}
.steps--circles-inline .steps__connector {
  flex: 1 1 auto;
  height: 1px;                          /* thinner hairline */
  min-width: var(--space-12);
  background: var(--steps-track);
}
.steps--circles-inline .steps__connector[data-step-state="complete"] {
  background: color-mix(in oklch, var(--steps-tone) 60%, #fff);   /* .btn-secondary label colour */
}

/* ── .steps--circles-text — top accent + label + title ─── */
.steps--circles-text {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: var(--space-12);
}
.steps--circles-text .steps__item {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-4);
  padding-top: var(--space-12);
  border-top: 4px solid var(--steps-track);
  transition: border-color 0.2s ease;
}
.steps--circles-text .steps__item[data-step-state="complete"],
.steps--circles-text .steps__item[data-step-state="current"] {
  border-top-color: var(--steps-tone);
}
.steps--circles-text .steps__label {
  font: 600 var(--text-caption) / 1 var(--font-mono);
  text-transform: uppercase;
  letter-spacing: var(--tracking-loose);
  color: var(--text-muted);
}
.steps--circles-text .steps__item[data-step-state="complete"] .steps__label,
.steps--circles-text .steps__item[data-step-state="current"] .steps__label {
  color: var(--steps-tone);
}
.steps--circles-text .steps__title {
  font: 600 var(--text-body) / 1.3 var(--font-sans);
  color: var(--text-primary);
}
.steps--circles-text .steps__item[data-step-state="upcoming"] .steps__title {
  color: var(--text-muted);
}

/* ── .steps--panels — boxed steps, top accent edge ──────── */
.steps--panels {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  border-radius: var(--space-12);
  overflow: hidden;
  background: var(--surface-card);
  box-shadow: var(--elev-1);
}
.steps--panels .steps__item {
  padding: var(--space-20);
  border-right: 1px solid var(--surface-sunk);
  position: relative;
}
.steps--panels .steps__item:last-child { border-right: none; }
.steps--panels .steps__item::before {
  content: "";
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 4px;
  background: var(--steps-track);
}
.steps--panels .steps__item[data-step-state="complete"]::before,
.steps--panels .steps__item[data-step-state="current"]::before {
  background: var(--steps-tone);
}
.steps--panels .steps__label {
  font: 600 var(--text-caption) / 1 var(--font-mono);
  text-transform: uppercase;
  letter-spacing: var(--tracking-loose);
  color: var(--text-muted);
}
.steps--panels .steps__item[data-step-state="complete"] .steps__label,
.steps--panels .steps__item[data-step-state="current"] .steps__label {
  color: var(--steps-tone);
}
.steps--panels .steps__title {
  margin-top: var(--space-8);
  font: 600 var(--text-body) / 1.3 var(--font-sans);
  color: var(--text-primary);
}
.steps--panels .steps__item[data-step-state="upcoming"] .steps__title {
  color: var(--text-muted);
}
.steps--panels .steps__desc {
  margin-top: var(--space-4);
  font: 400 var(--text-sm) / 1.5 var(--font-sans);
  color: var(--text-secondary);
}

/* ── .steps--panels-border — left accent edge ──────────── */
.steps--panels-border {
  display: flex;
  flex-direction: column;
}
.steps--panels-border .steps__item {
  padding: var(--space-12) var(--space-20);
  border-left: 4px solid var(--steps-track);
}
.steps--panels-border .steps__item[data-step-state="complete"],
.steps--panels-border .steps__item[data-step-state="current"] {
  border-left-color: var(--steps-tone);
}
.steps--panels-border .steps__label {
  font: 600 var(--text-caption) / 1 var(--font-mono);
  text-transform: uppercase;
  letter-spacing: var(--tracking-loose);
  color: var(--text-muted);
}
.steps--panels-border .steps__item[data-step-state="complete"] .steps__label,
.steps--panels-border .steps__item[data-step-state="current"] .steps__label {
  color: var(--steps-tone);
}
.steps--panels-border .steps__title {
  margin-top: var(--space-4);
  font: 600 var(--text-body) / 1.3 var(--font-sans);
  color: var(--text-primary);
}
.steps--panels-border .steps__item[data-step-state="upcoming"] .steps__title {
  color: var(--text-muted);
}
.steps--panels-border .steps__desc {
  margin-top: var(--space-2);
  font: 400 var(--text-sm) / 1.5 var(--font-sans);
  color: var(--text-secondary);
}

/* The old HTML-flex `.bar-chart` primitive (`.bar-chart--v` / `.bar-chart--h`
   with `.bar-row` / `.bar-label` / `.bar-track` / `.bar-fill` / `.bar-value`
   anatomy + `.bar-xaxis` / `.bar-xaxis-label` x-axis helpers + `--gradient`
   / `--gradient-brand` / `--success` / `--danger` modifiers) was retired
   May 2026 — the SVG `.bar-chart` primitive below (formerly `.bar-chart`,
   promoted May 2026 to take the canonical bar-chart slot) is its replacement.
   See `.claude/skills/lifi-ds-docs/references/bug-history.md → Bar chart —
   migration history` for the retirement narrative + dashboard consumer
   migration. */

/* ── .line-chart ──────────────────────────────────────────
   SVG container for line + area charts. Ship the SVG via HTML
   and style the series elements with the classes below so every
   chart picks up consistent stroke widths, grid styling, and
   theme-aware axis colors. Add .line-chart--area to invite
   the area fill helper. */
.line-chart {
  position: relative;
  width: 100%;
  min-height: 180px;
}
.line-chart > svg {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}
.line-chart .series {
  fill: none;
  stroke-width: 2.25;
  stroke-linejoin: round;
  stroke-linecap: round;
  transition: opacity 0.15s ease;
  filter: drop-shadow(0 1px 0 color-mix(in oklch, currentColor 16%, transparent));
}
.line-chart .series--area {
  fill: color-mix(in oklch, currentColor 16%, transparent);
  stroke: none;
  filter: none;
}
.line-chart .series-dot {
  fill: var(--surface-page);
  stroke: currentColor;
  stroke-width: 2.25;
  transition: r 0.15s ease, filter 0.15s ease;
}
/* ── Line-chart modifiers — mirror the area-chart pattern so
   users fluent in one are fluent in both. Compose modifiers
   freely: .line-chart.line-chart--step.line-chart--dots
   .line-chart--with-labels.line-chart--interactive is valid.

     .line-chart                base — smooth or linear (markup)
     .line-chart--step          step interpolation (staircase)
     .line-chart--dots          show .line-dot at each point
     .line-chart--with-labels   value labels above points
     .line-chart--with-axes     formal axis chrome + ticks
     .line-chart--minimal       strip grid + axes
     .line-chart--interactive   hover reveals crosshair + focus
   ─────────────────────────────────────────────────────── */
.line-chart .line-dot {
  fill: var(--surface-page);
  stroke: currentColor;
  stroke-width: 2.25;
  transition: r 0.15s ease, filter 0.15s ease;
}
.line-chart .line-dot--filled { fill: currentColor; }
.line-chart .line-dot--large { stroke-width: 3; }
.line-chart .line-dot--peak {
  fill: currentColor;
  stroke: var(--surface-page);
  stroke-width: 2.5;
  filter: drop-shadow(0 0 6px color-mix(in oklch, currentColor 55%, transparent));
}
.line-chart .line-dot--pulse {
  fill: color-mix(in oklch, currentColor 20%, transparent);
  stroke: none;
  transform-origin: center;
  transform-box: fill-box;
  animation: dsLinePulse 2.4s ease-in-out infinite;
}
@keyframes dsLinePulse {
  0%,100% { opacity: 0.55; transform: scale(1); }
  50%     { opacity: 0.15; transform: scale(1.6); }
}
@media (prefers-reduced-motion: reduce) {
  .line-chart .line-dot--pulse { animation: none; opacity: 0.35; }
}
/* Line chart value labels — same typography rules as bar labels:
   sans body font + weight 600 + tabular nums. See .bar-value-label
   for rationale. */
.line-chart .line-value-label {
  font-family: var(--font-sans);
  font-size: var(--text-caption);
  font-weight: var(--fw-semibold);
  line-height: 1;
  letter-spacing: var(--tracking-snug);
  font-variant-numeric: tabular-nums;
  fill: var(--text-primary);
  text-anchor: middle;
  paint-order: stroke fill;
  pointer-events: none;
}

.line-chart--minimal .chart-grid-line,
.line-chart--minimal .chart-axis-line,
.line-chart--minimal .chart-tick { display: none; }
.line-chart--with-axes .chart-grid-line,
.line-chart--with-axes .chart-axis-line,
.line-chart--with-axes .chart-tick { display: initial; }

/* Interactive — same mechanism as the area chart */
.line-chart--interactive .line-focus {
  opacity: 0;
  transition: opacity 0.12s ease;
  pointer-events: none;
}
.line-chart--interactive .line-hover-zone {
  fill: transparent;
  pointer-events: all;
  cursor: crosshair;
}
.line-chart--interactive .line-hover-zone:hover + .line-focus,
.line-chart--interactive .line-focus.is-active { opacity: 1; }
.line-chart--interactive .line-crosshair {
  stroke: color-mix(in oklch, var(--text-primary) 30%, transparent);
  stroke-width: 1;
  stroke-dasharray: 3 3;
  fill: none;
}
.line-chart--interactive .line-dot--focus {
  fill: currentColor;
  stroke: var(--surface-page);
  stroke-width: 2.5;
}
/* Series color hooks — use these classes on <path>/<g> elements to
   bind a line/area to a series color without inline styles. */
.series-1  { color: var(--series-1);  stroke: var(--series-1);  }
.series-2  { color: var(--series-2);  stroke: var(--series-2);  }
.series-3  { color: var(--series-3);  stroke: var(--series-3);  }
.series-4  { color: var(--series-4);  stroke: var(--series-4);  }
.series-5  { color: var(--series-5);  stroke: var(--series-5);  }
.series-6  { color: var(--series-6);  stroke: var(--series-6);  }
.series-7  { color: var(--series-7);  stroke: var(--series-7);  }
.series-8  { color: var(--series-8);  stroke: var(--series-8);  }
.series-9  { color: var(--series-9);  stroke: var(--series-9);  }
.series-10 { color: var(--series-10); stroke: var(--series-10); }
.series-11 { color: var(--series-11); stroke: var(--series-11); }
.series-12 { color: var(--series-12); stroke: var(--series-12); }

/* ── .area-chart ────────────────────────────────────────────
   SVG area-chart family. Extends the shared chart scaffolding
   (.chart-grid-line / .chart-axis-line / .chart-tick)
   and the series palette (.series-1..8) with area-specific
   helpers: fill class, filled-dot marker, gradient def hook.

   Configurable via a flat set of modifier classes — pick the
   ones you need and compose:

     .area-chart               base — linear path, filled area
     .area-chart--step         step interpolation (staircase)
     .area-chart--stacked      multiple layered series
     .area-chart--expanded     stacked with Y normalised to 100%
     .area-chart--gradient     vertical gradient fill
     .area-chart--minimal      hides grid + axes (for card inlays)
     .area-chart--with-axes    forces axis rendering + tick labels
     .area-chart--interactive  enables hover crosshair + focus dot

   Compose any subset — `.area-chart.area-chart--stacked
   .area-chart--gradient.area-chart--with-axes` is valid.
   ─────────────────────────────────────────────────────────── */
.area-chart {
  position: relative;
  width: 100%;
  min-height: 180px;
}
.area-chart > svg {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}

/* Area fill — by default expects a gradient via `fill="url(#…)"` on
   the path. When no gradient is supplied, the color-mix fallback
   kicks in so the chart still reads. This means gradient is the
   preferred rendering (matches shadcn/ui visual language) but is
   not strictly required. */
.area-chart .area-fill {
  fill: color-mix(in oklch, currentColor 20%, transparent);
  stroke: none;
  transition: opacity 0.15s ease;
}
.area-chart .area-line {
  fill: none;
  stroke: currentColor;
  stroke-width: 2.25;
  stroke-linejoin: round;
  stroke-linecap: round;
  /* Subtle drop shadow on the line — helps it punch through the
     gradient fill so the upper edge stays crisp at small sizes. */
  filter: drop-shadow(0 1px 0 color-mix(in oklch, currentColor 16%, transparent));
}
.area-chart .area-dot {
  fill: var(--surface-page);
  stroke: currentColor;
  stroke-width: 2.25;
}
/* Peak marker — filled dot used at the last data point to anchor the
   viewer's eye to the current value. Paired with a soft outer halo
   for that "live" feeling. */
.area-chart .area-dot--peak {
  fill: currentColor;
  stroke: var(--surface-page);
  stroke-width: 2.5;
  filter: drop-shadow(0 0 6px color-mix(in oklch, currentColor 55%, transparent));
}
/* Pulse halo ring — an SVG <circle> wrapping the peak dot that
   breathes softly. Purely decorative; optional. */
.area-chart .area-dot--pulse {
  fill: color-mix(in oklch, currentColor 20%, transparent);
  stroke: none;
  transform-origin: center;
  transform-box: fill-box;
  animation: dsAreaPulse 2.4s ease-in-out infinite;
}
@keyframes dsAreaPulse {
  0%,100% { opacity: 0.55; transform: scale(1); }
  50%     { opacity: 0.15; transform: scale(1.6); }
}
@media (prefers-reduced-motion: reduce) {
  .area-chart .area-dot--pulse { animation: none; opacity: 0.35; }
}

/* Minimal — stripped of chrome for dense contexts (stat-card footers,
   inline cards). Hides grid lines, axis lines, tick labels — the area
   shape does the talking. */
.area-chart--minimal .chart-grid-line,
.area-chart--minimal .chart-axis-line,
.area-chart--minimal .chart-tick { display: none; }

/* With axes — explicit axis rendering. Default doesn't suppress axes,
   but this modifier formalises the "full chrome" variant for matrices
   in docs and for `.chart-legend` pairing. */
.area-chart--with-axes .chart-grid-line,
.area-chart--with-axes .chart-axis-line,
.area-chart--with-axes .chart-tick { display: initial; }

/* Stacked — subtle emphasis bump on the fill so overlapping series
   stay readable. Both layers at 28% stay below the line threshold. */
.area-chart--stacked .area-fill {
  fill: color-mix(in oklch, currentColor 28%, transparent);
}

/* Expanded (normalised to 100%) — identical to stacked visually but
   semantically bounded. The modifier exists to give the variant a
   hook in JS / docs / tests. */
.area-chart--expanded .area-fill {
  fill: color-mix(in oklch, currentColor 34%, transparent);
}

/* Gradient — when this modifier is applied, the SVG markup should
   reference a `<linearGradient>` def via `fill="url(#gradId)"` on
   the `.area-fill` path. The class itself just defaults the fill
   to fully transparent so the gradient wins — preventing double-fill. */
.area-chart--gradient .area-fill {
  fill: transparent;
}

/* Interactive — crosshair + focus dot on hover. Works via a CSS-only
   target-following trick: each vertical gridline / point pair is
   grouped, and hovering any group shows its crosshair + dot.
   Groups have class `.area-focus` with a nested `.area-crosshair`
   + `.area-dot--focus`. JS can augment with tooltip rendering. */
.area-chart--interactive .area-focus {
  opacity: 0;
  transition: opacity 0.12s ease;
  pointer-events: none;
}
.area-chart--interactive .area-hover-zone {
  fill: transparent;
  pointer-events: all;
  cursor: crosshair;
}
.area-chart--interactive .area-hover-zone:hover + .area-focus,
.area-chart--interactive .area-focus.is-active { opacity: 1; }
.area-chart--interactive .area-crosshair {
  stroke: color-mix(in oklch, var(--text-primary) 30%, transparent);
  stroke-width: 1;
  stroke-dasharray: 3 3;
  fill: none;
}
.area-chart--interactive .area-dot--focus {
  fill: var(--accent-primary);
  stroke: var(--surface-page);
  stroke-width: 2;
}
/* Fade other series when hovering — drop non-hovered stacks to 40% */
.area-chart--interactive .area-fill:hover {
  filter: brightness(1.12);
}

/* ── Chart gradient variants — shared gradient API ───────────
   Opt-in gradient fills for every chart primitive. The goal is
   a single, predictable modifier vocabulary that works across
   both div-based charts (bars, progress — pure CSS gradients)
   and SVG-based charts (sparkline, line, area — SVG
   <linearGradient> defs).

   Two flavors per primitive:

     .ds-*--gradient         single-hue alpha fade
                             (solid at the anchor, 20% of the
                             same hue at the tip). Default choice
                             for dashboards — subtle, multi-series
                             friendly, plays well with semantic
                             tone overrides.

     .ds-*--gradient-brand   two-color brand gradient
                             (--accent-primary → --accent-secondary).
                             Hero/marketing moments — matches the
                             --grad-brand token direction. Use
                             sparingly: one per row, not every bar.

   Direction rules (mirror how the eye reads each chart):
     • vertical bar   — bottom → top (bar grows into its tip)
     • horizontal bar — left → right (reading direction)
     • progress bar   — left → right (matches horizontal bar)
     • line / area    — top → bottom (line at top, fading into axis)
     • sparkline area — same as line

   Techniques:
     • Div charts — pure CSS linear-gradient() on `background`.
       Override per instance via --color (start) and --color-end
       (finish) custom properties on the bar element.
     • SVG charts — the modifier class zeroes the flat fallback
       (`fill: transparent`) so the inline `fill="url(#id)"`
       wins. A reusable <defs> sprite snippet lives in design.md
       §06 — include it once per page and every chart can
       reference the same gradient IDs.

   Semantic composition:
     • .ds-*--gradient + .ds-*--success / --danger → same alpha
       fade, re-hued to the semantic color. Pattern works for
       bars + progress; for SVG charts, reference the matching
       defs sprite ID (ds-grad-area-success etc.).
   ────────────────────────────────────────────────────────── */

/* Progress bars fade left → right. (The .bar-chart--v / --h gradient pairs
   that previously shared this rule were retired May 2026 along with the
   HTML-flex `.bar-chart` primitive — see the migration note above. */
.progress--gradient .progress-fill {
  background: linear-gradient(
    to right,
    var(--color, var(--accent-primary)) 0%,
    color-mix(in oklch, var(--color, var(--accent-primary)) 20%, transparent) 100%
  );
}
.progress--gradient-brand .progress-fill {
  background: linear-gradient(
    to right,
    var(--accent-primary) 0%,
    var(--accent-secondary) 100%
  );
}

/* Semantic tones on progress gradients — swap --color so the alpha-fade
   math picks up the new hue automatically. (Applies only to `--gradient`,
   not `--gradient-brand`, because the brand variant is explicitly two-color
   and ignores --color.) */
.progress--gradient.progress--success .progress-fill {
  --color: var(--success);
}
.progress--gradient.progress--warn .progress-fill {
  --color: var(--warn);
}
.progress--gradient.progress--danger .progress-fill {
  --color: var(--danger);
}
.progress--gradient.progress--info .progress-fill {
  --color: var(--info);
}

/* SVG charts — the modifier zeroes the flat fallback so the
   inline <linearGradient> def wins. Caller supplies:
     <defs><linearGradient id="…">…</linearGradient></defs>
   on their SVG, and sets fill="url(#…)" on the area path.
   Same pattern as the pre-existing .area-chart--gradient.
   Both --gradient and --gradient-brand share this reset — the
   difference is which defs ID the caller references. */
.line-chart--gradient .series--area,
.line-chart--gradient-brand .series--area,
.sparkline--gradient .sparkline-area,
.sparkline--gradient-brand .sparkline-area {
  fill: transparent;
}
.area-chart--gradient-brand .area-fill {
  fill: transparent;
}

/* ──────────────────────────────────────────────────────────────
   Extended gradient coverage — line stroke + radial / donut /
   gauge strokes + advanced bars + heatmap.

   All variants follow the same two-flavor pattern:
     --gradient         single-hue alpha fade (Blue → Blue @ 20%)
     --gradient-brand   two-color brand gradient (Blue → Pink via Purple)

   SVG strokes and fills reference shared defs IDs so one <defs>
   sprite per page (documented in design/components/data-viz.md) covers every chart.
   The modifier classes below wire the class to the correct
   `url(#id)` reference — consumers just apply the class.
   ────────────────────────────────────────────────────────── */

/* Line chart — the LINE itself (not just the area) can carry the
   gradient stroke. Legacy behavior on .series untouched; only
   picked up by the modifier classes below. Area path is covered
   by the reset rule above. */
.line-chart--gradient  .series:not(.series--area) {
  stroke: url(#ds-grad-stroke-primary);
}
.line-chart--gradient-brand .series:not(.series--area) {
  stroke: url(#ds-grad-stroke-brand);
}

/* Advanced bars (SVG) — fill individual bar rects with a gradient.
   Vertical gradient direction (bar grows into the tip of the
   gradient). Selector combines `.bars.bars--*`
   (same element) to outrank the `.bars .bar-rect { fill:
   currentColor }` base rule that sits later in source order. */
.bars.bars--gradient .bar-rect {
  fill: url(#ds-grad-bars-primary);
}
.bars.bars--gradient-brand .bar-rect {
  fill: url(#ds-grad-bars-brand);
}

/* Radial chart — ring bar/segment strokes pick up the gradient.
   Works on every radial variant (label / grid / text / stacked /
   segments / half). Track stroke stays flat so the ring reads
   clearly against its background. */
.radial-chart--gradient .radial-bar,
.radial-chart--gradient .radial-segment {
  stroke: url(#ds-grad-ring-primary);
}
.radial-chart--gradient-brand .radial-bar,
.radial-chart--gradient-brand .radial-segment {
  stroke: url(#ds-grad-ring-brand);
}

/* Donut — the deprecated sibling of radial-chart, but still in use.
   Segment strokes use the same ring gradient defs. */
.donut--gradient .donut-segment {
  stroke: url(#ds-grad-ring-primary);
}
.donut--gradient-brand .donut-segment {
  stroke: url(#ds-grad-ring-brand);
}

/* Gauge — half-arc fill stroke picks up the gradient, matching
   the radial treatment so gauges and rings read as the same system. */
.gauge--gradient .gauge-fill {
  stroke: url(#ds-grad-ring-primary);
}
.gauge--gradient-brand .gauge-fill {
  stroke: url(#ds-grad-ring-brand);
}

/* Heatmap — two semantic variants:
   • --gradient reuses the default alpha-fade math (primary hue, 5
     intensity steps) but pushes saturation at the top end so the
     scale reads more vividly than the base. Useful as a "bolder"
     heatmap for hero dashboards.
   • --gradient-brand sweeps the hue across intensities — low
     intensity stays close to primary, high intensity lands at
     secondary via an OKLCH interpolation through the midpoint.
     Reads as a "brand spectrum" heatmap rather than a single-hue
     intensity scale. Use only when the data supports a two-hue
     perceptual ordering (e.g. cold → hot, slow → fast). */
.heatmap--gradient .heatmap-cell[data-intensity="1"] {
  background: color-mix(in oklch, var(--accent-primary) 30%, transparent);
}
.heatmap--gradient .heatmap-cell[data-intensity="2"] {
  background: color-mix(in oklch, var(--accent-primary) 52%, transparent);
}
.heatmap--gradient .heatmap-cell[data-intensity="3"] {
  background: color-mix(in oklch, var(--accent-primary) 78%, transparent);
}
.heatmap--gradient .heatmap-cell[data-intensity="4"] {
  background: linear-gradient(135deg,
    var(--accent-primary) 0%,
    color-mix(in oklch, var(--accent-primary) 70%, var(--accent-secondary)) 100%);
}
.heatmap--gradient-brand .heatmap-cell[data-intensity="1"] {
  background: color-mix(in oklch, var(--accent-primary) 40%, transparent);
}
.heatmap--gradient-brand .heatmap-cell[data-intensity="2"] {
  background: color-mix(in oklch,
    color-mix(in oklch, var(--accent-primary) 66%, var(--accent-secondary)) 60%,
    transparent);
}
.heatmap--gradient-brand .heatmap-cell[data-intensity="3"] {
  background: color-mix(in oklch,
    color-mix(in oklch, var(--accent-primary) 33%, var(--accent-secondary)) 80%,
    transparent);
}
.heatmap--gradient-brand .heatmap-cell[data-intensity="4"] {
  background: linear-gradient(135deg,
    color-mix(in oklch, var(--accent-primary) 20%, var(--accent-secondary)) 0%,
    var(--accent-secondary) 100%);
}

/* Chart legend with a leading SVG icon slot — for the "icons" variant
   of the area chart. The icon gets a 14×14 frame that doesn't
   disturb the swatch. */
.chart-legend-icon {
  width: var(--space-12);
  height: var(--space-12);
  flex-shrink: 0;
  color: currentColor;
}
.chart-legend-icon svg {
  width: 100%;
  height: 100%;
}

/* ── Area chart playground ─────────────────────────────────────
   Interactive demo in the docs — control panel + live chart.
   Uses the .seg segmented control for enum options and simple
   switch rows for boolean toggles. All styling hooks into the
   existing token system (--space-*, --text-*, --text-primary, etc.)
   so it theme-adapts cleanly. */
.area-playground {
  display: grid;
  grid-template-columns: 260px 1fr;
  gap: var(--space-24);
  align-items: start;
}
.area-playground-controls {
  display: flex;
  flex-direction: column;
  gap: var(--space-16);
  padding: var(--space-20);
  border-radius: var(--space-12);
  background: color-mix(in oklch, var(--glass-base), white 4%);
  border: 1px solid color-mix(in oklch, var(--text-primary) 8%, transparent);
}
.area-playground-group { display: flex; flex-direction: column; gap: var(--space-8); }
.area-playground-label {
  font: 600 var(--text-caption) / 1 var(--font-mono);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-muted);
}
.area-playground-toggles {
  display: flex; flex-direction: column; gap: var(--space-8);
}
.area-playground-toggle {
  display: flex; align-items: center; gap: var(--space-12);
  padding: var(--space-8) var(--space-12);
  border-radius: var(--space-12);
  cursor: pointer;
  font-size: var(--text-sm);
  color: var(--text-primary);
  user-select: none;
  transition: background 0.12s ease;
}
.area-playground-toggle:hover {
  background: color-mix(in oklch, var(--text-primary) 5%, transparent);
}
.area-playground-toggle input { display: none; }
.area-playground-toggle-track {
  width: var(--space-32); height: var(--space-20);
  border-radius: var(--r-full);
  background: color-mix(in oklch, var(--text-primary) 14%, transparent);
  position: relative;
  flex-shrink: 0;
  transition: background 0.15s ease;
}
.area-playground-toggle-track::after {
  content: '';
  position: absolute;
  top: 2px; left: 2px;
  width: var(--space-16); height: var(--space-16);
  border-radius: 50%;
  background: var(--text-primary);
  transition: left 0.15s ease, background 0.15s ease;
}
.area-playground-toggle input:checked ~ .area-playground-toggle-track {
  background: var(--accent-primary);
}
.area-playground-toggle input:checked ~ .area-playground-toggle-track::after {
  left: var(--space-12);
  background: var(--surface-page);
}
.area-playground-toggle-label { flex: 1; }
.area-playground-stage {
  padding: var(--space-24);
  border-radius: var(--space-12);
  background: color-mix(in oklch, var(--glass-base), white 2%);
  border: 1px solid color-mix(in oklch, var(--text-primary) 8%, transparent);
  min-height: 280px;
  display: flex;
  flex-direction: column;
  gap: var(--space-16);
}
.area-playground-stage .area-chart { min-height: 220px; }

@media (max-width: 900px) {
  .area-playground { grid-template-columns: 1fr; }
}

/* ══════════════════════════════════════════════════════════════
   PANEL — .panel
   ══════════════════════════════════════════════════════════════
   Universal floating-panel surface primitive. The canonical shape
   for full panels across the system: sidebars, drawers, modal-
   wrapper shells, the swap widget shell, side panels, command bars.
   May 2026 — promoted from one-off per-consumer recipes so the
   playground rail, the swap widget, and any future panel surface
   render from one source.

   Recipe: opaque raised surface (--surface-raised) + the floating-
   tier shadow (--elev-3). Theme-flips automatically: 97% in light /
   16% in dark for the fill; light/dark elev-3 variants live in
   :root + [data-theme="light"]. The .panel primitive moved off the
   Glass material in May 2026 so dense panel content reads with full
   contrast against page bg.

   What .panel DOES set:
     • Surface — background, shadow
     • Geometry — border-radius, padding
     • Internal stack — flex column with content rhythm gap

   What .panel does NOT set:
     • Width / height — consumer responsibility (centered widget
       uses max-width: var(--w-7); fixed rail uses left + width)
     • Position — consumer responsibility (fixed for rails,
       in-flow for in-page widgets, absolute for popovers)
     • Z-index — consumer responsibility (rail = 100, modal = 9000)

   Composes:
     <article class="panel ui-card">…</article>     swap widget shell
     <aside class="panel playground-rail">…</aside>  legacy alias; new
                                                     consumers can just use .panel.
*/
.panel {
  /* Surface — CONSUMES the elevation tokens (defaults on :root, so a content
     canvas or any ancestor scope can override them by inheritance — the
     default must NOT live here, or each .panel would set its own value and
     block the scope from cascading in; same lesson as the --card-gap /
     --grid-gap-* ladders). The :root defaults reproduce the original elevated
     look exactly: --elev-3 cast shadow + an invisible border. The reserved
     `1px solid transparent` border slot costs ZERO layout (box-sizing:
     border-box is global) and lets .panel--flat paint a hairline with no
     shift on any consumer. June 2026 — the elevated/flat surface axis,
     see CLAUDE.md → "Panel primitive". */
  background: var(--panel-surface);
  box-shadow: var(--panel-shadow);
  border: 1px solid var(--panel-border);

  /* Geometry — .panel is the container tier (form panel, receive panel,
     playground rail). Reaches for --panel-radius (sibling of
     --card-radius); decoupled so panels and cards inside them can flex
     independently via separate playground controls. May 2026.
     Padding consumes the --panel-padding token (default --space-24) so
     specific contexts can override per-axis — see the picker side-
     padding reduction in swap.css for the canonical override case. */
  border-radius: var(--panel-radius);
  padding: var(--panel-padding);

  /* Internal stack */
  display: flex;
  flex-direction: column;
  gap: var(--panel-gap);
}

/* ── Panel surface variants — elevated (default) vs flat ────────────────
   The elevation axis answers ONE question: "is this surface floating above
   the page?" Chrome + overlays float — the app rail, dropdowns, modals, the
   FAB — and keep the default --elev-3. CONTENT panels that sit in a
   scrollable canvas are peers on the page; at scale (the Portal's ~44
   panels) a cast shadow on each reads as noise that double-encodes the
   panel↔canvas tint delta. .panel--flat drops the shadow and moves the
   separation onto a hairline (the Linear / Stripe / Vercel dense-console
   convention; Material 3's "filled + outlined" card).

   Two ways to apply — both flip the SAME two tokens:
     • Per-instance — add .panel--flat / .panel--elevated to one panel.
     • Scope-wide — set --panel-shadow / --panel-border on a canvas wrapper
       (portal.css → .portal-canvas) so every descendant panel follows while
       the rail + overlays (outside the canvas) stay elevated. This is also
       the channel the Theme Composer's Panels section paints (June 2026).
   The axis flips all THREE surface tokens as one coherent identity:
   elevated = raised-FRAME fill + --elev-3 + no border; flat = CONTENT-tier
   fill (--surface-card, white-capable, tracks the surface seed) + no shadow
   + hairline. A flat panel IS a content surface sitting on the canvas, so it
   wears the content tier — which also lets it reach white via the composer,
   where the frame tier (--surface-raised, derived below page) never could. */
.panel--flat     { --panel-surface: var(--surface-card);   --panel-shadow: none;          --panel-border: var(--border-subtle); }
.panel--elevated { --panel-surface: var(--surface-raised); --panel-shadow: var(--elev-3); --panel-border: transparent; }

/* ── Panel slots — additive structural roles inside .panel ──────────────
   .panel is the frame; these three OPTIONAL slots own panel-EDGE behavior
   only and compose WITH the content primitives — they never replace them:
     .panel__header  pairs with .screen-header (two classes, one <header>):
                     .panel__header owns the panel-edge behavior (sticky,
                     header→body spacing handoff, optional bottom hairline);
                     .screen-header keeps owning the title/back/trailing grid.
     .panel__footer  pairs with .action-bar — the button row goes INSIDE it.
                     Absorbs the *positioning* role of .action-bar--footer
                     (margin-top:auto pin + top hairline). (.action-bar--footer
                     stays for non-panel modal consumers — see CLAUDE.md.)
     .panel__body    the clip-and-host scroll region: scroll the BODY, not the
                     frame. Codifies the sub-panel scroll rule (clip the panel
                     via overflow:hidden on the consumer; host scroll here).
   A panel may use any subset; bare children still stack via the flex column
   + --panel-gap. These are layout-only — no surface/padding of their own. */
.panel__header { flex: 0 0 auto; }

/* Hairline under the header (opt-in). */
.panel__header--ruled {
  padding-bottom: var(--panel-gap);
  border-bottom: 1px solid var(--border-subtle);
}

/* Sticky header over the panel's own scroll (opt-in). Bleeds to the panel
   edges so scrolling content passes UNDER an opaque head, then meets the
   body padding below the line. Pairs with .panel__body as the scroller. */
.panel__header--sticky {
  position: sticky;
  top: calc(-1 * var(--panel-padding));
  z-index: 2;
  margin: calc(-1 * var(--panel-padding)) calc(-1 * var(--panel-padding)) var(--panel-gap);
  padding: var(--panel-padding) var(--panel-padding) var(--panel-gap);
  background: var(--surface-raised);
}

/* Scrollable body. overflow-y:auto only engages when the panel has a bounded
   height (e.g. the widget's --widget-max-h cap, or a fixed-height rail); on an
   unbounded in-page panel it's a harmless no-op and the body grows naturally.
   NOTE: clips horizontally — don't host edge-bleed hover rows directly here;
   give those their own scroll host (see .ui-token-picker__body). */
.panel__body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}

/* Pinned footer. margin-top:auto pins it to the panel bottom (the panel is
   always a flex column). Top hairline + top padding separate it from the body. */
.panel__footer {
  flex: 0 0 auto;
  margin-top: auto;
  padding-top: var(--panel-gap);
  border-top: 1px solid color-mix(in oklch, var(--text-primary) 8%, transparent);
}


/* ══════════════════════════════════════════════════════════════
   THEME COMPOSER PANEL — .theme-composer
   ══════════════════════════════════════════════════════════════
   The ONE container the ThemeComposer editor (theme-editor.js → mount)
   renders into across all three host surfaces — the FAB (floating), the
   Portal Settings → Theme modal, and the playground rail (flush). The editor
   itself carries no chrome; this primitive IS the chrome, so "reuse" means
   "import the panel," not "rebuild it three times" (design/theme-composer.md
   §5 / §8). Composes .panel + its .panel__header / __body / __footer slots:
   the header sits in flow (NOT sticky — the clean clip-and-host shape the
   modal uses), the body is the scroll host + editor mount target, an optional
   footer pins per-theme actions.

   Width is published as --theme-composer-width (default --w-9 = 480) so the
   Colors section (per-accent OKLCH rows + L/C/H sliders) breathes — the old
   288 px glass FAB sub-panel clipped it. A narrow host overrides it (the rail
   via --flush). June 2026 — see design/theme-composer.md §5/§8 + surfaces.md. */
.theme-composer {
  --theme-composer-width: var(--w-9);
  width: var(--theme-composer-width);
  max-width: 100%;
  max-height: min(80vh, 760px);
  /* Clip-and-host scroll (CLAUDE.md → "Sub-panel scroll architecture"): the
     FRAME clips, .panel__body scrolls. overflow:hidden here + the body's own
     overflow-y:auto means content can never paint over the in-flow header. */
  overflow: hidden;
}

/* Header + body are semantic hooks ON TOP of the .panel__* slots (which carry
   the behavior). The body is the editor's mount target. No own rules needed
   beyond the flush override below — .panel__header / .panel__body do the work. */

/* ── Floating variant (FAB) — anchored bottom-right with the same fade+slide
   open/close as .ds-doc-menu-panel. Narrows to --w-6 (384, the rail tier) so
   the floating composer reads as the twin of the playground rail's theme screen
   (its flush host) rather than the wider --w-9 default-modal width — which stays
   correct for the Portal modal context on the base rule. 384 > the old 288 FAB
   sub-panel, so the OKLCH slider rows still clear without clipping. */
.theme-composer--floating {
  --theme-composer-width: var(--w-6);
  position: fixed;
  right: var(--space-24);
  bottom: 88px;
  z-index: 9998;
  opacity: 0;
  transform: translateY(10px) scale(0.97);
  pointer-events: none;
  transition: opacity 0.2s ease, transform 0.2s ease;
}
.theme-composer--floating.is-visible {
  opacity: 1;
  transform: translateY(0) scale(1);
  pointer-events: all;
}

/* ── Flush variant (rail) — chromeless, fills its host. The playground rail
   IS a .panel and its active screen already owns the scroll, so the composer
   here sheds its own surface / shadow / padding / scroll and just carries the
   body (the rail screen keeps the header + the actions kebab). This is what
   makes the rail a genuine third consumer of the primitive without nesting a
   second panel surface inside the rail's. */
.theme-composer--flush {
  width: 100%;
  max-width: none;
  max-height: none;
  background: transparent;
  box-shadow: none;
  border: 0;
  border-radius: 0;
  padding: 0;
  overflow: visible;            /* the rail screen owns the scroll */
}
.theme-composer--flush .theme-composer__body {
  overflow: visible;            /* don't nest a second scroller in the rail screen */
}

/* ── Modal context (Portal) — the .modal-overlay centers it; borrow the
   modal's slide-up entrance so it doesn't pop in cold. */
.modal-overlay > .theme-composer {
  animation: modal-slide-up 0.25s ease-out;
}


/* ══════════════════════════════════════════════════════════════
   ACTION CARD — .action-card
   ══════════════════════════════════════════════════════════════
   Reusable "doorway" affordance: icon + title + description +
   trailing arrow, rendered as a <button> or <a>. Designed for
   cross-project use — drop it into any page that loads this
   stylesheet and it inherits the active theme, palette, and
   spacing scale. Use for: modal launchers, tool entry points,
   settings shortcuts, "get started" empty-state CTAs, feature
   link rows in docs or navigation menus.

   Anatomy:
     .action-card
       .action-card-icon    — optional 44×44 accent badge
       .action-card-body    — required title/desc stack
         .action-card-title — required
         .action-card-desc  — optional supporting line
       .action-card-meta    — optional right-aligned chip
                                 (shortcut key, badge, timestamp)
       .action-card-arrow   — optional trailing affordance

   Sizes:
     (default · md)             body/caption type · 44px icon · 14/20 pad
     .action-card--sm        sm/caption type   · 36px icon · 12/16 pad
     .action-card--lg        body/sm type      · 56px icon · 20/24 pad

   Tones (border + icon tint):
     (default)                  neutral; accent border on hover
     .action-card--accent    accent border by default + gradient wash
     .action-card--muted     borderless — for dense list rows
     .action-card--success   green (on-brand positive)
     .action-card--danger    red (destructive)

   Layout:
     (default)                  horizontal row
     .action-card--vertical  stacked: icon above body
     .action-card--compact   single-line (no description row)

   Customization — every visual property is a CSS custom property,
   override per-instance with style="--ac-accent: var(--warn);" etc.
   See the --ac-* token list inside the rule.
   ══════════════════════════════════════════════════════════════ */
.action-card {
  /* ── Customization tokens — override on the element for local tweaks */
  --ac-padding-y:   var(--space-16);
  --ac-padding-x:   var(--space-20);
  --ac-gap:         var(--space-16);
  --ac-radius:      var(--card-radius);
  /* Defaults consume the resolved Materials API (--mat-*) wired via
     :where(.action-card) — paper-by-class. Consumers that need a
     custom appearance override the --ac-* tokens directly (e.g.
     style="--ac-bg: red"); the chain is:
       material default (paper)  →  --mat-*  →  --ac-*  →  rendered.
     Per-instance overrides on --ac-* still win as before, so this is
     a non-breaking migration. May 2026.
     Hover state mixes accent over --surface-card (theme-aware) so
     hover reads correctly in both themes regardless of material. */
  --ac-bg:          var(--mat-bg);
  --ac-bg-hover:    color-mix(in oklch, var(--accent-primary) 5%, var(--surface-card));
  --ac-border:      color-mix(in oklch, currentColor 4%, transparent);
  --ac-border-hover: color-mix(in oklch, var(--accent-primary) 32%, transparent);
  --ac-shadow:      var(--mat-shadow);
  --ac-shadow-hover: var(--mat-shadow-hover),
                     0 6px 20px -8px color-mix(in oklch, var(--accent-primary) 22%, transparent);
  --ac-icon-size:   44px;
  --ac-icon-radius: 12px;
  --ac-icon-bg:     linear-gradient(135deg,
                      color-mix(in oklch, var(--accent-primary) 18%, transparent) 0%,
                      color-mix(in oklch, var(--accent-secondary) 14%, transparent) 100%);
  --ac-icon-border: color-mix(in oklch, var(--accent-primary) 20%, transparent);
  --ac-icon-color:  var(--accent-primary);
  --ac-title-size:  var(--text-body);
  --ac-desc-size:   var(--text-sm);
  --ac-arrow-color: var(--accent-primary);
  --ac-gradient-wash: none;

  display: flex;
  align-items: center;
  gap: var(--ac-gap);
  width: 100%;
  padding: var(--ac-padding-y) var(--ac-padding-x);
  border-radius: var(--ac-radius);
  background: var(--ac-bg);
  border: 1px solid var(--ac-border);
  box-shadow: var(--ac-shadow);
  color: var(--text-primary);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  font: inherit;
  position: relative;
  overflow: hidden;
  isolation: isolate;
  transition:
    border-color 0.18s ease,
    background 0.18s ease,
    transform 0.18s ease,
    box-shadow 0.18s ease;
}
/* Gradient wash overlay — opt-in via --ac-gradient-wash. On the --accent
   tone this reveals on hover for "come here" energy; on others it's
   transparent by default. Sits under content via z-index. */
.action-card::before {
  content: '';
  position: absolute; inset: 0;
  background: var(--ac-gradient-wash);
  opacity: 0;
  transition: opacity 0.2s ease;
  pointer-events: none;
  z-index: -1;
}
.action-card:hover::before { opacity: 1; }
.action-card:hover {
  background: var(--ac-bg-hover);
  border-color: var(--ac-border-hover);
  transform: translateY(-1px);
  box-shadow: var(--ac-shadow-hover);
}
.action-card:active {
  transform: translateY(0);
}
.action-card:focus-visible {
  outline: 2px solid var(--accent-primary-mid);
  outline-offset: 2px;
  border-color: var(--ac-border-hover);
}
.action-card:disabled,
.action-card[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
  pointer-events: none;
}

/* ── Sub-parts ───────────────────────────────────────── */
.action-card-icon {
  width: var(--ac-icon-size);
  height: var(--ac-icon-size);
  border-radius: var(--ac-icon-radius);
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--ac-icon-bg);
  border: 1px solid var(--ac-icon-border);
  color: var(--ac-icon-color);
}
/* Icon SVG auto-sizes to 50% of the chip — so sm=18, md=22, lg=28.
   Works for both Lucide and Material stroke-weight icons at 1.5–2. */
.action-card-icon svg {
  width: calc(var(--ac-icon-size) * 0.5);
  height: calc(var(--ac-icon-size) * 0.5);
  display: block;
}

.action-card-body {
  flex: 1; min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.action-card-title {
  font-size: var(--ac-title-size);
  line-height: var(--lh-body);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-snug);
  color: var(--text-primary);
  /* Prevent long titles from shrinking the body flex item below the
     gap, which would push the arrow off. */
  overflow-wrap: anywhere;
}
.action-card-desc {
  font-size: var(--ac-desc-size);
  line-height: var(--lh-sm);
  color: var(--text-muted);
  letter-spacing: var(--tracking-normal);
  overflow-wrap: anywhere;
}

/* Optional right-aligned meta slot — for shortcut keys (⌘K),
   status badges, or timestamps. */
.action-card-meta {
  flex-shrink: 0;
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--space-8);
  font: 600 var(--text-caption) / 1 var(--font-mono);
  letter-spacing: var(--tracking-wide);
  color: var(--text-faint);
}

.action-card-arrow {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  color: var(--ac-arrow-color);
  transition: transform 0.18s ease;
}
.action-card-arrow svg {
  width: 20px;
  height: 20px;
  display: block;
}
.action-card:hover .action-card-arrow {
  transform: translateX(4px);
}

/* ── Size modifiers ──────────────────────────────────── */
.action-card--sm {
  --ac-padding-y: 12px;
  --ac-padding-x: var(--space-16);
  --ac-gap: var(--space-12);
  --ac-radius: 12px;
  --ac-icon-size: 36px;
  --ac-icon-radius: 10px;
  --ac-title-size: var(--text-sm);
  --ac-desc-size: var(--text-caption);
}
.action-card--lg {
  --ac-padding-y: var(--space-20);
  --ac-padding-x: var(--space-24);
  --ac-gap: var(--space-20);
  --ac-radius: 16px;
  --ac-icon-size: 56px;
  --ac-icon-radius: 14px;
  --ac-title-size: var(--text-h3);
  --ac-desc-size: var(--text-sm);
}

/* ── Tone modifiers ──────────────────────────────────── */
/* Accent — stronger brand presence for primary CTAs. Starts with an
   accent-tinted border and reveals a gradient wash on hover. */
.action-card--accent {
  --ac-border: color-mix(in oklch, var(--accent-primary) 20%, transparent);
  --ac-border-hover: color-mix(in oklch, var(--accent-primary) 45%, transparent);
  --ac-bg-hover: color-mix(in oklch, var(--accent-primary) 6%, transparent);
  --ac-gradient-wash: linear-gradient(135deg,
    color-mix(in oklch, var(--accent-primary) 10%, transparent) 0%,
    color-mix(in oklch, var(--accent-secondary) 7%, transparent) 100%);
  --ac-shadow-hover: 0 8px 24px -8px color-mix(in oklch, var(--accent-primary) 30%, transparent);
}

/* Muted — borderless, for dense list rows in settings / command
   palettes. Loses the hover shadow to feel lighter. */
.action-card--muted {
  --ac-bg: color-mix(in oklch, var(--text-primary) 3%, transparent);
  --ac-bg-hover: color-mix(in oklch, var(--text-primary) 6%, transparent);
  --ac-border: transparent;
  --ac-border-hover: transparent;
  --ac-shadow-hover: none;
  --ac-icon-bg: color-mix(in oklch, var(--text-primary) 8%, transparent);
  --ac-icon-border: transparent;
  --ac-icon-color: var(--text-muted);
  --ac-arrow-color: var(--text-faint);
}
.action-card--muted:hover { transform: none; }

/* Success — positive action (confirm, complete, upgrade). */
.action-card--success {
  --ac-border: color-mix(in oklch, var(--success) 22%, transparent);
  --ac-border-hover: color-mix(in oklch, var(--success) 48%, transparent);
  --ac-bg-hover: color-mix(in oklch, var(--success) 7%, transparent);
  --ac-icon-bg: color-mix(in oklch, var(--success) 15%, transparent);
  --ac-icon-border: color-mix(in oklch, var(--success) 25%, transparent);
  --ac-icon-color: var(--success);
  --ac-arrow-color: var(--success);
  --ac-shadow-hover: 0 6px 20px -8px color-mix(in oklch, var(--success) 28%, transparent);
}

/* Danger — destructive action (delete, disconnect, revoke). */
.action-card--danger {
  --ac-border: color-mix(in oklch, var(--danger) 22%, transparent);
  --ac-border-hover: color-mix(in oklch, var(--danger) 48%, transparent);
  --ac-bg-hover: color-mix(in oklch, var(--danger) 7%, transparent);
  --ac-icon-bg: color-mix(in oklch, var(--danger) 15%, transparent);
  --ac-icon-border: color-mix(in oklch, var(--danger) 25%, transparent);
  --ac-icon-color: var(--danger);
  --ac-arrow-color: var(--danger);
  --ac-shadow-hover: 0 6px 20px -8px color-mix(in oklch, var(--danger) 28%, transparent);
}

/* ── Layout modifiers ────────────────────────────────── */
/* Vertical — stacks icon above the body. Good for grid tiles. */
.action-card--vertical {
  flex-direction: column;
  align-items: flex-start;
  text-align: left;
  gap: var(--space-12);
  padding: var(--space-20);
}
.action-card--vertical .action-card-arrow {
  align-self: flex-start;
  margin-top: var(--space-4);
}

/* Compact — kills the description line gap, for single-line shortcut
   rows. Pair with a title + optional meta + arrow. */
.action-card--compact {
  --ac-padding-y: 10px;
}
.action-card--compact .action-card-body { gap: 0; }

/* ── Responsive ──────────────────────────────────────── */
@media (max-width: 600px) {
  .action-card--lg {
    --ac-icon-size: 48px;
    --ac-padding-y: var(--space-16);
    --ac-padding-x: var(--space-20);
    --ac-title-size: var(--text-body);
  }
}

/* ── .bars ──────────────────────────────────────────────────
   SVG-based bar chart family — the plot-area primitive composed
   inside .bar-chart and related chart wrappers. Covers the full
   shadcn/ui matrix (default · horizontal · grouped · stacked ·
   label · label-inside · mixed · active · negative · interactive)
   via composable modifier classes.

   Everything binds to the existing tokens: --series-1..8 for color,
   shared .chart-grid-line / --axis-line / --tick for chrome.
   Rounded corners (rx/ry) keep bars feeling approachable. Optional
   gradient fills via <linearGradient> defs mirror the area-chart
   treatment so the two components read as one system.

     .bars                  base — vertical bars, single series
     .bars--horizontal      flip to horizontal rows
     .bars--grouped         multi-series bars side-by-side
     .bars--stacked         multi-series bars stacked vertically
     .bars--mixed           one color per bar (categorical)
     .bars--active          one bar emphasised, others dimmed
     .bars--negative        baseline in the middle, bars up/down
     .bars--with-labels     value labels above each bar
     .bars--labels-inside   value labels inside bars (contrast)
     .bars--with-axes       formal axis chrome + tick labels
     .bars--minimal         strip grid + axes for card inlays
     .bars--interactive     hover brightens + optional crosshair
   ──────────────────────────────────────────────────────────── */
.bars {
  position: relative;
  width: 100%;
  min-height: 180px;
}
.bars > svg {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}
.bars .bar-rect {
  fill: currentColor;
  stroke: none;
  transition: opacity 0.18s ease, filter 0.15s ease;
}
.bars .bar-rect:hover {
  filter: brightness(1.12);
}
/* Value labels above or inside bars.
   Uses the design system's sans body type (Figtree) at weight 600 —
   not the mono caption used for axis ticks. Values are *content*,
   not measurements; they should read like content. Tabular numerals
   keep multi-digit numbers aligned column-to-column. */
.bars .bar-value-label {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  font-weight: var(--fw-semibold);
  line-height: 1;
  letter-spacing: var(--tracking-snug);
  font-variant-numeric: tabular-nums;
  fill: var(--text-primary);
  /* Suppress the stroke inherited from <g class="series-N"> parent — the
     series-N classes set both color AND stroke for line/area charts, and
     that stroke would otherwise paint a colored 1px halo around bar-value
     text. Bar text is content, not a line — no stroke needed. */
  stroke: none;
  text-anchor: middle;
  pointer-events: none;
}
.bars--labels-inside .bar-value-label {
  /* Always white for in-bar labels — --surface-page flipped in dark mode
     and made the label disappear on accent-filled bars. White-95%
     reads cleanly against every series color in both themes. */
  fill: rgba(255, 255, 255, 0.95);
}
/* Category label along the baseline (x-axis replacement when axes off) */
.bars .bar-category-label {
  font: 500 var(--text-caption) / 1 var(--font-mono);
  fill: var(--text-faint);
  text-anchor: middle;
  letter-spacing: var(--tracking-wide);
}

/* Active variant — one bar emphasised, others dimmed */
.bars--active .bar-rect:not(.is-active) { opacity: 0.3; }
.bars--active .bar-rect.is-active {
  filter: drop-shadow(0 0 12px color-mix(in oklch, currentColor 45%, transparent));
}

/* Interactive — pointer cursor + brighter hover */
.bars--interactive .bar-rect { cursor: pointer; }
.bars--interactive .bar-rect:hover { filter: brightness(1.18); }
.bars--interactive .bar-hover-zone {
  fill: transparent;
  pointer-events: all;
  cursor: pointer;
}
.bars--interactive .bar-hover-zone:hover + .bar-focus,
.bars--interactive .bar-focus.is-active { opacity: 1; }
.bars--interactive .bar-focus {
  opacity: 0;
  transition: opacity 0.12s ease;
  pointer-events: none;
}
.bars--interactive .bar-crosshair {
  stroke: color-mix(in oklch, var(--text-primary) 30%, transparent);
  stroke-width: 1;
  stroke-dasharray: 3 3;
  fill: none;
}

/* Negative variant — baseline sits in the middle; bars extend above
   for positive values and below for negative. The negative-zone
   baseline line uses a stronger axis color to reinforce the zero. */
.bars--negative .bar-baseline {
  stroke: color-mix(in oklch, var(--text-primary) 28%, transparent);
  stroke-width: 1.25;
}

/* Minimal / with-axes — same logic as area chart */
.bars--minimal .chart-grid-line,
.bars--minimal .chart-axis-line,
.bars--minimal .chart-tick { display: none; }
.bars--with-axes .chart-grid-line,
.bars--with-axes .chart-axis-line,
.bars--with-axes .chart-tick { display: initial; }

/* ── .chart-avatar-row ────────────────────────────────────────
   Horizontal row of avatars positioned to align under a bar chart's
   data slots. Each direct child becomes an even-flex cell; consumers
   drop in .avatar primitives (or any inline element) and the row's
   flex layout pins each one to the center of its slot.

   Typical usage: paired with an SVG .bars container above. The row's
   --avatar-row-inset matches the chart's plot-area inset so avatars
   align to bar centers. The default 32px inset fits the canonical
   bar chart used by Data Visualization examples; override per chart
   when the SVG's left-axis gutter differs.

   Why an HTML row instead of SVG <image> tags inside the chart:
   avatars often want tooltips, click handlers, links, and labels
   that work better as HTML than SVG primitives. The row sits BELOW
   the chart, not inside it. Pairs with the rotated x-axis labels
   (which still live inside the SVG via <text class="bar-category-label">). */
.chart-avatar-row {
  --avatar-row-inset: var(--space-32);
  --avatar-row-gap:   var(--space-8);
  display: flex;
  gap: var(--avatar-row-gap);
  width: 100%;
  padding-inline: var(--avatar-row-inset);
  margin-top: var(--space-12);
}
.chart-avatar-row > * {
  flex: 1;
  min-width: 0;
  display: flex;
  justify-content: center;
  align-items: center;
}

/* ── .bar-chart ─────────────────────────────────────────
   Vertical bar chart optimized for ranked categorical comparisons —
   one bar per category, identity per bar (avatar + label), value
   above. Composes the existing .bars (SVG) + .chart-axis-title +
   .chart-avatar-row + .series-1..12 categorical palette into a
   single wrapper that coordinates plot-area inset so the bars and
   avatars stay aligned without per-instance pixel math.

   Reach for this primitive when you have:
     • A ranked list of 5–20 categories (DEX volumes, bridge throughput,
       protocol TVLs, top portfolios, leaderboard rankings).
     • A clear identity per row (avatar + label) — categorical fill,
       not series-grouped.
     • A single metric per category (no grouped/stacked bars).

   For grouped/stacked categorical comparisons, reach for .bars--grouped
   or .bars--stacked instead. For horizontal ranked rows under 10
   categories without identity per row, use the `.bars--h` SVG primitive directly (no `.bar-chart` wrapper).

   Customization slots (override per-instance via inline style):
     --bar-chart-plot-inset-start  — left gutter of plot area
                                        (default 11% — matches a 70/800
                                        viewBox y-axis gutter)
     --bar-chart-plot-inset-end    — right gutter
                                        (default 3.4% — matches a 27/800
                                        viewBox right margin)
     --bar-chart-gap               — gap between SVG and avatar row
     --bar-chart-aspect            — aspect ratio for the SVG container

   The wrapper SETS these on the .chart-avatar-row child via the cascade,
   so consumers tune insets in ONE place. */
.bar-chart {
  --bar-chart-plot-inset-start: 6%;    /* 48/800 — y-axis gutter, matches chartFrame marginL */
  --bar-chart-plot-inset-end:   1.5%;  /* 12/800 — right breathing, matches chartFrame marginR */
  --bar-chart-gap:              var(--space-12);
  --bar-chart-aspect:           800 / 380;
  display: flex;
  flex-direction: column;
  gap: var(--bar-chart-gap);
  width: 100%;
}
.bar-chart > .bars {
  aspect-ratio: var(--bar-chart-aspect);
  width: 100%;
}
/* Optional: when consumer uses .chart-avatar-row as a SEPARATE HTML row
   below the SVG (alternative pattern — works for short, horizontal labels
   where rotation isn't needed). The default pattern places both the
   avatar and the label INSIDE the SVG as a rotated <g> per bar so they
   move as one identity unit. */
.bar-chart > .chart-avatar-row {
  padding-inline-start: var(--bar-chart-plot-inset-start);
  padding-inline-end:   var(--bar-chart-plot-inset-end);
  margin-top: 0;
}

/* ── .bar-chart — variant axes ──────────────────────────
   Four orthogonal axes the primitive varies on. Pick by what
   you need; combine freely (e.g. .bar-chart--mono.bar-chart--compact).

     • Bar shape       --bar-radius slot          (system-wide rx/ry)
     • Highlight       .bars--active + .is-active (one bar emphasised)
     • Palette         --mono / --sequential / --diverging
     • Identity        --no-labels / --no-avatars
     • Density         --compact / --hero

   Comparison line:   compose .chart-threshold inside the SVG
                      (see the chart-chrome family — it's reusable
                      across any .bars consumer, not a leaderboard
                      modifier).

   The variant modifiers re-route .series-N color WITHOUT forcing
   consumers to strip .series-N from markup — consumers add ONE
   modifier class on the wrapper and the cascade does the rest. */

/* ── Bar shape — system-wide rx via SVG2 geometry-as-CSS ──────
   Browser support is GA (Chrome 64+, Safari 14+, Firefox 72+).
   CSS overrides the per-rect rx attribute, so this slot is a
   SYSTEM-WIDE decision — all bars take the same value. For per-bar
   rx variation (rare; only the smallest bars at the right side
   of long leaderboards typically want rx=2), set the rx attribute
   directly on those specific <rect> elements and DON'T use this
   slot. */
.bar-chart { --bar-radius: 4; }
.bar-chart .bar-rect {
  rx: var(--bar-radius);
  ry: var(--bar-radius);
}

/* ── Highlight state — extend .bars--active to identity units ─
   .bars--active dims sibling .bar-rect to opacity 0.3 (authored
   upstream at line ~11066). The identity units (rotated <g> with
   avatar + label) were left at full opacity, so emphasis read as
   half-finished. Extend the dim to identity siblings; the active
   identity stays loud AND its label ink lifts from --text-faint
   to --text-primary so the active row reads as one unit. */
.bar-chart .bars--active .chart-x-identity {
  opacity: 0.45;
  transition: opacity 0.12s ease;
}
.bar-chart .bars--active .chart-x-identity.is-active { opacity: 1; }
.bar-chart .bars--active .chart-x-identity.is-active .chart-x-identity__label {
  color: var(--text-primary);
}

/* ── Palette mode — Mono ──────────────────────────────────────
   All bars one hue. No rank signal in color; use when the chart
   is about identifying categories, not ranking them. Slot lets
   consumers override per-instance (success-toned for a positive
   trend chart, danger-toned for a regression chart, etc.). */
.bar-chart--mono { --bar-chart-mono-tone: var(--accent-primary); }
.bar-chart--mono > .bars [class*="series-"] .bar-rect {
  fill: var(--bar-chart-mono-tone);
}
.bar-chart--mono > .bars [class*="series-"] .bar-value-label {
  fill: var(--bar-chart-mono-tone);
}

/* ── Palette mode — Sequential ────────────────────────────────
   High-rank → high-saturation. series-1 (top bar in a typical
   ranked-descending dataset) gets --seq-9 (most saturated); each
   subsequent bar steps down the ramp. Bars 10..12 share --seq-1
   (faintest) because the ramp only has 9 stops. Reach for this
   when color SHOULD reinforce ranking — top-N portfolios, density
   maps, market share. */
.bar-chart--sequential > .bars .series-1  .bar-rect { fill: var(--seq-9); }
.bar-chart--sequential > .bars .series-2  .bar-rect { fill: var(--seq-8); }
.bar-chart--sequential > .bars .series-3  .bar-rect { fill: var(--seq-7); }
.bar-chart--sequential > .bars .series-4  .bar-rect { fill: var(--seq-6); }
.bar-chart--sequential > .bars .series-5  .bar-rect { fill: var(--seq-5); }
.bar-chart--sequential > .bars .series-6  .bar-rect { fill: var(--seq-4); }
.bar-chart--sequential > .bars .series-7  .bar-rect { fill: var(--seq-3); }
.bar-chart--sequential > .bars .series-8  .bar-rect { fill: var(--seq-2); }
.bar-chart--sequential > .bars .series-9  .bar-rect,
.bar-chart--sequential > .bars .series-10 .bar-rect,
.bar-chart--sequential > .bars .series-11 .bar-rect,
.bar-chart--sequential > .bars .series-12 .bar-rect { fill: var(--seq-1); }

/* ── Palette mode — Diverging ─────────────────────────────────
   For benchmark deltas — bars above the mean color through the
   green half, bars below color through the pink half, bars at
   the mid hover near neutral slate. Consumer is responsible for
   ordering bars by SIGNED magnitude (most-positive → most-negative)
   — this modifier just colors them. With 12 series stops mapped
   onto a 9-stop diverging ramp: top 4 → --div-9..6 (green); middle
   4 → --div-5 (neutral); bottom 4 → --div-4..1 (pink). */
.bar-chart--diverging > .bars .series-1  .bar-rect { fill: var(--div-9); }
.bar-chart--diverging > .bars .series-2  .bar-rect { fill: var(--div-8); }
.bar-chart--diverging > .bars .series-3  .bar-rect { fill: var(--div-7); }
.bar-chart--diverging > .bars .series-4  .bar-rect { fill: var(--div-6); }
.bar-chart--diverging > .bars .series-5  .bar-rect,
.bar-chart--diverging > .bars .series-6  .bar-rect,
.bar-chart--diverging > .bars .series-7  .bar-rect,
.bar-chart--diverging > .bars .series-8  .bar-rect { fill: var(--div-5); }
.bar-chart--diverging > .bars .series-9  .bar-rect { fill: var(--div-4); }
.bar-chart--diverging > .bars .series-10 .bar-rect { fill: var(--div-3); }
.bar-chart--diverging > .bars .series-11 .bar-rect { fill: var(--div-2); }
.bar-chart--diverging > .bars .series-12 .bar-rect { fill: var(--div-1); }

/* ── Identity modes — drop the labels and/or the avatars ──────
   Three identity modes layer onto the default rotated pattern:

     --no-labels       hide the entire .chart-x-identity unit (axis
                       is just bars + ticks)
     --no-avatars      keep the tilted labels but suppress the avatar
                       (flex re-centers the label to the pivot)
     --avatars-only    HIDE the labels and show avatars HORIZONTAL
                       (un-rotated) directly under the bar centers

   For --avatars-only the consumer also authors a different identity
   markup PER BAR using .chart-x-identity.chart-x-identity--horizontal —
   no rotate() in the transform, foreignObject anchored to the avatar
   width centered on the bar's center x. See the .chart-x-identity--horizontal
   rule below for the layout shift.

   If labels are PERMANENTLY absent, prefer omitting the .chart-x-identity
   markup entirely — modifiers are for toggleable demos and runtime
   state. */
.bar-chart--no-labels .chart-x-identity { display: none; }
.bar-chart--no-avatars .chart-x-identity__content > .avatar,
.bar-chart--no-avatars .chart-x-identity__content > .avatar-tandem {
  display: none;
}
/* No rotated avatar-identity labels below the baseline, so the deep
   800/380 bottom margin (sized for those diagonal labels) is dead space.
   Tighten to 800/320 — same as --avatars-only. Flat category labels still
   fit; the live binder (dashboard-live.js → chartFrame) sizes the baseline's
   bottom margin to the labels actually present (48 with category labels,
   32 without — both keep plotH divisible by 16 so gridlines stay on the
   4-grid) rather than the avatar-bearing 108. */
.bar-chart--no-avatars { --bar-chart-aspect: 800 / 320; }

/* ── --avatars-only (wrapper) + --horizontal (per-bar) ────────
   Wrapper modifier signals "no labels; tight aspect" — labels are
   hidden via CSS, aspect tightens from 800/380 to 800/320 because
   we no longer need the diagonal label area below the baseline.

   The per-bar .chart-x-identity--horizontal marker is the implementation
   half — consumer authors the <g> without rotate(), with a tight
   foreignObject anchored to (avatar_size / 2) so the foreignObject
   centers on the bar's center x. The flex inside flips from
   justify-end to justify-center so the avatar sits in the middle
   of the (now tight) foreignObject.

   Two-part design (wrapper + per-bar marker) because SVG transform
   attributes can't be partially overridden via CSS — the per-bar
   transform="translate(N, M) rotate(-45)" lives in markup, so the
   markup must change anyway. The wrapper modifier is the SIGNAL;
   the per-bar marker is the SHAPE. */
.bar-chart--avatars-only {
  --bar-chart-aspect: 800 / 320;
}
.bar-chart--avatars-only .chart-x-identity__label { display: none; }
.chart-x-identity--horizontal .chart-x-identity__content {
  justify-content: center;
}

/* ── --label-first (wrapper) ──────────────────────────────────
   Flips the identity unit's visual order from [avatar][gap][label]
   to [label][gap][avatar] without changing markup. Implementation
   detail: CSS `order: -1` on the label — that bumps the SPAN ahead
   of the IMG in flex flow WITHOUT reversing the main axis. flex-
   direction stays `row` and justify-content stays `flex-end`, so the
   right-pack at the rotation pivot is unchanged: IMG anchors at the
   right edge (the bar's center x after -45° rotation) and SPAN sits
   to its left, extending down-left along the diagonal.

   Why not flex-direction: row-reverse? Reversing the main axis ALSO
   inverts what flex-end means (in row-reverse, flex-end packs items
   to the LEFT, not the right). The cluster would migrate to the LEFT
   half of the foreignObject and end up rotating below the SVG bottom
   edge — labels paint past the viewBox. `order` is the surgical
   answer: it swaps children visually without disturbing the axis.

   Avatar-on-bar centering AND baseline gap: two problems, one transform.

   1) justify-end packs the IMG's RIGHT EDGE at the rotation pivot, which
      means the avatar's GEOMETRIC CENTER sits half-an-avatar-width LEFT
      of the bar's center x. To align the avatar's center with the bar's
      center, we translate it half its own width to the right via
      `translateX(50%)` — a percentage transform self-adapts to whatever
      avatar size is in play (--xs / --sm / etc.).

   2) The rotation pivot is placed 16 SVG units BELOW the baseline, sized
      for the DEFAULT mode's LABEL to clear the baseline (the label's TOP
      after rotation sits just below the baseline; the avatar trails far
      down-left of the pivot at ~60 units below baseline). When --label-
      first moves the AVATAR to the pivot, the avatar inherits that small
      16-unit offset — only ~4 units of visible clearance between the
      baseline and the avatar's top. To match the default mode's breathing
      room, the avatar needs to be shifted DOWN by ~40 SVG units.

      The math gotcha: the parent's -45° rotation transforms any CSS
      translate. To shift the avatar by (0, +N) in SVG coords, the local-
      space translate must be (-N/√2, +N/√2) — equal LEFT and DOWN shifts
      in the unrotated frame. For N=40, that's (-28px, +28px). Combined
      with the 50% centering: translate(calc(50% - 28px), 28px).

   The foreignObject's right edge sits at the pivot, so the shifted avatar
   extends past the fO bounds vertically — overflow: visible on the fO
   keeps it visible (per the site's `svg { overflow: visible }` convention).
   The :not(.chart-x-identity--horizontal) filter excludes the avatars-only
   mode, where avatars are already center-justified inside a tight 24px-wide
   fO and don't need either fix.

   Choose --label-first when the avatar is the recognizable mark
   (chains, well-known tokens) and the label is supporting text. The
   default (avatar leading, label anchored at the bar) is the inverse —
   pick by which element should anchor to the data point. Markup is
   unchanged: <img class="avatar"> stays SOURCE-ORDER FIRST, <span
   class="chart-x-identity__label"> stays SOURCE-ORDER SECOND. Only
   the flex visual order flips, via `order`. */
.bar-chart--label-first .chart-x-identity__label {
  order: -1;
}
.bar-chart--label-first .chart-x-identity:not(.chart-x-identity--horizontal) > foreignObject {
  overflow: visible;
}
.bar-chart--label-first .chart-x-identity:not(.chart-x-identity--horizontal) .chart-x-identity__content {
  /* Close the flex gap so the rotated label sits flush against the avatar.
     Combined with the label's translate below, this eliminates the visible
     "floating" horizontal gap between the avatar (anchored at bar center) and
     the label (which would otherwise trail far down-left). */
  --identity-gap: 0;
}
.bar-chart--label-first .chart-x-identity:not(.chart-x-identity--horizontal) .chart-x-identity__content > .avatar,
.bar-chart--label-first .chart-x-identity:not(.chart-x-identity--horizontal) .chart-x-identity__content > .avatar-tandem {
  transform: translate(calc(50% - 28px), 28px);
}

/* ── Density tiers — Compact / default / Hero ─────────────────
   Three tiers along the chart-density axis. Compact for sidebar
   slots and dashboard tiles where vertical real estate is scarce;
   default for in-page sections; Hero for marketing decks and single-
   stage compositions where the chart is the page.

   The modifier owns chrome the wrapper can reach via the cascade:
   aspect ratio, gap, label font-size. AVATAR SIZE stays the
   consumer's call via the .avatar class on the <img> inside the
   <foreignObject> — author .avatar--xs (default) or .avatar--sm
   (hero); compact reuses .avatar--xs (the smallest tier currently
   in the avatar system). When .avatar--2xs lands in the avatar
   primitive, compact can adopt it. */
.bar-chart--compact {
  --bar-chart-aspect: 800 / 280;
  --bar-chart-gap:    var(--space-8);
}
.bar-chart--compact .chart-x-identity__label {
  font-size: var(--text-micro);
}
.bar-chart--hero {
  --bar-chart-aspect: 800 / 480;
  --bar-chart-gap:    var(--space-16);
}
.bar-chart--hero .chart-x-identity__label {
  font-size: var(--text-sm);
}

/* ── .chart-x-identity__content ───────────────────────────────────
   Inner HTML structure for the .chart-x-identity rotated SVG <g>.
   Embedded via <foreignObject> inside the SVG so flexbox can lay out
   the avatar + label without per-label pixel math.

   Structure (in HTML, inside <foreignObject>):
     <div class="chart-x-identity__content">
       <img class="chart-x-identity__avatar" src="…"/>
       <span class="chart-x-identity__label">Label</span>
     </div>

   Layout contract:
     • The <foreignObject>'s RIGHT EDGE sits at the rotation pivot
       (the bar's center x).
     • Flexbox `justify-content: flex-end` pushes the avatar + label
       to the RIGHT of the container — so the label always ends at
       the pivot regardless of its length.
     • `gap` between avatar and label is consistent across all bars.
     • Avatar comes FIRST in the source order (leading), label SECOND.
       After the right-pack, avatar sits to the left of the label
       within the flex row.
     • The whole unit rotates -45° via the parent <g>'s transform,
       so the visual reads: bar (top-right) → label → gap → avatar
       (bottom-left).

   Customization slots:
     --identity-gap         — gap between avatar and label
     --identity-avatar-size — circular avatar diameter
     --identity-label-color — label ink

   For TANDEM identities (token + chain badge), add a second <img>
   inside a wrapper <div class="chart-x-identity__avatar-tandem">
   with the chain badge as a relatively-positioned overlay. */
.chart-x-identity__content {
  --identity-gap:          var(--space-8);
  --identity-height:       var(--space-24);  /* matches .avatar--xs */
  --identity-label-color:  var(--text-faint);

  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--identity-gap);
  height: var(--identity-height);
  width: 100%;
}
/* Avatar rendering goes through the canonical .avatar primitive — drop
   in `class="avatar avatar--sm avatar--circle"` (or any size + shape
   modifier combo). The flex layout above sizes the row around it.
   For tandem identities (token + chain badge), use `.avatar-tandem`
   instead of a single `.avatar`. */
.chart-x-identity__label {
  font-family: var(--font-sans);
  font-size: var(--text-caption);
  font-weight: var(--fw-medium);
  line-height: 1;
  color: var(--identity-label-color);
  letter-spacing: var(--tracking-normal);
  white-space: nowrap;
}

/* ── .bars playground ────────────────────────────────────
   Same layout pattern as the area chart playground. Reuses the
   shared .area-playground-* classes so the control panel,
   toggles, and stage visuals stay identical between the two
   playgrounds — a user moves between them without re-learning. */
.bars-playground {
  display: grid;
  grid-template-columns: 260px 1fr;
  gap: var(--space-24);
  align-items: start;
}
.bars-playground-stage {
  padding: var(--space-24);
  border-radius: var(--space-12);
  background: color-mix(in oklch, var(--glass-base), white 2%);
  border: 1px solid color-mix(in oklch, var(--text-primary) 8%, transparent);
  min-height: 280px;
  display: flex;
  flex-direction: column;
  gap: var(--space-16);
}
.bars-playground-stage .bars { min-height: 220px; }
@media (max-width: 900px) {
  .bars-playground { grid-template-columns: 1fr; }
}

/* ── .radial-chart ────────────────────────────────────────
   Supersedes the previous .donut component. SVG-based radial
   chart with rounded stroke-caps, covering the full shadcn/ui
   radial-chart matrix via composable modifier classes:

     .radial-chart              single progress ring (default)
     .radial-chart--label       + absolutely-positioned center label
     .radial-chart--grid        + concentric grid circles behind
     .radial-chart--text        + big hero number centered
     .radial-chart--stacked     concentric rings (multi-series)
     .radial-chart--segments    full donut split into slice segments
     .radial-chart--half        semi-circle (anchored bottom)

   Configurable via --rd-* CSS custom properties on the element:
     --rd-size / --rd-radius / --rd-thickness / --rd-gap
     --rd-track-color / --rd-bar-color / --rd-value-size
   Override per-instance without forking the class.

   SVG math: viewBox 0 0 120 120; ring centered at (60,60).
   Circumference for r = (120/2 - thickness/2) → SVG `stroke-dasharray`
   drives the fill percentage. Rotate SVG -90° so arcs start at 12.
   ─────────────────────────────────────────────────────────── */
.radial-chart {
  /* Configuration tokens */
  --rd-size: 160px;
  --rd-thickness: 14;
  --rd-gap: 6;           /* gap between stacked rings (SVG units) */
  --rd-track-color: color-mix(in oklch, var(--text-primary) 8%, transparent);
  --rd-bar-color: var(--accent-primary);
  --rd-value-size: var(--text-h2);
  --rd-caption-color: var(--text-muted);
  --rd-label-gap: var(--space-4);

  position: relative;
  display: inline-block;
  width: var(--rd-size);
  aspect-ratio: 1;
  flex-shrink: 0;
}
.radial-chart > svg {
  width: 100%;
  height: 100%;
  transform: rotate(-90deg);
  overflow: visible;
}

/* Background track — always behind the data bar */
.radial-track {
  fill: none;
  stroke: var(--rd-track-color);
}

/* Data bar — the filled arc. Rounded caps give the pill end used
   by shadcn; use --linecap="butt" for segmented pie-style charts. */
.radial-bar {
  fill: none;
  stroke: var(--rd-bar-color);
  stroke-linecap: round;
  transition: stroke-dashoffset 0.5s cubic-bezier(0.22, 1, 0.36, 1),
              stroke 0.2s ease, filter 0.15s ease;
}
.radial-bar:hover { filter: brightness(1.15); }

/* Segments — pie-style, butt caps so slices meet edge-to-edge */
.radial-segment {
  fill: none;
  stroke-linecap: butt;
  transition: stroke-dashoffset 0.4s ease, filter 0.15s ease;
}
.radial-segment:hover { filter: brightness(1.12); }

/* Grid variant — concentric background circles under the bar */
.radial-grid {
  fill: none;
  stroke: color-mix(in oklch, var(--text-primary) 5%, transparent);
  stroke-width: 0.75;
}

/* Center label — absolutely-positioned DIV, upright regardless of
   the SVG rotation (which is what makes the arc start at 12). */
.radial-label {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--rd-label-gap);
  pointer-events: none;
  text-align: center;
  padding: 12%;
}
.radial-value {
  font-size: var(--rd-value-size);
  line-height: 1;
  font-weight: var(--fw-extrabold);
  letter-spacing: var(--tracking-tighter);
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
}
.radial-caption {
  font-size: var(--text-caption);
  line-height: var(--lh-caption);
  color: var(--rd-caption-color);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  font-weight: var(--fw-semibold);
}
/* Optional delta chip below the caption — e.g. "+12.4% MoM" */
.radial-delta {
  display: inline-flex;
  align-items: center;
  gap: var(--space-4);
  margin-top: var(--space-4);
  padding: var(--space-4) var(--space-8);
  border-radius: var(--r-full);
  font-size: var(--text-caption);
  font-weight: var(--fw-semibold);
  font-variant-numeric: tabular-nums;
  background: color-mix(in oklch, currentColor 12%, transparent);
}
.radial-delta--pos { color: var(--success); }
.radial-delta--neg { color: var(--danger); }

/* Size modifiers — scale value typography + container width together */
.radial-chart--sm  {
  --rd-size: 100px;
  --rd-value-size: var(--text-h3);
}
.radial-chart--lg  {
  --rd-size: 240px;
  --rd-value-size: var(--text-h1);
}

/* Text variant — massive hero number (one metric, one glance) */
.radial-chart--text .radial-value {
  font-size: var(--text-display);
  font-weight: var(--fw-black);
  letter-spacing: var(--tracking-tighter);
}
.radial-chart--text.radial-chart--sm .radial-value {
  font-size: var(--text-h1);
}

/* Half-circle variant — anchors to the bottom (uses a gauge-style
   half-arc path in markup; CSS just adjusts aspect-ratio + label). */
.radial-chart--half {
  aspect-ratio: 2 / 1.15;
  width: 220px;
}
.radial-chart--half > svg {
  transform: none;
}
.radial-chart--half .radial-label {
  inset: auto 0 10% 0; /* % units exempt from grid */
  top: auto;
}

/* ── Deprecated donut alias — kept one release cycle to avoid
   breaking anything that still references the old class. Points
   at the same visual treatment; migrate new code to .radial-chart. */
.donut { display: inline-block; width: 160px; aspect-ratio: 1; position: relative; }
.donut--sm { width: 100px; }
.donut--lg { width: 240px; }
.donut > svg { width: 100%; height: 100%; transform: rotate(-90deg); }
.donut-track { fill: none; stroke: color-mix(in oklch, var(--text-primary) 8%, transparent); }
.donut-segment { fill: none; stroke-linecap: butt; transition: stroke-dashoffset 0.4s ease; }
.donut-label { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-4); text-align: center; pointer-events: none; }
.donut-value { font-size: var(--text-h2); line-height: 1; font-weight: var(--fw-extrabold); letter-spacing: var(--tracking-tighter); font-variant-numeric: tabular-nums; color: var(--text-primary); }
.donut-caption { font-size: var(--text-caption); color: var(--text-muted); text-transform: uppercase; letter-spacing: var(--tracking-wide); font-weight: var(--fw-semibold); }

/* ── .gauge ──────────────────────────────────────────────
   Semi-circular 0→100 gauge. Same dashoffset trick as the donut
   but arc is drawn across a half-circle, so the fill visibly
   "fills up" from left to right. */
.gauge {
  position: relative;
  display: inline-block;
  width: 220px;
  aspect-ratio: 2 / 1.15;
}
.gauge > svg {
  width: 100%;
  height: 100%;
  overflow: visible;
}
.gauge-track {
  fill: none;
  stroke: color-mix(in oklch, var(--text-primary) 10%, transparent);
  stroke-linecap: round;
}
.gauge-fill {
  fill: none;
  stroke: var(--accent-primary);
  stroke-linecap: round;
  transition: stroke-dashoffset 0.4s ease, stroke 0.2s ease;
}
.gauge--success .gauge-fill { stroke: var(--success); }
.gauge--warn    .gauge-fill { stroke: var(--warn); }
.gauge--danger  .gauge-fill { stroke: var(--danger); }
.gauge-label {
  position: absolute;
  left: 0; right: 0;
  bottom: 6%;
  text-align: center;
}
.gauge-value {
  font-size: var(--text-h1);
  line-height: 1;
  font-weight: var(--fw-extrabold);
  letter-spacing: var(--tracking-tighter);
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
}
.gauge-caption {
  font-size: var(--text-caption);
  line-height: var(--lh-caption);
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  font-weight: var(--fw-semibold);
  margin-top: 4px;
}

/* ── .heatmap ────────────────────────────────────────────
   CSS-grid matrix. Cells carry `data-intensity="0…4"` and the
   background is derived from --accent-primary via color-mix, so
   heatmaps stay on-brand across every Theme Composer theme.
   Good for week-in-review / activity patterns. */
.heatmap {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: repeat(var(--cols, 7), 1fr);
}
/* .heatmap--calendar — GitHub-style calendar grid. 7 rows of day-of-
   week × N columns of week-since-window-start. Every cell sets its
   own `--row` (1=Sun … 7=Sat) and `--col` (1-based week index) via
   `data-dash-var`, so cells lay out as a true day-by-day matrix
   regardless of which weekday the window starts on. The column count
   is set on the host via `--cols` (matches calendarCols in the data
   feed). */
.heatmap--calendar {
  grid-template-columns: repeat(var(--cols, 13), 1fr);
  grid-template-rows: repeat(7, 1fr);
  grid-auto-flow: column;
}
.heatmap--calendar .heatmap-cell {
  grid-row: var(--row, auto);
  grid-column: var(--col, auto);
}
.heatmap-cell {
  aspect-ratio: 1;
  border-radius: var(--space-4);
  border: 1px solid color-mix(in oklch, var(--text-primary) 4%, transparent);
  transition: transform 0.15s ease, filter 0.15s ease;
  cursor: default;
}
.heatmap-cell:hover {
  transform: scale(1.15);
  filter: brightness(1.15);
  z-index: 1;
  position: relative;
}
.heatmap-cell[data-intensity="0"] {
  background: color-mix(in oklch, var(--text-primary) 5%, transparent);
}
.heatmap-cell[data-intensity="1"] {
  background: color-mix(in oklch, var(--accent-primary) 22%, transparent);
}
.heatmap-cell[data-intensity="2"] {
  background: color-mix(in oklch, var(--accent-primary) 42%, transparent);
}
.heatmap-cell[data-intensity="3"] {
  background: color-mix(in oklch, var(--accent-primary) 68%, transparent);
}
.heatmap-cell[data-intensity="4"] {
  background: var(--accent-primary);
}
/* Heatmap legend strip — companion strip showing the 5-step scale. */
.heatmap-scale {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: var(--text-caption);
  color: var(--text-muted);
  letter-spacing: var(--tracking-wide);
}
.heatmap-scale .heatmap-cell {
  width: var(--space-12);
  aspect-ratio: 1;
  transform: none !important;
}

/* ── .chart-legend ───────────────────────────────────────
   Swatch + label rows. Default is horizontal (wraps to multi-row
   on narrow screens); --stack forces vertical. Swatch shape is
   modifier-driven: rect (default) / line / dot. Optional value
   column right-aligns numbers for at-a-glance comparison. */
.chart-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-16);
}
.chart-legend--stack {
  flex-direction: column;
  flex-wrap: nowrap;
  gap: var(--space-8);
  align-items: stretch;
}
.chart-legend-item {
  display: inline-flex;
  align-items: center;
  gap: var(--space-8);
  font-size: var(--text-sm);
  line-height: var(--lh-sm);
  color: var(--text-muted);
  min-width: 0;
}
.chart-legend-swatch {
  width: var(--space-12);
  height: var(--space-12);
  border-radius: var(--space-4);
  flex-shrink: 0;
  background: currentColor;
}
.chart-legend-swatch--line {
  height: var(--space-4);
  width: var(--space-16);
  border-radius: var(--space-4);
}
.chart-legend-swatch--dot {
  border-radius: 50%;
}
.chart-legend-label {
  color: var(--text-primary);
  font-weight: var(--fw-medium);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
.chart-legend-value {
  color: var(--text-primary);
  font-weight: var(--fw-semibold);
  font-variant-numeric: tabular-nums;
  margin-left: auto;
}
.chart-legend-delta {
  font-size: var(--text-caption);
  line-height: var(--lh-caption);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--tracking-tight);
}


/* ── .chart-tooltip ──────────────────────────────────────
   Floating hover panel for charts: category label + per-series
   rows (indicator · name · value), optional total footer.
   Inspired by shadcn/ui ChartTooltipContent, restyled to LI.FI
   tokens — panel glass, alpha borders, tabular-nums for values.

   Base markup:
     <div class="chart-tooltip">
       <div class="chart-tooltip__label">Jan 15, 2026</div>
       <ul class="chart-tooltip__list">
         <li class="chart-tooltip__item" style="--swatch: var(--series-1);">
           <span class="chart-tooltip__indicator"></span>
           <span class="chart-tooltip__name">Bridges</span>
           <span class="chart-tooltip__value">$12,430</span>
         </li>
       </ul>
     </div>

   Container modifiers:
     --hide-label       omit the category header
     --hide-indicator   collapse the swatch column
     --stack            name above value (hero-metric / single-series)
     --sm               compact padding + smaller type

   Indicator variants (on .chart-tooltip__indicator):
     default            solid 10×10 · 2px radius square
     --dot              circle (50% radius)
     --line             3×14 vertical bar — matches line-chart stroke
     --dashed           2px dashed vertical stroke, no fill
     --icon             14×14 slot for an inline SVG (category legends)

   Footer row:
     .chart-tooltip__item--total    heavier weight + top border */
.chart-tooltip {
  --chart-tooltip-bg:     var(--nav-panel-bg);
  --chart-tooltip-border: var(--alpha-border-subtle);

  display: grid;
  gap: var(--space-8);
  min-width: 176px;
  max-width: 280px;
  padding: var(--space-12);
  border-radius: var(--space-12);
  border: 1px solid var(--chart-tooltip-border);
  background: var(--chart-tooltip-bg);
  backdrop-filter: blur(12px) saturate(1.2);
  -webkit-backdrop-filter: blur(12px) saturate(1.2);
  box-shadow:
    0 16px 48px -12px oklch(0% 0 0 / 0.55),
    0 2px 4px oklch(0% 0 0 / 0.3);
  font-family: var(--font-sans);
  /* Base type — caption scale (12 px). The tooltip is chart chrome, not
     editorial copy, but the 12 px floor from the formal-restraint rule
     still applies: nothing inside a tooltip goes below --text-caption. */
  font-size: var(--text-caption);
  line-height: var(--lh-caption);
  color: var(--text-primary);
}
[data-theme="light"] .chart-tooltip {
  box-shadow:
    0 16px 48px -12px oklch(0% 0 0 / 0.18),
    0 2px 4px oklch(0% 0 0 / 0.08);
}

/* Size — compact. Tightens padding + gap, never drops type below the
   caption floor. A denser tooltip lives in the same type scale. */
.chart-tooltip--sm {
  min-width: 140px;
  padding: var(--space-8) var(--space-12);
  gap: var(--space-8);
  /* font-size stays var(--text-caption) — see base rule */
}

/* Floating — cursor-following hover variant. The base primitive is just the
   panel; this adds the positioning + show/hide an interactive consumer needs.
   Positioned in viewport-local px by JS (LifiLineAreaCharts.attachInteractive)
   inside the chart wrapper (position: relative); centered above the point. */
.chart-tooltip--floating {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 5;
  transform: translate(-50%, calc(-100% - var(--space-8)));
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.12s ease;
}
.chart-tooltip--floating.is-visible { opacity: 1; }

/* Header label — caption weight 600 (same scale, heavier weight) */
.chart-tooltip__label {
  font-size: var(--text-caption);
  line-height: var(--lh-caption);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-snug);
  color: var(--text-muted);
}
.chart-tooltip--hide-label .chart-tooltip__label { display: none; }

/* Items list */
.chart-tooltip__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-8);
}

/* Per-series row */
.chart-tooltip__item {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--space-8);
  min-width: 0;
}
.chart-tooltip--hide-indicator .chart-tooltip__item {
  grid-template-columns: 1fr auto;
}
.chart-tooltip--hide-indicator .chart-tooltip__indicator { display: none; }

/* Color indicator — series swatch via --swatch custom property */
.chart-tooltip__indicator {
  --swatch: var(--accent-primary);
  width: var(--space-12);
  height: var(--space-12);
  border-radius: var(--space-4);
  background: var(--swatch);
  flex-shrink: 0;
  align-self: center;
}
.chart-tooltip__indicator--dot {
  border-radius: 50%;
}
.chart-tooltip__indicator--line {
  width: var(--space-4);
  height: var(--space-12);
  border-radius: var(--space-4);
}
.chart-tooltip__indicator--dashed {
  width: 0;
  height: var(--space-12);
  background: transparent;
  border-left: 2px dashed var(--swatch);
  border-radius: 0;
}
.chart-tooltip__indicator--icon {
  width: var(--space-12);
  height: var(--space-12);
  background: transparent;
  color: var(--swatch);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 0;
}
.chart-tooltip__indicator--icon svg {
  width: 100%;
  height: 100%;
  display: block;
}

/* Series name */
.chart-tooltip__name {
  color: var(--text-muted);
  font-weight: var(--fw-regular);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

/* Value — right-aligned, tabular */
.chart-tooltip__value {
  color: var(--text-primary);
  font-weight: var(--fw-semibold);
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
}

/* Stack layout — name above value, for hero/single-series tooltips */
.chart-tooltip--stack .chart-tooltip__item {
  grid-template-columns: auto 1fr;
  grid-template-rows: auto auto;
  row-gap: var(--space-4);
}
.chart-tooltip--stack .chart-tooltip__indicator {
  grid-row: 1 / span 2;
  align-self: start;
  margin-top: var(--space-4);
}
.chart-tooltip--stack .chart-tooltip__name {
  grid-column: 2;
  grid-row: 1;
}
.chart-tooltip--stack .chart-tooltip__value {
  grid-column: 2;
  grid-row: 2;
  text-align: left;
  /* Hero value in stack mode — bumps from caption (12) to body (16),
     one step up the type scale. The surrounding caption-scale chrome
     anchors the hierarchy so the value reads as the headline. */
  font-size: var(--text-body);
  line-height: var(--lh-caption);
  font-weight: var(--fw-bold);
}

/* Total footer row */
.chart-tooltip__item--total {
  padding-top: var(--space-8);
  margin-top: var(--space-4);
  border-top: 1px solid var(--alpha-border-subtle);
}
.chart-tooltip__item--total .chart-tooltip__name {
  color: var(--text-primary);
  font-weight: var(--fw-semibold);
}
.chart-tooltip__item--total .chart-tooltip__value {
  /* One step up from caption to small — the total is emphasized
     but not hero-level. Stays within the type scale, no raw px. */
  font-size: var(--text-sm);
  line-height: var(--lh-sm);
}

/* Muted sublabel (e.g. secondary value or formatter hint).
   Stays at caption — the 12 px floor applies to all chart-tooltip
   content. If a sub-value needs to feel visually lighter, use opacity
   / color, not a smaller type size. */
.chart-tooltip__sub {
  color: var(--text-faint);
  font-size: var(--text-caption);
  line-height: var(--lh-caption);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--tracking-tight);
}


@media (max-width: 600px) {
  .stat-card-value { font-size: var(--text-h3); line-height: var(--lh-h3); }
  .stat-card--lg .stat-card-value { font-size: var(--text-h2); line-height: var(--lh-h2); }
}


/* ══════════════════════════════════════════════════════════════════════════════
   8d. AVATAR — .avatar  (tier-2 universal; .ds-doc-logo → .ds-doc-avatar Apr 2026
                          → .avatar May 2026, promoted out of catalog-meta tier
                          once production consumers were confirmed in index.html
                          and playground.html)
   ════════════════════════════════════════════════════════════════════════════
   Plated brand-mark tiles for chains, tokens, bridges, DEXs, wallets, social
   networks, and LI.FI sub-products. Every avatar ships as a square SVG
   (32×32 viewBox) pre-plated with its brand background colour — no wrapper
   tint required. Shape + size come from modifiers; the source file is
   unchanged.

   Asset library lives at `avatars/`:
     marks/chains/    — blockchain marks
     marks/tokens/    — token-ticker marks
     avatars/bridges/   — bridge protocols
     avatars/dexs/      — DEX protocols
     avatars/wallets/   — wallet apps
     marks/social/    — social-network marks
     marks/lifi/      — LI.FI sub-product marks

   Full-logo brand wordmarks (varied aspect ratio, dark + light pairs) live
   in `logos/partners/` and use `.ds-doc-partner-logo` (defined below) — never
   try to render those through `.avatar` (would crop the wordmark).

   Filenames use kebab-case, single-word chain names stay as-is
   (`ethereum.svg`, `base.svg`). Multi-word names replace separators with
   `-` (`arbitrum-nova.svg`, `polygon-zkevm.svg`).

   Markup:
     <img class="avatar avatar--md avatar--circle"
          src="avatars/chains/ethereum.svg" alt="Ethereum">

   Defaults:
     size  = md (40px — was 32px before Apr 2026; bumped so catalog previews
             read confidently and 32px viewBox SVGs still render crisply)
     shape = square (4px radius, follows --radius-sm)

   Use `.avatar-stack` to overlap a row of avatars ("integrated with N chains"
   marketing pattern). Pairs with `.chart-legend-icon` for legend contexts.
   ════════════════════════════════════════════════════════════════════════════ */
/* `--avatar-size` is consumed via fallback rather than hard-set on
   .avatar itself. That lets a parent (.avatar-stack, .avatar-tandem)
   set the cluster's size in one place — children inherit. A child can
   still override with its own .avatar--xs/--sm/--lg/--xl/--2xl modifier. */
.avatar {
  --avatar-radius: 4px;
  width: var(--avatar-size, 40px);
  height: var(--avatar-size, 40px);
  border-radius: var(--avatar-radius);
  object-fit: cover;
  overflow: hidden;
  flex-shrink: 0;
  display: block;
  /* `--avatar-bg` is the per-instance brand-tint slot — e.g., the chain or
     token's identity colour when the avatar renders as a letter-block
     placeholder (no SVG loaded). Falls back to the neutral fallback tint
     used while a real SVG is loading. Authors set it via inline style on
     the avatar element (`style="--avatar-bg:#627EEA"`); the canonical
     hex avoids hard-baking a brand colour into styles.css. */
  background: var(--avatar-bg, var(--alpha-dim));
}

/* Sizes — six tiers tuned for the cases that ship: 24 / 32 / 40 / 48 /
   64 / 96. Default (no modifier) = 40px. */
.avatar--xs  { --avatar-size: 24px; }   /* dense lists, table cells */
.avatar--sm  { --avatar-size: 32px; }   /* lists, compact rows */
/* (default)   --avatar-size: 40px       lists, marquee, common UI */
.avatar--lg  { --avatar-size: 48px; }   /* card thumbnails, stat tiles */
.avatar--xl  { --avatar-size: 64px; }   /* hero rows, feature cards */
.avatar--2xl { --avatar-size: 96px; }   /* marketing heroes, brand library */

/* Shapes — square (sharp), rounded (squircle-ish), circle. */
.avatar--square  { --avatar-radius: 4px; }
.avatar--rounded { --avatar-radius: 25%; }
.avatar--circle  { --avatar-radius: 50%; }

/* 1px alpha ring — adds a subtle inner edge that separates the plate
   from surrounding UI on low-contrast surfaces (e.g. Ethereum's
   purple bg next to --surface-card). Always safe to opt-in. */
.avatar--ring {
  box-shadow: inset 0 0 0 1px var(--alpha-border-subtle);
}

/* Plate modifiers — colored ground behind a transparent-PNG avatar
   (people photos, alpha logos). The plate shows through wherever the
   image has alpha, and is invisible behind opaque images (JPEG, plated
   chain SVG) — so applying a plate to a JPEG is a no-op, not a bug.

   Every modifier routes through the existing `--avatar-bg` slot, so
   an inline `style="--avatar-bg:<color>"` is still the escape hatch for
   one-off custom plates. The default (no modifier) keeps the existing
   `var(--alpha-dim)` fallback — a subtle neutral plate, NOT transparent.
   Reach for `--plate-none` only when you want a free-floating silhouette.

   Three tiers — role-named, not paint-named, so the modifier describes
   intent (primary / secondary / tertiary) rather than the rendered hue:
     • Accent trio (primary / secondary / tertiary) — consumes the
       LI.FI 1.0 brand tokens (--lifi-sapphire / --lifi-pink / --lifi-ink),
       so secondary + tertiary shift darker in light mode for WCAG.
     • Neutral pair (ink / paper)      — OKLCH literals at hue 268 with
       chroma ~0; same look in both themes so ink stays deep and paper
       stays light regardless of mode.
     • Gradient trio (brand / brand-rich / brand-radial) — brand-grounded
       gradients; radial is the teal-centred vignette that re-creates the
       original Imagen portrait ground (lighter halo behind the face). */
.avatar--plate-primary      { --avatar-bg: var(--lifi-sapphire); }
.avatar--plate-secondary    { --avatar-bg: var(--lifi-pink); }
.avatar--plate-tertiary     { --avatar-bg: var(--lifi-ink); }
.avatar--plate-ink          { --avatar-bg: oklch(20% 0.015 268); }
.avatar--plate-paper        { --avatar-bg: oklch(95% 0.005 268); }
.avatar--plate-brand        { --avatar-bg: var(--grad-brand); }
.avatar--plate-brand-rich   { --avatar-bg: var(--grad-brand-rich); }
.avatar--plate-brand-radial    { --avatar-bg: radial-gradient(circle at 50% 35%, var(--lifi-pink) 0%, var(--lifi-sapphire) 100%); }
.avatar--plate-spectral-radial { --avatar-bg: var(--grad-spectral-radial); }   /* rose → orange → lime → teal */
.avatar--plate-none            { --avatar-bg: transparent; }    /* opts out of the --alpha-dim fallback */

/* Placeholder modifier — generic "unselected slot" tint.
   When a picker hasn't received a selection yet (Send/Receive token
   chip, chain selector, wallet badge), the slot reads as a calm muted
   disc instead of a real brand mark. The empty form IS the affordance;
   pair the chip with a "Select" label.

   Recipe: --avatar-bg drops to a `color-mix(--text-primary, transparent)`
   tint that sits one step above the .chip-avatar-neutral surface
   (8% dark / 6% light), so the disc reads against the pill without
   competing with real content. Theme-aware via the [data-theme="light"]
   override.

   Standalone use (no chip wrapper) renders a subtle gray disc against
   --surface-card — same recipe, no extra rules. */
.avatar--placeholder {
  --avatar-bg: color-mix(in oklch, var(--text-primary) 18%, transparent);
}
[data-theme="light"] .avatar--placeholder {
  --avatar-bg: color-mix(in oklch, var(--text-primary) 14%, transparent);
}

/* Tandem variant — apply the same tint to the base avatar AND the
   corner badge in one declaration on the wrapper. .avatar-badge has
   its own `background:` (not routed through --avatar-bg), so the badge
   bg is set directly. The 2 px transparent cutout between base and
   badge still shows whatever surface sits behind, so the two same-tint
   shapes read as separate discs without an outline. */
.avatar-tandem--placeholder > .avatar:first-child {
  --avatar-bg: color-mix(in oklch, var(--text-primary) 18%, transparent);
}
.avatar-tandem--placeholder > .avatar-badge {
  background: color-mix(in oklch, var(--text-primary) 18%, transparent);
}
[data-theme="light"] .avatar-tandem--placeholder > .avatar:first-child {
  --avatar-bg: color-mix(in oklch, var(--text-primary) 14%, transparent);
}
[data-theme="light"] .avatar-tandem--placeholder > .avatar-badge {
  background: color-mix(in oklch, var(--text-primary) 14%, transparent);
}

/* ── .avatar--initial — letter-block identity variant ──────────────
   The canonical monogram avatar: 1–2 letters derived from a label
   (person, org, integration) on a deterministic spectral plate. The
   ONE primitive that replaces the page-local letter-blocks that kept
   drifting (`.portal-initial`, inline `--avatar-bg` radials). Use it
   wherever a user / org / integration has no real photo or mark —
   the Gmail / Slack / Linear convention.

   Colour comes from a `--avatar-tone` slot (one of the 12 categorical
   `--spectral-*` hues). The deterministic label→tone mapping + the
   initials live in `shared.js → initInitialAvatars()` / window
   .lifiInitialAvatar() — author markup with `data-initial="<label>"`
   and the JS fills the glyph + `.avatar--tone-N` class. For a fixed
   tone (System, brand), set `.avatar--tone-N` (or `--tone-neutral`)
   in markup and the JS leaves it alone.

   White-text contrast: the spectral DARK aliases are bright pastels
   (L≈0.70–0.84) that fail white text, so the plate NORMALISES the
   tone's lightness down to a deep, white-safe band via relative-
   colour `oklch(from … min(l, …) c h)`. Same value in both themes
   (the cap pulls dark-mode brights down; light-mode tones already
   sit below it), so the disc reads identically light/dark while the
   hue stays full-chroma. The glyph is decorative + redundant (the
   visible name sits beside it) — host on a `<span aria-hidden="true">`.

   Glyph scales with the disc: `font-size` is a ratio of --avatar-size
   (like the avatar's own size system), the one calc-derived type value
   here — a 2-letter monogram must track the disc diameter at every
   tier, which a fixed --text-* token can't do. */
.avatar--initial {
  --avatar-tone: var(--spectral-9);            /* default = blue; JS/markup overrides */
  --avatar-initial-floor: oklch(from var(--avatar-tone) min(l, 0.46) c h);  /* deep base — white-safe */
  --avatar-initial-hi:    oklch(from var(--avatar-tone) min(l, 0.56) c h);  /* upper highlight */
  --avatar-bg: radial-gradient(circle at 50% 30%,
                 var(--avatar-initial-hi) 0%,
                 var(--avatar-initial-floor) 100%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-sans);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-tight);
  line-height: 1;
  text-transform: uppercase;
  color: oklch(100% 0 0);                       /* white — clears the white-safe floor */
  font-size: calc(var(--avatar-size, 40px) * 0.4);
  -webkit-user-select: none;
          user-select: none;
}

/* Categorical tone classes — one per --spectral-* hue (coral → rose).
   The deterministic label hash in shared.js picks one of these. */
.avatar--tone-1  { --avatar-tone: var(--spectral-1);  }   /* coral   */
.avatar--tone-2  { --avatar-tone: var(--spectral-2);  }   /* orange  */
.avatar--tone-3  { --avatar-tone: var(--spectral-3);  }   /* amber   */
.avatar--tone-4  { --avatar-tone: var(--spectral-4);  }   /* lime    */
.avatar--tone-5  { --avatar-tone: var(--spectral-5);  }   /* green   */
.avatar--tone-6  { --avatar-tone: var(--spectral-6);  }   /* teal    */
.avatar--tone-7  { --avatar-tone: var(--spectral-7);  }   /* cyan    */
.avatar--tone-8  { --avatar-tone: var(--spectral-8);  }   /* sky     */
.avatar--tone-9  { --avatar-tone: var(--spectral-9);  }   /* blue    */
.avatar--tone-10 { --avatar-tone: var(--spectral-10); }   /* violet  */
.avatar--tone-11 { --avatar-tone: var(--spectral-11); }   /* magenta */
.avatar--tone-12 { --avatar-tone: var(--spectral-12); }   /* rose    */

/* Flat opt-out — solid fill instead of the radial. Crisper at tiny
   table-cell sizes; reach for it in dense rows. (0,2,0) wins over the
   base radial; composes with a tone class (flat + --tone-N → solid hue). */
.avatar--initial.avatar--flat {
  --avatar-bg: var(--avatar-initial-floor);
}

/* Neutral tone — non-human / system entities (no identity colour).
   Muted ink on a quiet grey plate, theme-aware; mirrors the retired
   `.portal-initial--system` look. Declared AFTER --flat so it wins when
   both are present (neutral is inherently flat — no radial to suppress). */
.avatar--initial.avatar--tone-neutral {
  --avatar-bg: color-mix(in oklch, var(--text-primary) 12%, var(--surface-raised));
  color: var(--text-muted);
}

/* ── .avatar-stack — horizontal overlap cluster ────────────────────
   The "integrated with N chains" pattern. Each avatar after the
   first shifts left 30% of its width; the page-coloured outline
   keeps them visually distinct.

   Sizing: --avatar-size flows from the wrapper to all child .avatar
   elements. Default 40px (no modifier); the size-modifier ladder
   mirrors .avatar's. A child can override the cluster size with
   its own .avatar--xs/--sm/--lg/--xl/--2xl modifier. Count is
   markup — N children = N-avatar stack. */
.avatar-stack {
  display: inline-flex;
  align-items: center;
  padding-left: 0;
}
.avatar-stack > .avatar {
  position: relative;
}
.avatar-stack > .avatar + .avatar {
  margin-left: calc(var(--avatar-size, 40px) * -0.30);
}
.avatar-stack--tight > .avatar + .avatar {
  margin-left: calc(var(--avatar-size, 40px) * -0.45);
}
.avatar-stack--xs  { --avatar-size: 24px; }
.avatar-stack--sm  { --avatar-size: 32px; }
.avatar-stack--lg  { --avatar-size: 48px; }
.avatar-stack--xl  { --avatar-size: 64px; }
.avatar-stack--2xl { --avatar-size: 96px; }

/* True cutout via mask-image — every avatar except the LAST has a
   circular hole punched where the next avatar overlaps. The hole is
   transparent (not page-coloured), so whatever surface sits behind
   the stack shows through — works on cards, panels, tinted
   backgrounds, anywhere. Replaces the older box-shadow ring approach
   that hardcoded --surface-page and broke on non-page surfaces.

   Geometry — next avatar's center sits at:
     default 30% overlap → 120% horizontal (next.left = 70% of this.width;
                                            next.center = 70% + 50% = 120%)
     --tight 45% overlap → 105% horizontal
   Cutout radius = next avatar radius + 2px gap = (--avatar-size × 0.5) + 2px. */
.avatar-stack > .avatar:not(:last-child) {
  --avatar-cutout-pos: 120%;
  -webkit-mask-image: radial-gradient(
    circle round(calc(var(--avatar-size, 40px) * 0.5 + 2px), 1px)
    at var(--avatar-cutout-pos) 50%,
    transparent 99.5%, black 100%
  );
  mask-image: radial-gradient(
    circle round(calc(var(--avatar-size, 40px) * 0.5 + 2px), 1px)
    at var(--avatar-cutout-pos) 50%,
    transparent 99.5%, black 100%
  );
}
.avatar-stack--tight > .avatar:not(:last-child) {
  --avatar-cutout-pos: 105%;
}

/* ── .avatar-tandem — base avatar + corner badge(s) ────────────────
   The token+chain pattern: a primary avatar (the token) with a
   smaller avatar (the chain) clipping a corner. Compose ANY number
   of badges; the canonical case is one (token + host chain in
   .avatar-badge--br), bridging surfaces use two (source + destination
   in --tr + --br).

   Markup:
     <span class="avatar-tandem avatar-tandem--lg">
       <img class="avatar avatar--circle" src="usdc.svg">
       <img class="avatar-badge avatar-badge--br" src="ethereum.svg">
     </span>

   Sizing: --avatar-size flows from the wrapper to the base .avatar
   AND to .avatar-badge (which renders at 42% of that size). Default
   40px (no modifier); the size-modifier ladder mirrors .avatar's. */
.avatar-tandem {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--avatar-size, 40px);
  height: var(--avatar-size, 40px);
  flex-shrink: 0;
}
.avatar-tandem--xs  { --avatar-size: 24px; }
.avatar-tandem--sm  { --avatar-size: 32px; }
.avatar-tandem--lg  { --avatar-size: 48px; }
.avatar-tandem--xl  { --avatar-size: 64px; }
.avatar-tandem--2xl { --avatar-size: 96px; }

/* Badge — small avatar attached to a corner. ~40% of the base size,
   circular shape, nudges slightly outward (10% of badge size = 4%
   of parent) so it reads as "clipping the corner" without overhanging
   enough to risk being clipped by parent containers. Visual
   separation from the base comes from a true cutout (mask-image
   rule below), not from an outline — so the gap shows whatever
   surface sits behind the tandem (page, card, tinted panel, etc.),
   not a hardcoded page-coloured ring. */
.avatar-badge {
  position: absolute;
  width: calc(var(--avatar-size, 40px) * 0.40);
  height: calc(var(--avatar-size, 40px) * 0.40);
  border-radius: 50%;
  background: var(--alpha-dim);   /* fallback tint while the SVG loads, neutral on any surface */
  object-fit: cover;
  flex-shrink: 0;
}
.avatar-badge--br { bottom: 0; right: 0;  transform: translate(10%, 10%); }
.avatar-badge--tr { top: 0;    right: 0;  transform: translate(10%, -10%); }
.avatar-badge--bl { bottom: 0; left: 0;   transform: translate(-10%, 10%); }
.avatar-badge--tl { top: 0;    left: 0;   transform: translate(-10%, -10%); }

/* True cutout for tandem badges — base avatar gets a circular hole
   punched in each corner that has a badge. Same principle as the
   stack cutout: mask-image makes the gap truly transparent so the
   surface behind shows through.

   Multi-badge support — a tandem can carry up to 4 corners (br/tr/bl/tl).
   Four mask layers compose with `mask-composite: intersect`, each layer
   either an actual cutout (when that corner's badge exists, set by the
   :has() rule) or a fallback opaque layer (no cutout). Intersection of
   opaque regions = avatar visible everywhere except the cutout corners.

   Geometry — badges nudge outward 10% of badge size (4% of parent)
   so they read as "clipping" the corner without major parent-clip
   risk. Math:
     badge size  = --avatar-size × 0.40  → radius 20% of parent
     base corner center without translate = (80%, 80%) for br
     translate(10%, 10%) on badge        = ±4% of parent
     final center for each corner:
       --br → ( 84%, 84% )
       --tr → ( 84%, 16% )
       --bl → ( 16%, 84% )
       --tl → ( 16%, 16% )
   Cutout radius = badge radius + 2px gap = (--avatar-size × 0.20) + 2px,
   rounded to integer pixels for crisp edges at every avatar size. */
.avatar-tandem > .avatar:first-child {
  --mask-br: linear-gradient(black, black);
  --mask-tr: linear-gradient(black, black);
  --mask-bl: linear-gradient(black, black);
  --mask-tl: linear-gradient(black, black);
  -webkit-mask-image: var(--mask-br), var(--mask-tr), var(--mask-bl), var(--mask-tl);
          mask-image: var(--mask-br), var(--mask-tr), var(--mask-bl), var(--mask-tl);
  -webkit-mask-composite: source-in;   /* webkit syntax for "intersect" */
          mask-composite: intersect;
}
.avatar-tandem:has(> .avatar-badge--br) > .avatar:first-child {
  --mask-br: radial-gradient(
    circle round(calc(var(--avatar-size, 40px) * 0.20 + 2px), 1px)
    at 84% 84%,
    transparent 99.5%, black 100%
  );
}
.avatar-tandem:has(> .avatar-badge--tr) > .avatar:first-child {
  --mask-tr: radial-gradient(
    circle round(calc(var(--avatar-size, 40px) * 0.20 + 2px), 1px)
    at 84% 16%,
    transparent 99.5%, black 100%
  );
}
.avatar-tandem:has(> .avatar-badge--bl) > .avatar:first-child {
  --mask-bl: radial-gradient(
    circle round(calc(var(--avatar-size, 40px) * 0.20 + 2px), 1px)
    at 16% 84%,
    transparent 99.5%, black 100%
  );
}
.avatar-tandem:has(> .avatar-badge--tl) > .avatar:first-child {
  --mask-tl: radial-gradient(
    circle round(calc(var(--avatar-size, 40px) * 0.20 + 2px), 1px)
    at 16% 16%,
    transparent 99.5%, black 100%
  );
}

/* ── .avatar-stack__more — "+N more" overflow tile ───────────────────
   Compose with .avatar + .avatar--circle to inherit size + shape +
   stack chrome (the page-coloured outline from .avatar-stack > .avatar).
   The tile carries a neutral fill + tabular numerals, sized to scale
   with the cluster's --avatar-size.

     <span class="avatar avatar-stack__more avatar--circle">+12</span>

   Use as the LAST child in an .avatar-stack to indicate the cluster
   is truncated (3 visible + "+12" = 15 total chains, etc.). */
.avatar-stack__more {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: color-mix(in oklch, var(--text-primary) 8%, transparent);
  color: var(--text-secondary);
  font-family: var(--font-sans);
  font-size: calc(var(--avatar-size, 40px) * 0.32);
  font-weight: var(--fw-semibold);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--tracking-firm);
  line-height: 1;
}

/* Label companion — avatar + name pair, e.g. in tables or chips. */
.avatar-label {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  line-height: var(--lh-sm);
  color: var(--text-primary);
}

/* Size modifiers — pair the label's gap + font-size with the .avatar
   size sitting inside it. Default (no modifier) keeps the historic
   8px / 14px values that pair with the default 40px avatar. */
.avatar-label--xs  { gap: var(--space-4);  font-size: var(--text-caption); }   /* with .avatar--xs (24) */
.avatar-label--sm  { gap: var(--space-8);  font-size: var(--text-sm); }        /* with .avatar--sm (32) */
.avatar-label--lg  { gap: var(--space-12); font-size: var(--text-body); }      /* with .avatar--lg (48) */
.avatar-label--xl  { gap: var(--space-16); font-size: var(--text-body-lg); }   /* with .avatar--xl (64) */
.avatar-label--2xl { gap: var(--space-20); font-size: var(--text-h3); }        /* with .avatar--2xl (96) */


/* ══════════════════════════════════════════════════════════════════════════════
   8d-2. PARTNER LOGOS — .ds-doc-partner-logo  (full brand wordmarks)
   ════════════════════════════════════════════════════════════════════════════
   Partners ship as FULL brand wordmarks — varied aspect ratio, brand
   typography baked in, supplied as a `<brand>-dark.svg` + `<brand>-light.svg`
   pair so the right variant shows on each theme. Partners are the only
   surface where we render external brands as themselves (vs. the plated
   .avatar tile used everywhere else).

   Asset library: `logos/partners/` only.
     logos/partners/<brand>-dark.svg
     logos/partners/<brand>-light.svg

   Markup (inline pair pattern — both files in source, CSS swaps on theme):
     <span class="ds-doc-partner-logo" role="img" aria-label="MetaMask">
       <img class="ds-doc-partner-logo__dark"  src="logos/partners/metamask-dark.svg"  alt="" loading="lazy">
       <img class="ds-doc-partner-logo__light" src="logos/partners/metamask-light.svg" alt="" loading="lazy">
     </span>

   Catalog grid wrapper:
     .ds-doc-partner-logo-grid    auto-fill, min cell 220px (so wordmarks breathe)
     .ds-doc-partner-logo-cell    figure with the .ds-doc-partner-logo + filename caption

   Sizes (height-constrained; width auto):
     .ds-doc-partner-logo--sm   24px  — table rows / chip insertions
     .ds-doc-partner-logo--md   32px  — default; carousel + cloud
     .ds-doc-partner-logo--lg   40px  — catalog preview cell

   The marketing partners-carousel `.partner-logo` class in `page.css` predates
   this primitive and remains an alias in that scope — both render the same
   pair-with-theme-swap pattern. The `.ds-` form is the catalog primitive.
   ════════════════════════════════════════════════════════════════════════════ */
.ds-doc-partner-logo {
  --partner-logo-h: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: var(--partner-logo-h);
  flex-shrink: 0;
}

.ds-doc-partner-logo--sm { --partner-logo-h: 24px; }
.ds-doc-partner-logo--md { --partner-logo-h: 32px; }  /* default */
.ds-doc-partner-logo--lg { --partner-logo-h: 40px; }

.ds-doc-partner-logo > img {
  height: 100%;
  width: auto;
  max-width: 100%;
  display: block;
  object-fit: contain;
}

/* Theme swap — show -dark on dark theme, -light on light. */
.ds-doc-partner-logo > .ds-doc-partner-logo__dark  { display: block; }
.ds-doc-partner-logo > .ds-doc-partner-logo__light { display: none; }
[data-theme="light"] .ds-doc-partner-logo > .ds-doc-partner-logo__dark  { display: none; }
[data-theme="light"] .ds-doc-partner-logo > .ds-doc-partner-logo__light { display: block; }

/* Catalog grid — auto-fill cells with breathing room for wordmarks. */
.ds-doc-partner-logo-grid {
  --card-gap: var(--gap-card-sm);
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: var(--card-gap);
}

.ds-doc-partner-logo-cell {
  position: relative;   /* anchors the per-asset download overlay (.ds-doc-cell-dl) */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-12);
  margin: 0;
  padding: var(--space-24) var(--space-16);
  min-height: 96px;
  border-radius: 8px;
  background: color-mix(in oklch, currentColor 3%, transparent);
  border: 1px solid color-mix(in oklch, currentColor 6%, transparent);
  transition: background-color 0.15s ease, border-color 0.15s ease;
}

.ds-doc-partner-logo-cell:hover {
  background: color-mix(in oklch, var(--accent-primary) 6%, transparent);
  border-color: color-mix(in oklch, var(--accent-primary) 22%, transparent);
}

.ds-doc-partner-logo-cell figcaption {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  line-height: 1.3;
  color: var(--text-muted);
  text-align: center;
  word-break: break-word;
  max-width: 100%;
}


/* ══════════════════════════════════════════════════════════════════════════════
   8e. BADGE — .badge  (tier-2 universal; added May 2026)
   ════════════════════════════════════════════════════════════════════════════
   Small floating indicator anchored to a corner of a host element —
   notification counts, "NEW" labels, unread dots. Inspired by MUI
   <Badge>: a saturated pill on the top-right of a button / icon /
   avatar that pulls the eye to something needing attention.

   Distinct from .chip — .chip labels a thing in flow, .badge attaches
   to a thing as an ornament. Solid fill + on-tone text are the
   visual signature; tinted backgrounds are reserved for .chip.

   Composition — wrap the host + badge in .badge-host so the host
   provides positioning context AND receives the cutout mask:

     <span class="badge-host">
       <button class="btn-with-icon">…</button>
       <span class="badge">4</span>
     </span>

   Defaults — brand accent fill (LI.FI blue, the dominant "unread"
   case), 20px pill, top-right anchor. Status tones (.badge-success /
   -warn / -danger / -info / -neutral) opt in by adding a single
   class.

   Sizes — three tiers cover every shipping case:
     .badge        20px tall, 12px text         (default)
     .badge--lg    24px tall, 14px text         (xl buttons, hero rows)
     .badge--dot   12×12 circle, no content     (presence/unread marker)

   Badges deliberately sit OFF the system 32/40/48/56 sizing ladder.
   They are corner-anchored ornaments, not type-aligned controls — the
   parallel decision lives in MUI, Apple HIG, and Material defaults
   (see CLAUDE.md → Sizing ladder note in design/components/imagery.md Badge entry).

   Anchor — .badge--top-right (default), --top-left, --bottom-right,
   --bottom-left. For circular hosts add .badge-host--circle to the
   wrapper; offsets shrink so the badge sits at the host's tangent
   instead of overhanging.

   Separation — TRUE CUTOUT via mask-image on the host (the first
   non-badge child of .badge-host). Two-layer architecture:

     1. CSS ellipse fallback (this block) — four radial-gradient mask
        layers composed with mask-composite: intersect, each defaulting
        to opaque (linear-gradient black, black). When a badge exists at
        a corner (detected by :has()), the corresponding layer becomes a
        radial-gradient ellipse with a transparent center. Sized from
        --badge-w / --badge-h (default to --badge-size). This renders
        approximately correctly before JS hydrates — covers .badge--dot
        and single-char content exactly, approximates pill content.

     2. SVG mask upgrade (shared.js → initBadgeHosts()) — measures each
        .badge offsetWidth / offsetHeight and writes an inline SVG
        data-URI mask-image directly to the host element. The SVG draws
        an outer rect (host bounds) with an inner pill-shaped hole via
        fill-rule:evenodd. Inline-style specificity overrides the CSS
        ellipse. ResizeObserver re-syncs on content changes; the cutout
        gives EXACTLY 2px gap around the pill — including the curved
        ends — for any content width.

   Why two layers: ellipse with semi-axes (W/2+2, H/2+2) doesn't fully
   contain a W×H pill's bounding-box corners — they sit at (±W/2, ±H/2),
   outside the ellipse curve. The CSS fallback's corner overhang is
   small (~0.7px on a 44×20 "NEW" pill) but visible at high zoom. The
   SVG mask is geometrically exact. Pure-CSS approaches can't express
   a rectangle in a single gradient, so the upgrade must run in JS.
   Same surface guarantee as the ellipse: whatever sits behind shows
   through (page, card, panel, accent halo). No per-context tuning.

   Text colour is COLOR-AWARE: --badge-fg derives from --badge-bg
   via OKLCH relative-color (same formula as --on-accent-primary at
   :root). Lightness flips dark↔light at L=0.62 so any consumer-set
   --badge-bg (built-in tone, partner color, custom hue) gets correct
   contrast automatically — no per-tone --badge-fg overrides.
   ════════════════════════════════════════════════════════════════════════════ */

/* Host wrapper — supplies positioning context AND cascades sizing
   variables to both the host child (which is masked) and the badge
   child (which sets its own dimensions).

   --badge-size  — height baseline (default / lg / dot).
   --badge-w     — actual rendered width of the badge (set by JS via
                   initBadgeHosts() in shared.js, falls back to
                   --badge-size for circles).
   --badge-h     — actual rendered height (also JS-set, falls back).

   The :has() rules detect badge size modifiers and update --badge-size
   on the wrapper so both children read the same value. --badge-w
   and --badge-h get overridden by initBadgeHosts() based on the
   badge's measured offsetWidth / offsetHeight, which is what makes
   the cutout track pill-shaped content correctly. */
.badge-host {
  --badge-size: var(--space-20);
  --badge-w: var(--badge-size);
  --badge-h: var(--badge-size);
  position: relative;
  display: inline-flex;
  vertical-align: middle;
}
.badge-host:has(> .badge--lg)  { --badge-size: var(--space-24); }
.badge-host:has(> .badge--dot) { --badge-size: var(--space-12); }

/* The badge itself.
   --badge-bg drives the tone (variant classes override).
   --badge-fg is COLOR-AWARE: derived from --badge-bg via OKLCH
     relative-color, same formula as --on-accent-primary at :root.
     Lightness flips light↔dark at L=0.62 so any consumer-supplied
     --badge-bg (built-in tone, partner color, accent halo) gets a
     correctly-contrasted text without per-tone --badge-fg overrides. */
.badge {
  --badge-bg: var(--accent-primary);
  --badge-fg: oklch(from var(--badge-bg) clamp(0.08, (0.62 - l) * 100, 0.96) 0 0);

  position: absolute;
  top: 0;
  right: 0;
  transform: translate(35%, -35%);

  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;

  min-width: var(--badge-size, var(--space-20));
  height: var(--badge-size, var(--space-20));
  padding: 0 var(--space-8);
  border-radius: 999px;            /* pill — collapses to circle when content is 1 char */

  background: var(--badge-bg);
  color: var(--badge-fg);
  font-family: var(--font-sans);
  font-size: var(--text-caption);
  font-weight: var(--fw-semibold);
  line-height: 1;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;

  pointer-events: none;            /* ornament, not interactive */
  user-select: none;
  z-index: 1;
}

/* Size modifiers — set their own dimensions (font, padding) on the
   .badge directly; --badge-size for the host's cutout is set on the
   wrapper via :has() above so the host can read it as a sibling. */
.badge--lg  { padding: 0 var(--space-12); font-size: var(--text-sm); }
.badge--dot {
  min-width: var(--space-12);
  height: var(--space-12);
  padding: 0;
  border-radius: 50%;
}

/* Anchor modifiers — translate the badge half-outward along each axis
   so it clips the host's corner. 35% translation puts the badge's
   center 15% inside the host's bounding box on each axis. */
.badge--top-right    { top: 0;    right: 0;  bottom: auto; left: auto; transform: translate( 35%, -35%); }
.badge--top-left     { top: 0;    left: 0;   bottom: auto; right: auto; transform: translate(-35%, -35%); }
.badge--bottom-right { bottom: 0; right: 0;  top: auto;    left: auto; transform: translate( 35%,  35%); }
.badge--bottom-left  { bottom: 0; left: 0;   top: auto;    right: auto; transform: translate(-35%,  35%); }

/* Circular-host offset — push the badge along the 45° diagonal so it
   sits at the circle's tangent instead of overhanging into space.
   Smaller translate (15%) puts the badge's center 35% inside the
   host's bounding box.

   First selector catches the DEFAULT badge (no anchor modifier, which
   is implicitly top-right) — without it, default badges in a circle
   host would fall through to the base `.badge` rule's translate(35%,
   -35%) and sit too far outside the host. The explicit anchor rules
   that follow have equal specificity but win via source order. */
.badge-host--circle .badge,
.badge-host--circle .badge--top-right    { transform: translate( 15%, -15%); }
.badge-host--circle .badge--top-left     { transform: translate(-15%, -15%); }
.badge-host--circle .badge--bottom-right { transform: translate( 15%,  15%); }
.badge-host--circle .badge--bottom-left  { transform: translate(-15%,  15%); }

/* Tones — solid fill. Only --badge-bg is set per tone; --badge-fg
   auto-derives via the OKLCH relative-color rule on .badge above.
   Status tones echo the .chip vocabulary (success / warn / danger /
   info / neutral) so a consumer reaches for the same word in both
   families. */
.badge-success { --badge-bg: var(--success); }
.badge-warn    { --badge-bg: var(--warn); }
.badge-danger  { --badge-bg: var(--danger); }
.badge-info    { --badge-bg: var(--info); }
.badge-neutral { --badge-bg: var(--text-primary); }

/* ── True cutout on the host — same pattern as .avatar-tandem ─────────
   Four mask layers compose with mask-composite: intersect; each
   defaults to fully opaque (linear-gradient black, black). When a
   badge exists at a corner (detected via :has() on the wrapper), the
   corresponding layer becomes a radial gradient with a transparent
   center — punching a real hole in the host so whatever surface
   sits behind shows through.

   The cutout is an ELLIPSE sized to the badge's actual bounding box
   plus a 2px gap on every side, NOT a fixed circle. This makes the
   gap track pill content correctly — a "NEW" badge gets a wider
   horizontal cutout than a "4" badge.

   Geometry — for a badge at top-right with `transform: translate(35%, -35%)`
   on a rectangular host:
     Badge center X = host_w - 0.15 × badge_w   (15% of badge_w INSIDE host)
     Badge center Y = 0.15 × badge_h
     Cutout ellipse semi-axes = (badge_w/2 + 2px, badge_h/2 + 2px)
     Cutout center = badge center (perfectly aligned)
   For circle host (.badge-host--circle, translate ±15%):
     Badge center X = host_w - 0.35 × badge_w
     Badge center Y = 0.35 × badge_h
     Same cutout sizing, shifted inward to sit at the host's tangent.

   --badge-w and --badge-h default to --badge-size on the wrapper
   (circle math, correct for .badge--dot and single-character content).
   For pill content they are set per-badge by initBadgeHosts() in
   shared.js, which measures each badge's offsetWidth/offsetHeight
   and assigns the value to its .badge-host. */
.badge-host > :not(.badge):first-child {
  --badge-cut-tr: linear-gradient(black, black);
  --badge-cut-tl: linear-gradient(black, black);
  --badge-cut-br: linear-gradient(black, black);
  --badge-cut-bl: linear-gradient(black, black);
  -webkit-mask-image: var(--badge-cut-tr), var(--badge-cut-tl),
                      var(--badge-cut-br), var(--badge-cut-bl);
          mask-image: var(--badge-cut-tr), var(--badge-cut-tl),
                      var(--badge-cut-br), var(--badge-cut-bl);
  -webkit-mask-composite: source-in;   /* webkit syntax for "intersect" */
          mask-composite: intersect;
}

/* Rectangular hosts — cutout sits 15% of badge_w / badge_h inside
   the corner. Default .badge (no anchor modifier) IS top-right, so
   it shares the same rule via the :not() chain. */
.badge-host:not(.badge-host--circle):has(> .badge:not(.badge--top-left):not(.badge--bottom-right):not(.badge--bottom-left)) > :not(.badge):first-child,
.badge-host:not(.badge-host--circle):has(> .badge--top-right) > :not(.badge):first-child {
  --badge-cut-tr: radial-gradient(
    ellipse calc(var(--badge-w) * 0.5 + 2px) calc(var(--badge-h) * 0.5 + 2px)
    at calc(100% - var(--badge-w) * 0.15) calc(var(--badge-h) * 0.15),
    transparent 99.5%, black 100%
  );
}
.badge-host:not(.badge-host--circle):has(> .badge--top-left) > :not(.badge):first-child {
  --badge-cut-tl: radial-gradient(
    ellipse calc(var(--badge-w) * 0.5 + 2px) calc(var(--badge-h) * 0.5 + 2px)
    at calc(var(--badge-w) * 0.15) calc(var(--badge-h) * 0.15),
    transparent 99.5%, black 100%
  );
}
.badge-host:not(.badge-host--circle):has(> .badge--bottom-right) > :not(.badge):first-child {
  --badge-cut-br: radial-gradient(
    ellipse calc(var(--badge-w) * 0.5 + 2px) calc(var(--badge-h) * 0.5 + 2px)
    at calc(100% - var(--badge-w) * 0.15) calc(100% - var(--badge-h) * 0.15),
    transparent 99.5%, black 100%
  );
}
.badge-host:not(.badge-host--circle):has(> .badge--bottom-left) > :not(.badge):first-child {
  --badge-cut-bl: radial-gradient(
    ellipse calc(var(--badge-w) * 0.5 + 2px) calc(var(--badge-h) * 0.5 + 2px)
    at calc(var(--badge-w) * 0.15) calc(100% - var(--badge-h) * 0.15),
    transparent 99.5%, black 100%
  );
}

/* Circular hosts — cutout sits 35% of badge_w / badge_h inside the
   corner (badge sits deeper inside the host's bounding box because
   the wrapper translate is shorter). */
.badge-host--circle:has(> .badge:not(.badge--top-left):not(.badge--bottom-right):not(.badge--bottom-left)) > :not(.badge):first-child,
.badge-host--circle:has(> .badge--top-right) > :not(.badge):first-child {
  --badge-cut-tr: radial-gradient(
    ellipse calc(var(--badge-w) * 0.5 + 2px) calc(var(--badge-h) * 0.5 + 2px)
    at calc(100% - var(--badge-w) * 0.35) calc(var(--badge-h) * 0.35),
    transparent 99.5%, black 100%
  );
}
.badge-host--circle:has(> .badge--top-left) > :not(.badge):first-child {
  --badge-cut-tl: radial-gradient(
    ellipse calc(var(--badge-w) * 0.5 + 2px) calc(var(--badge-h) * 0.5 + 2px)
    at calc(var(--badge-w) * 0.35) calc(var(--badge-h) * 0.35),
    transparent 99.5%, black 100%
  );
}
.badge-host--circle:has(> .badge--bottom-right) > :not(.badge):first-child {
  --badge-cut-br: radial-gradient(
    ellipse calc(var(--badge-w) * 0.5 + 2px) calc(var(--badge-h) * 0.5 + 2px)
    at calc(100% - var(--badge-w) * 0.35) calc(100% - var(--badge-h) * 0.35),
    transparent 99.5%, black 100%
  );
}
.badge-host--circle:has(> .badge--bottom-left) > :not(.badge):first-child {
  --badge-cut-bl: radial-gradient(
    ellipse calc(var(--badge-w) * 0.5 + 2px) calc(var(--badge-h) * 0.5 + 2px)
    at calc(var(--badge-w) * 0.35) calc(100% - var(--badge-h) * 0.35),
    transparent 99.5%, black 100%
  );
}


/* ══════════════════════════════════════════════════════════════════════════════
   8f. NUMERICS — .num-count  (tier-2 universal; added May 2026)
   ════════════════════════════════════════════════════════════════════════════
   Animated numeric counter. JS in numerics.js tweens between values; CSS
   owns the visual contract.

   Recipe (always-on):
     display: inline-block        — keeps the counter on its container's baseline
     font-variant-numeric: tabular-nums — prevents width jitter while digits tick

   Flash modes — opt-in via [data-flash]:
     none   (default)  — no decoration; reach for this when a sibling delta
                          chip already carries the up/down signal
     bright            — brief text-shadow halo + ink lift to --text-primary
                          on update; no hue shift. KPI hero surfaces.
     hue               — color flashes toward --success (up) / --danger (down)
                          on update; decays back over 0.6s. Use only where no
                          sibling delta chip exists.

   Animation shape — snap-on / decay-off:
     The base rule (without .is-flashing*) declares a long 0.6s transition.
     The .is-flashing* rules override transition-duration to 0.05s so the
     class addition snaps to the lit state. The JS removes the class ~50ms
     later; the base 0.6s transition cascades back to default ink.

   Reduced-motion: the @media block below suppresses every transition so
   the text just snaps to its new value. JS also suppresses the flash and
   the tween in this mode.

   Source of truth: design/components/numerics.md + #num-count in the catalog.
   ════════════════════════════════════════════════════════════════════════════ */

.num-count {
  display: inline-block;
  font-variant-numeric: tabular-nums;
}

/* Bright flash — soft brand-accent halo + subtle backdrop tint. Reads as
   "highlighted" / "just lit up" in BOTH themes. The accent hue is stable
   across light/dark (per the hue-parity rule); only luminance flips. No
   direction signal — the same hue regardless of the value going up or down.

   Why accent, not --text-primary (prior recipe, retired May 2026):
   text-primary inverts between themes (dark in light mode, light in dark
   mode). A halo built from text-primary darkened the area in light mode,
   the opposite of the affordance. Brand accent stays in the blue family
   across themes, so the halo + backdrop always lift the area toward an
   energetic/highlighted look.

   Glyph-traced glow recipe (v4 — no rectangle anywhere, ever):
     The flash is built purely from layered text-shadows. text-shadow paints
     from the GLYPH outlines (not the box edges), so the resulting cloud
     follows the shape of the digits and has no rectangular contour at all.
     No background-color, no border-radius, no box-shadow — anything box-
     bound would trace the rectangle, even at low opacity.

     Three layers compose the falloff:
       * tight (8 px blur, 70 % mix)  — concentrated trace on each digit
       * mid   (20 px blur, 45 % mix) — broader halo around the text
       * wide  (36 px blur, 25 % mix) — soft outer reach into the surface

     Why three layers, not one big one: a single 36 px blur at 25 % reads
     too diffuse — the glyphs don't feel "lit up", just surrounded by mist.
     Stacking a tight bright trace + a mid halo + a wide soft reach gives
     concentration on the digits AND soft outward fade. Three is the
     minimum to feel both "bright" and "subtle"; more layers don't help.

     v3 (May 2026) added a low-opacity box-shadow for extended reach but
     readers spotted the rectangle outline at the box edge (the shadow is
     brightest there). v4 drops it; text-shadow's wide layer covers the
     outer reach without ever touching a box edge. */
.num-count[data-flash="bright"] {
  transition: text-shadow 0.6s ease-out;
}
.num-count[data-flash="bright"].is-flashing {
  text-shadow:
    0 0 8px color-mix(in oklch, var(--accent-primary) 55%, transparent),
    0 0 20px color-mix(in oklch, var(--accent-primary) 35%, transparent),
    0 0 36px color-mix(in oklch, var(--accent-primary) 18%, transparent);
  transition-duration: 0.05s;
}

/* Hue flash — direction-toned glyph glow. Use only when no sibling delta
   chip exists (avoids double-signaling direction).

   Same recipe shape as bright (three glyph-traced text-shadow layers,
   no box-bound paint) so both flash modes feel like the same primitive
   with a different colour input. The colour token IS the direction
   signal: --success for up, --danger for down.

   Text colour itself does not change — the glow alone signals direction.
   Keeping the ink stable means the value stays in the cascaded text-primary
   colour even at the peak of the flash, so the digits never read as a
   "different state" while the glow tells the reader "this just moved up
   (or down)". Subtler and more consistent with bright than the prior
   color-snap recipe (May 2026 v1, retired). */
.num-count[data-flash="hue"] {
  transition: text-shadow 0.6s ease-out;
}
.num-count[data-flash="hue"].is-flashing-up {
  text-shadow:
    0 0 8px color-mix(in oklch, var(--success) 55%, transparent),
    0 0 20px color-mix(in oklch, var(--success) 35%, transparent),
    0 0 36px color-mix(in oklch, var(--success) 18%, transparent);
  transition-duration: 0.05s;
}
.num-count[data-flash="hue"].is-flashing-down {
  text-shadow:
    0 0 8px color-mix(in oklch, var(--danger) 55%, transparent),
    0 0 20px color-mix(in oklch, var(--danger) 35%, transparent),
    0 0 36px color-mix(in oklch, var(--danger) 18%, transparent);
  transition-duration: 0.05s;
}

@media (prefers-reduced-motion: reduce) {
  .num-count[data-flash] { transition: none; }
}


/* ══════════════════════════════════════════
   9. FORM CONTROLS — inputs, selects, textareas
   ══════════════════════════════════════════ */

/* ══════════════════════════════════════════════════════════════════════════════
   DS INPUT SYSTEM
   ════════════════════════════════════════════════════════════════════════════
   Size scale mirrors buttons and segmented controls. Inputs use neutral surface
   elevation (not accent tint) so they stay readable across every palette the
   Theme Composer produces. Accent is reserved for :focus. Add the optional
   `.input--tinted` / `.field-inline--tinted` modifier when brand affinity
   is intentional (hero forms, onboarding).

   Tokens (see `--field-*` in :root + [data-theme="light"]):
     --field-bg / --field-bg-hover / --field-bg-focus
     --field-border / --field-border-hover / --field-border-focus
     --field-ring · --field-placeholder

   Size matrix (radius · V-pad × H-pad · font / matches):
     sm    12 ·  9 × 14  · 0.82 rem  (btn-sm, seg-sm — ~36 px tall)
     md    14 · 12 × 16  · 0.9 rem   (btn-md, seg-md — ~43 px tall) [default]
     lg    18 · 16 × 20  · 1 rem     (btn-lg, seg-lg — ~54 px tall)
   ════════════════════════════════════════════════════════════════════════════ */

/* ── Form group / row layout ─────────────────────────────────────────────── */
.form-group { margin-bottom: var(--gap-form-field); }               /* 24 → 16 on phone */
.form-row   { display: grid; grid-template-columns: 1fr 1fr; gap: var(--gap-form-row); }
.form-group--sm { margin-bottom: var(--space-8); }
.form-group--lg { margin-bottom: var(--space-32); }

/* ── Label (stacked) ─────────────────────────────────────────────────────── */
.form-label {
  display: block;
  font-family: var(--font-sans);
  font-size: var(--text-sm); font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-normal);
  color: var(--text-secondary);
  margin-bottom: var(--space-8);
}
.form-label--sm {
  font-size: var(--text-caption); font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-loose); text-transform: uppercase;
  color: var(--text-muted); margin-bottom: 4px;
}
.form-label--lg {
  font-size: var(--text-sm); margin-bottom: var(--space-8);
}

/* ── Shared control base (input, select, textarea) ───────────────────────── */
.input,
.select,
.textarea {
  width: 100%;
  padding: var(--space-12) var(--space-16);          /* md — matches btn-md height */
  border-radius: var(--space-12);                    /* md — matches btn-md */
  background: var(--field-bg);
  border: 1px solid var(--field-border);
  color: var(--text-primary);
  font-family: var(--font-sans);
  font-size: var(--text-sm); font-weight: var(--fw-medium);
  line-height: var(--lh-sm);
  transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;
  outline: none; box-sizing: border-box;
}
.input::placeholder,
.textarea::placeholder { color: var(--field-placeholder); }

.input:hover:not(:focus),
.select:hover:not(:focus),
.textarea:hover:not(:focus) {
  background: var(--field-bg-hover);
  border-color: var(--field-border-hover);
}
.input:focus,
.select:focus,
.textarea:focus {
  background: var(--field-bg-focus);
  border-color: var(--field-border-focus);
  box-shadow: 0 0 0 3px var(--field-ring);
}
.input:disabled,
.select:disabled,
.textarea:disabled {
  opacity: 0.5; cursor: not-allowed;
}

/* ── Invalid state — bare-control + inline-wrapper shapes ─────────────────
   Drop-in modifier that paints a danger-colored border + focus ring. The
   `[aria-invalid="true"]` companion selector means server-side validation
   that flips the ARIA flag updates the visual without needing the class —
   pick whichever markup style fits the framework. Pairs with `.form-error`
   (sibling of `.muted--field`) for the message text below the field. */
.input--invalid,
.input[aria-invalid="true"],
.select--invalid,
.select[aria-invalid="true"],
.textarea--invalid,
.textarea[aria-invalid="true"] {
  border-color: var(--danger);
}
.input--invalid:focus,
.input[aria-invalid="true"]:focus,
.select--invalid:focus,
.select[aria-invalid="true"]:focus,
.textarea--invalid:focus,
.textarea[aria-invalid="true"]:focus {
  border-color: var(--danger);
  box-shadow: 0 0 0 3px color-mix(in oklch, var(--danger) 22%, transparent);
}

/* ── Select — custom chevron (positions match --sm / --lg paddings) ─────── */
.select {
  appearance: none; cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1.5L6 6.5L11 1.5' stroke='%23888' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 16px center;
  padding-right: 40px;
}

/* ── Textarea ────────────────────────────────────────────────────────────── */
.textarea { min-height: 120px; resize: vertical; line-height: var(--lh-body); }

/* ── Compact size (.--sm, matches btn-sm / seg-sm) ───────────────────────── */
.input--sm,
.select--sm,
.textarea--sm {
  padding: var(--space-8) var(--space-12);
  border-radius: var(--space-12);
  font-size: var(--text-sm);
}
.select--sm {
  padding-right: var(--space-32);
  background-position: right 12px center;
}
.textarea--sm { min-height: var(--space-64); }

/* ── Large size (.--lg, matches btn-lg / seg-lg) ─────────────────────────── */
.input--lg,
.select--lg,
.textarea--lg {
  padding: var(--space-16) var(--space-20);
  border-radius: var(--space-20);
  font-size: var(--text-body);
}
.select--lg {
  padding-right: 44px;
  background-position: right 20px center;
}
.textarea--lg { min-height: 160px; }

/* ── Brand-tinted modifier — opt-in accent affinity ──────────────────────── */
.input--tinted,
.select--tinted,
.textarea--tinted {
  background: color-mix(in oklch, var(--accent-primary) 5%, var(--field-bg));
  border-color: color-mix(in oklch, var(--accent-primary) 20%, var(--field-border));
}
.input--tinted:hover:not(:focus),
.select--tinted:hover:not(:focus),
.textarea--tinted:hover:not(:focus) {
  background: color-mix(in oklch, var(--accent-primary) 8%, var(--field-bg));
  border-color: color-mix(in oklch, var(--accent-primary) 30%, var(--field-border));
}

/* ── Inline-label field (label sits inside the chrome) ───────────────────── */
.field-inline {
  display: flex; flex-direction: column; width: 100%;
  padding: var(--space-8) var(--space-16) var(--space-12);       /* md */
  border-radius: var(--space-12);
  background: var(--field-bg);
  border: 1px solid var(--field-border);
  transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;
  cursor: text; box-sizing: border-box;
}
.field-inline:hover:not(:focus-within) {
  background: var(--field-bg-hover);
  border-color: var(--field-border-hover);
}
.field-inline:focus-within {
  background: var(--field-bg-focus);
  border-color: var(--field-border-focus);
  box-shadow: 0 0 0 3px var(--field-ring);
}
.field-label {
  font-family: var(--font-sans);
  font-size: var(--text-caption); font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-loose); text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: var(--space-4); pointer-events: none;
}
.field-input,
.field-select,
.field-textarea {
  font-family: var(--font-sans);
  font-size: var(--text-sm); font-weight: var(--fw-medium);
  color: var(--text-primary);
  background: transparent;
  border: 0; padding: 0; outline: none;
  width: 100%; box-sizing: border-box;
}
.field-input::placeholder,
.field-textarea::placeholder { color: var(--field-placeholder); }
.field-select {
  appearance: none; cursor: pointer;
  padding-right: 20px;
  background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1.5L6 6.5L11 1.5' stroke='%23888' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 0 center;
}
.field-textarea { min-height: var(--space-64); resize: vertical; }

/* Inline — sm */
.field-inline.field-inline--sm { padding: var(--space-4) var(--space-12) var(--space-8); border-radius: var(--space-12); }
.field-inline.field-inline--sm .field-label { font-size: var(--text-caption); margin-bottom: 0; }
.field-inline.field-inline--sm .field-input,
.field-inline.field-inline--sm .field-select,
.field-inline.field-inline--sm .field-textarea { font-size: var(--text-sm); }

/* Inline — lg */
.field-inline.field-inline--lg { padding: var(--space-12) var(--space-20) var(--space-12); border-radius: var(--space-20); }
.field-inline.field-inline--lg .field-label { font-size: var(--text-caption); margin-bottom: var(--space-4); }
.field-inline.field-inline--lg .field-input,
.field-inline.field-inline--lg .field-select,
.field-inline.field-inline--lg .field-textarea { font-size: var(--text-body); }

/* Inline — tinted modifier */
.field-inline--tinted {
  background: color-mix(in oklch, var(--accent-primary) 5%, var(--field-bg));
  border-color: color-mix(in oklch, var(--accent-primary) 20%, var(--field-border));
}
.field-inline--tinted:hover:not(:focus-within) {
  background: color-mix(in oklch, var(--accent-primary) 8%, var(--field-bg));
  border-color: color-mix(in oklch, var(--accent-primary) 30%, var(--field-border));
}

/* ── Inline field — trailing icon slot ──────────────────────────────────────
   A generic slot for a control glued to the right edge of the field chrome
   (password toggle, clear button, unit selector, etc.). The input text
   reserves right-padding matching the slot width so long values never run
   under the button. Opt-in via .field-inline--has-trail on the wrapper. */
.field-inline--has-trail { position: relative; }
.field-inline--has-trail .field-input,
.field-inline--has-trail .field-select,
.field-inline--has-trail .field-textarea { padding-right: 36px; }
.field-inline--has-trail.field-inline--sm .field-input,
.field-inline--has-trail.field-inline--sm .field-select,
.field-inline--has-trail.field-inline--sm .field-textarea { padding-right: var(--space-32); }
.field-inline--has-trail.field-inline--lg .field-input,
.field-inline--has-trail.field-inline--lg .field-select,
.field-inline--has-trail.field-inline--lg .field-textarea { padding-right: var(--space-40); }

.field-trail {
  position: absolute;
  top: 50%; right: 8px;
  transform: translateY(-50%);
  width: var(--space-32); height: var(--space-32);
  display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0;
  background: transparent; border: 0; padding: 0;
  color: var(--text-muted);
  cursor: pointer;
  font-family: var(--font-sans);                     /* defensive — <button> UA default is Arial; render brand Figtree if ever given a text label / ::after glyph */
  border-radius: var(--space-12);
  transition: color 0.18s ease, background 0.18s ease, box-shadow 0.18s ease;
}
.field-inline--sm .field-trail { right: 8px; width: var(--space-24); height: var(--space-24); border-radius: var(--space-8); }
.field-inline--lg .field-trail { right: 12px; width: 36px; height: 36px; border-radius: var(--space-12); }
.field-trail:hover {
  color: var(--text-primary);
  background: var(--overlay-hover);
}
.field-trail:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--field-ring);
  color: var(--text-primary);
}
.field-trail svg { width: var(--space-20); height: var(--space-20); stroke-width: 2; display: block; }
.field-inline--sm .field-trail svg { width: var(--space-12); height: var(--space-12); }
.field-inline--lg .field-trail svg { width: var(--space-20); height: var(--space-20); }

/* Password-toggle — eye/eye-off glyph swap driven by aria-pressed.
   Default (aria-pressed="false") shows the "eye" icon (password hidden);
   aria-pressed="true" shows "eye-off" (password revealed). Behavior is
   wired in shared.js via [data-password-toggle]. */
.field-trail .field-trail-on  { display: none; }
.field-trail[aria-pressed="true"] .field-trail-off { display: none; }
.field-trail[aria-pressed="true"] .field-trail-on  { display: block; }

/* ── Inline field — invalid state ────────────────────────────────────────
   Mirrors the bare-control invalid block above but binds to the wrapper's
   :focus-within ring so the label-inside-chrome layout stays coherent. */
.field-inline--invalid,
.field-inline[aria-invalid="true"] {
  border-color: var(--danger);
}
.field-inline--invalid:focus-within,
.field-inline[aria-invalid="true"]:focus-within {
  border-color: var(--danger);
  box-shadow: 0 0 0 3px color-mix(in oklch, var(--danger) 22%, transparent);
}

/* ── Inline field — leading icon slot ─────────────────────────────────────
   Symmetric mirror of `.field-inline--has-trail`. Reserves left padding
   on the input (and shifts the inline label right by the same amount so
   long labels don't collide with the icon). Canonical use: search-with-
   magnifier, currency icon. For TEXT addons (e.g., "$", "https://") use
   `.field-prefix` / `.field-suffix` — those flow inline with the input
   baseline rather than absolute-positioning over the chrome. */
.field-inline--has-lead { position: relative; }
.field-inline--has-lead .field-input,
.field-inline--has-lead .field-select,
.field-inline--has-lead .field-textarea { padding-left: 36px; }
.field-inline--has-lead .field-label { padding-left: 36px; }
.field-inline--has-lead.field-inline--sm .field-input,
.field-inline--has-lead.field-inline--sm .field-select,
.field-inline--has-lead.field-inline--sm .field-textarea { padding-left: var(--space-32); }
.field-inline--has-lead.field-inline--sm .field-label { padding-left: var(--space-32); }
.field-inline--has-lead.field-inline--lg .field-input,
.field-inline--has-lead.field-inline--lg .field-select,
.field-inline--has-lead.field-inline--lg .field-textarea { padding-left: var(--space-40); }
.field-inline--has-lead.field-inline--lg .field-label { padding-left: var(--space-40); }

.field-lead {
  position: absolute;
  top: 50%; left: 8px;
  transform: translateY(-50%);
  width: var(--space-32); height: var(--space-32);
  display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0;
  background: transparent; border: 0; padding: 0;
  color: var(--text-muted);
  border-radius: var(--space-12);
  pointer-events: none;
}
.field-inline--sm .field-lead { left: 8px; width: var(--space-24); height: var(--space-24); border-radius: var(--space-8); }
.field-inline--lg .field-lead { left: 12px; width: 36px; height: 36px; border-radius: var(--space-12); }
.field-lead svg { width: var(--space-20); height: var(--space-20); stroke-width: 2; display: block; }
.field-inline--sm .field-lead svg { width: var(--space-12); height: var(--space-12); }
.field-inline--lg .field-lead svg { width: var(--space-20); height: var(--space-20); }

/* ── Inline field — text prefix / suffix slots ────────────────────────────
   For purely textual addons that flow on the input's baseline ($ + amount,
   https://example.com, "kg", "%"). Different layout from the icon slots
   (.field-lead / .field-trail are absolute-positioned 32px buttons):
   prefix/suffix sit in a flex row, take their intrinsic text width, and
   the input stretches between them.

   Markup:
     <label class="field-inline">
       <span class="field-label">URL</span>
       <span class="field-control">
         <span class="field-prefix">https://</span>
         <input class="field-input" placeholder="example.com">
         <span class="field-suffix">.com</span>
       </span>
     </label>

   .field-control is opt-in — only present when prefix/suffix exists.
   Plain inline fields keep their flat structure. */
.field-control {
  display: flex; align-items: baseline; gap: 4px;
}
.field-prefix,
.field-suffix {
  font: inherit;
  color: var(--text-muted);
  white-space: nowrap;
  flex-shrink: 0;
  pointer-events: none;
  user-select: none;
}
.field-control .field-input,
.field-control .field-select,
.field-control .field-textarea { flex: 1 1 auto; min-width: 0; }

/* ── Accessibility utility — visually hidden, still in the AOM ────────────
   `.sr-only` keeps content invisible to sighted users but exposed to
   screen readers and the accessibility tree. Standard pattern: clip to
   1×1 px, no padding/border, off-flow. Use for:
     • <legend class="sr-only"> when surrounding chrome already names a
       group (the legend is still required for fieldset semantics);
     • text labels inside icon-only / swatch-only controls that need a
       reading name without visible text.
   Don't use for content the user might need to read — that's just
   hidden, not accessible. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ── Checkbox — checkbox ──────────────────────────────────────────────── */
/* Accessible pattern: native input kept in-flow for focus/aria, visually
   replaced with a token-driven box. Hover/focus states mirror .input so
   the control reads as part of the same family. */
.checkbox {
  display: inline-flex;
  align-items: center;
  gap: var(--cluster-sm);
  font-size: var(--text-sm);
  font-weight: var(--fw-medium);
  color: var(--text-secondary);
  cursor: pointer;
  user-select: none;
  line-height: var(--lh-sm);
}
.checkbox input[type="checkbox"] {
  appearance: none;
  -webkit-appearance: none;
  width: var(--space-20);
  height: var(--space-20);
  flex-shrink: 0;
  margin: 0;
  background: var(--field-bg);
  border: 1px solid var(--field-border);
  border-radius: var(--space-8);
  display: grid;
  place-content: center;
  cursor: pointer;
  transition: background 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;
}
.checkbox:hover input[type="checkbox"] {
  background: var(--field-bg-hover);
  border-color: var(--field-border-hover);
}
.checkbox input[type="checkbox"]:focus-visible {
  outline: none;
  border-color: var(--field-border-focus);
  box-shadow: 0 0 0 3px var(--field-ring);
}
.checkbox input[type="checkbox"]::before {
  content: "";
  width: var(--space-12);
  height: var(--space-12);
  transform: scale(0);
  transform-origin: center;
  transition: transform 0.12s ease-out;
  background-color: var(--accent-primary-mid);
  /* inline check glyph — keeps the primitive self-contained */
  clip-path: polygon(14% 44%, 0 60%, 40% 100%, 100% 20%, 86% 8%, 38% 70%);
}
.checkbox input[type="checkbox"]:checked {
  background: color-mix(in oklch, var(--accent-primary) 12%, var(--field-bg));
  border-color: var(--accent-primary-mid);
}
.checkbox input[type="checkbox"]:checked::before {
  transform: scale(1);
}
/* Indeterminate — the select-all / partial-selection state (set via the JS
   `input.indeterminate` property; there is no markup attribute). Same tinted
   box as :checked, the check glyph swapped for a horizontal dash (the
   Gmail / macOS convention). Added June 2026 with the Portal withdraw-fees
   select-all; additive — no prior consumer set the property. */
.checkbox input[type="checkbox"]:indeterminate {
  background: color-mix(in oklch, var(--accent-primary) 12%, var(--field-bg));
  border-color: var(--accent-primary-mid);
}
.checkbox input[type="checkbox"]:indeterminate::before {
  transform: scale(1);
  clip-path: inset(38% 8% 38% 8%);
}
.checkbox input[type="checkbox"]:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
.checkbox:has(input:disabled) {
  cursor: not-allowed;
  color: var(--text-faint);
}
/* Size variants — mirror .input scale */
.checkbox.checkbox--sm { font-size: var(--text-sm); gap: var(--space-8); }
.checkbox.checkbox--sm input[type="checkbox"] { width: var(--space-16); height: var(--space-16); border-radius: var(--space-4); }
.checkbox.checkbox--sm input[type="checkbox"]::before { width: var(--space-8); height: var(--space-8); }
.checkbox.checkbox--lg { font-size: var(--text-sm); gap: var(--cluster-md); }
.checkbox.checkbox--lg input[type="checkbox"] { width: var(--space-24); height: var(--space-24); border-radius: var(--space-8); }
.checkbox.checkbox--lg input[type="checkbox"]::before { width: var(--space-12); height: var(--space-12); }

/* ── Radio — radio (one-of-N selection) ──────────────────────────────────
   Sibling to .checkbox: native <input type="radio"> wrapped in <label>,
   visually replaced with a token-driven dot. Same --field-* tokens, same
   sizes, same accent tint via --accent-primary-mid — so a stack of
   inputs reads as one rhythm across check / radio / switch.

   Sizes mirror .checkbox 1:1:
     .radio--sm   16px outer / 8px dot
     .radio       20px outer / 12px dot   (default)
     .radio--lg   24px outer / 14px dot

   Modifiers:
     .radio--swatch  hides the dot, shows a colored swatch via --swatch-color;
                     used inside .radio-group--row for color-picker shapes.
     .radio--invalid pairs with aria-invalid="true" on the parent fieldset;
                     repaints border + ring with --field-border-invalid /
                     --field-ring-invalid (already defined for .input).

   Compose with .radio-group (a <fieldset> wrapper) for accessible group
   semantics + automatic gap. With-description shape uses the optional
   .radio-content child block — see below. */
.radio {
  display: inline-flex;
  align-items: center;
  gap: var(--cluster-sm);
  font-size: var(--text-sm);
  font-weight: var(--fw-medium);
  color: var(--text-secondary);
  cursor: pointer;
  user-select: none;
  line-height: var(--lh-sm);
}
.radio input[type="radio"] {
  appearance: none;
  -webkit-appearance: none;
  width: var(--space-20);
  height: var(--space-20);
  flex-shrink: 0;
  margin: 0;
  background: var(--field-bg);
  border: 1px solid var(--field-border);
  border-radius: 50%;
  display: grid;
  place-content: center;
  cursor: pointer;
  transition: background 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;
}
.radio:hover input[type="radio"] {
  background: var(--field-bg-hover);
  border-color: var(--field-border-hover);
}
.radio input[type="radio"]:focus-visible {
  outline: none;
  border-color: var(--field-border-focus);
  box-shadow: 0 0 0 3px var(--field-ring);
}
.radio input[type="radio"]::before {
  content: "";
  width: var(--space-12);
  height: var(--space-12);
  border-radius: 50%;
  transform: scale(0);
  transform-origin: center;
  transition: transform 0.12s ease-out;
  background-color: var(--accent-primary-mid);
}
.radio input[type="radio"]:checked {
  background: color-mix(in oklch, var(--accent-primary) 12%, var(--field-bg));
  border-color: var(--accent-primary-mid);
}
.radio input[type="radio"]:checked::before {
  transform: scale(1);
}
.radio input[type="radio"]:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
.radio:has(input:disabled) {
  cursor: not-allowed;
  color: var(--text-faint);
}

/* Size variants — mirror .checkbox scale */
.radio.radio--sm { font-size: var(--text-sm); gap: var(--space-8); }
.radio.radio--sm input[type="radio"] { width: var(--space-16); height: var(--space-16); }
.radio.radio--sm input[type="radio"]::before { width: var(--space-8); height: var(--space-8); }
.radio.radio--lg { font-size: var(--text-sm); gap: var(--cluster-md); }
.radio.radio--lg input[type="radio"] { width: var(--space-24); height: var(--space-24); }
.radio.radio--lg input[type="radio"]::before { width: var(--space-12); height: var(--space-12); }

/* Invalid state — pair with aria-invalid="true" on the fieldset for
   screen-reader semantics; .form-error slot below carries the message. */
.radio.radio--invalid input[type="radio"],
.radio input[type="radio"][aria-invalid="true"] {
  border-color: var(--field-border-invalid, var(--danger));
}
.radio.radio--invalid input[type="radio"]:focus-visible,
.radio input[type="radio"][aria-invalid="true"]:focus-visible {
  box-shadow: 0 0 0 3px var(--field-ring-invalid, color-mix(in oklch, var(--danger) 25%, transparent));
}

/* Swatch modifier — color-picker shape. The label COMPOSES with
   .swatch.swatch--sm.swatch--circle.swatch--interactive so the visual
   styling (32 px circle, --swatch-color background, hover scale, focus
   ring, --swatch-tone variant-aware state) all comes from the unified
   swatch primitive. This rule just handles the native-input integration:
   hide the actual <input type="radio"> (keep it clickable + focusable),
   and route the input's :checked / :focus-visible states up to the
   parent label via :has() so the swatch's is-active visual fires.

   Markup:
     <label class="radio radio--swatch swatch swatch--sm swatch--circle swatch--interactive"
            style="--swatch-color: var(--lifi-sapphire);">
       <input type="radio" name="theme" value="blue" checked>
       <span class="sr-only">Blue</span>
     </label>

   Why compose instead of fork — the visual (colored circle picker with
   hover scale + focus ring + active glow) is the SAME thing rendered by
   .swatch--interactive elsewhere (theme composer rows, preset buttons).
   Routing both through .swatch keeps a single source of truth; the radio
   wrapper adds form/a11y semantics on top. May 2026 v11 — replaces the
   prior self-contained .radio--swatch visual rules. */
.radio.radio--swatch {
  /* Neutralise the base .radio gap (label + input had a gap between them;
     here the input overlays the swatch). */
  gap: 0;
  /* Position context inherited from .swatch--interactive's position:
     relative — needed for the absolute-positioned <input> overlay. */
}
.radio.radio--swatch input[type="radio"] {
  /* The native input becomes a transparent overlay covering the swatch.
     Still focusable, still receives clicks, still participates in form
     submission + keyboard radio-group navigation — but invisible. */
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  margin: 0;
  padding: 0;
  border: none;
  background: transparent;
  cursor: pointer;
}
.radio.radio--swatch input[type="radio"]::before { display: none; }
/* Native :checked → the swatch's is-active visual.
   This mirrors the .swatch--interactive.is-active rule (border-color +
   two-layer halo using --swatch-tone, plus the checkmark glyph) but is
   triggered by native form state instead of a JS-toggled class. Same
   visual recipe, two trigger paths. */
.radio.radio--swatch:has(input:checked) {
  border-color: var(--swatch-tone);
  box-shadow:
    0 0 0 2px color-mix(in oklch, var(--swatch-tone) 48%, transparent),
    0 0 18px 4px color-mix(in oklch, var(--swatch-tone) 32%, transparent);
}
/* Checkmark glyph on :checked — same recipe as .swatch--interactive.is-active
   above. Auto-coloured via --swatch-on-color so it stays legible against the
   swatch's own hue. */
.radio.radio--swatch:has(input:checked)::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-color: var(--swatch-on-color, currentColor);
  -webkit-mask: var(--swatch-checkmark) center / 50% 50% no-repeat;
  mask: var(--swatch-checkmark) center / 50% 50% no-repeat;
}
/* Native :focus-visible → the swatch's focus ring. */
.radio.radio--swatch:has(input:focus-visible) {
  outline: none;
  box-shadow: 0 0 0 3px color-mix(in oklch, var(--swatch-tone) 35%, transparent);
}
/* If both checked AND focused, the focus ring stacks above the active halo. */
.radio.radio--swatch:has(input:checked:focus-visible) {
  box-shadow:
    0 0 0 3px color-mix(in oklch, var(--swatch-tone) 35%, transparent),
    0 0 0 2px color-mix(in oklch, var(--swatch-tone) 48%, transparent),
    0 0 18px 4px color-mix(in oklch, var(--swatch-tone) 32%, transparent);
}

/* With-description shape — wrap the secondary line in .radio-content so
   the radio dot stays anchored to the title baseline (not the midline of
   the wrapped block). */
.radio:has(.radio-content) {
  align-items: flex-start;
  gap: var(--space-12);
}
.radio:has(.radio-content) input[type="radio"] {
  margin-top: var(--space-4);
}
.radio-content {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  min-width: 0;
}
.radio-content__title {
  color: var(--text-primary);
  font-weight: var(--fw-medium);
  font-size: var(--text-sm);
  line-height: var(--lh-sm);
}
.radio-content__desc {
  color: var(--text-secondary);
  font-size: var(--text-caption);
  line-height: var(--lh-caption);
  font-weight: var(--fw-regular);
}

/* ── Radio group — fieldset wrapper for accessible groups ────────────────
   <fieldset class="radio-group"> + optional <legend>. Resets the native
   fieldset chrome (border, padding, margin) and lays out children in a
   column (default) or row.

     .radio-group           default — column, 12px gap
     .radio-group--sm       column, 8px gap (pair with .radio--sm)
     .radio-group--lg       column, 16px gap (pair with .radio--lg)
     .radio-group--row      inline, wraps. Suitable for 2–4 short options.

   The <legend> reads like a .form-label; the wrapper IS the group, the
   legend IS its label. Drop a .muted--field or .form-error sibling below
   the wrapper for helper text or validation messaging. */
.radio-group {
  border: 0;
  padding: 0;
  margin: 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-12);
}
.radio-group.radio-group--sm { gap: var(--space-8); }
.radio-group.radio-group--lg { gap: var(--space-16); }
.radio-group.radio-group--row {
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  column-gap: var(--space-24);
  row-gap: var(--space-12);
}
.radio-group > legend {
  font-size: var(--text-caption);
  font-weight: var(--fw-semibold);
  color: var(--text-primary);
  letter-spacing: var(--tracking-snug);
  margin-bottom: var(--space-8);
  padding: 0;
}

/* ── Radio card — selectable card (one-of-N from rich options) ───────────
   A card-shaped radio. Native <input type="radio"> hidden inside
   <label class="radio-card">; selection state via :has(input:checked).
   Surface uses --surface-card so it lifts above the container; selected
   state shifts the border to --accent-primary-mid, soft-tints the fill
   with the accent, and reveals a corner checkmark indicator (Carbon-style
   — visible at any density, survives light/dark).

   Variants:
     .radio-card               default — 16px padding, 12px radius
     .radio-card--sm           denser — 12px padding, 8px radius

   With description: nest .radio-card__title + .radio-card__desc inside
   the label. Compose horizontally with .radio-group--row, vertically
   with the default .radio-group (or any flex/grid container). */
.radio-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: var(--space-16);
  background: var(--surface-card);
  border: 1px solid var(--field-border);
  border-radius: var(--space-12);
  cursor: pointer;
  user-select: none;
  transition: border-color 0.18s ease, background 0.18s ease, box-shadow 0.18s ease;
  min-width: 0;
}
.radio-card:hover { border-color: var(--field-border-hover); }
.radio-card:has(input:focus-visible) {
  border-color: var(--field-border-focus);
  box-shadow: 0 0 0 3px var(--field-ring);
}

/* Native input — visually hidden, kept focusable + form-serializable */
.radio-card input[type="radio"] {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.radio-card:has(input:checked) {
  border-color: var(--accent-primary-mid);
  background: color-mix(in oklch, var(--accent-primary) 4%, var(--surface-card));
}
.radio-card:has(input:disabled) {
  opacity: 0.5;
  cursor: not-allowed;
}

.radio-card.radio-card--sm {
  padding: var(--space-12);
  border-radius: var(--space-8);
}

.radio-card__title {
  color: var(--text-primary);
  font-weight: var(--fw-semibold);
  font-size: var(--text-sm);
  line-height: var(--lh-sm);
}
.radio-card__desc {
  color: var(--text-secondary);
  font-size: var(--text-caption);
  line-height: var(--lh-caption);
  font-weight: var(--fw-regular);
}

/* Corner check indicator — accent-filled disc with inset check glyph.
   The SVG carries both the disc (<circle>) and the glyph (<path>); CSS
   paints them via fill / stroke to keep the markup minimal. Hidden by
   default; reveals when :has(input:checked). */
.radio-card__check {
  position: absolute;
  top: var(--space-12);
  right: var(--space-12);
  width: var(--space-20);
  height: var(--space-20);
  opacity: 0;
  transform: scale(0.6);
  transform-origin: center;
  transition: opacity 0.18s ease, transform 0.18s ease;
  pointer-events: none;
}
.radio-card.radio-card--sm .radio-card__check {
  top: var(--space-8);
  right: var(--space-8);
  width: var(--space-16);
  height: var(--space-16);
}
.radio-card__check circle { fill: var(--accent-primary-mid); }
.radio-card__check path {
  fill: none;
  stroke: var(--text-on-accent, oklch(99% 0.003 268));
  stroke-width: 3;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.radio-card:has(input:checked) .radio-card__check {
  opacity: 1;
  transform: scale(1);
}

/* ── .muted — universal muted helper / placeholder copy ───────────────────
   The single primitive for "muted, smaller, non-data informational line"
   across product, marketing, dashboards, and the catalog itself. Replaces
   the inline `style="color:var(--text-muted|faint)"` pattern that was
   accumulating drift (~144 occurrences across styles.css alone). Four
   contexts — pick the modifier that matches placement, never re-author
   the typography.

   | Modifier          | Context                                       | Margin / align          |
   |-------------------|-----------------------------------------------|--------------------------|
   | `.muted`          | base typography only (no spacing assumed)     | none                    |
   | `.muted--field`   | form-field helper, sits below input/select    | margin-top: --space-8   |
   | `.muted--slot`    | inside a component slot (card footer, etc.)   | none — slot owns spacing|
   | `.muted--block`   | standalone block under a heading / hero       | text-align: center      |

   Base typography (all variants):
     · font-size  → --text-caption (12px)
     · weight     → --fw-regular (400)
     · ink        → --text-faint (40% via --field-placeholder semantic alias)
     · line-height→ --lh-caption (~1.33)

   Link affordance is auto-applied (inline <a> gets accent-primary-mid
   underline). Don't add inline color/weight overrides on a .muted — if a
   context needs different chrome, use a modifier or add a new one.

   Migration history. v11 May 2026: `.form-hint` → `.hint` (a v6 promotion
   artifact that anchored the primitive to forms — dropped the prefix once
   the visual idiom proved universal). v20 May 2026: `.hint` → `.muted`
   (vocabulary alignment — the visual property is muted ink, and "hint"
   reads as a tone/intent that designers were applying inconsistently).
   Hard rename, no aliases. See `design/components/forms.md → Muted` and
   `#muted` in the catalog.
   ----------------------------------------------------------------------- */
.muted {
  font-size: var(--text-caption);
  font-weight: var(--fw-regular);
  color: var(--field-placeholder);
  line-height: var(--lh-caption);
}
.muted--field { margin-top: var(--space-8); }
.muted--slot  { /* slot host owns spacing — no margin */ }
.muted--block { text-align: center; }

/* Inline links inside muted copy — same recipe as .form-error and .ds-doc-link */
.muted a {
  color: var(--accent-primary-mid);
  text-decoration: underline;
  text-decoration-color: color-mix(in oklch, var(--accent-primary-mid) 40%, transparent);
  text-underline-offset: 2px; text-decoration-thickness: 1px;
  font-weight: var(--fw-medium); transition: color 0.2s, text-decoration-color 0.2s;
}
.muted a:hover { color: var(--accent-primary); text-decoration-color: var(--accent-primary); }

/* ── .link-arrow — small accent INLINE link ("Resume →", "Contact support →") ─
   Universal tier-2 INLINE / standalone accent link — inside prose, list rows,
   and meta notes (e.g. "4 remaining on your plan. Contact support →"; a
   checklist "Resume →"). Quiet by default (no underline), underlines on hover.
   The arrow glyph is authored in the markup — the class owns only the type +
   colour + hover. Absorbed the Portal's retired .portal-panel__link.

   NOT the panel-HEADER trailing action. The panel-header action ("View all",
   "Open explorer") is a neutral button — .btn-neutral.btn-sm in the
   .screen-header__trailing slot — so it reads as a defined, tappable control
   rather than a link (DS standard, June 2026; see CLAUDE.md → "Panel
   primitive"). Don't reach for .link-arrow in a panel header. */
.link-arrow {
  font-size: var(--text-sm);
  font-weight: var(--fw-medium);
  color: var(--accent-primary-mid);
  text-decoration: none;
  white-space: nowrap;
  transition: color 0.2s, text-decoration-color 0.2s;
}
.link-arrow:hover { color: var(--accent-primary); text-decoration: underline; }

/* ── Error message — sibling of .muted--field ────────────────────────────
   Use .form-error for validation messaging; .muted stays for purely
   informational helper text. Both slot below the field — only one shows
   per row. Wire via aria-describedby on the input so screen readers pick
   up the message; pair with `.input--invalid` (or `aria-invalid="true"`)
   for the visual border state. */
.form-error {
  font-size: var(--text-caption);
  color: var(--danger);
  margin-top: var(--space-8);
  line-height: var(--lh-caption);
  font-weight: var(--fw-medium);
}

/* ── .field-valid — success counterpart of .form-error ──────────────────
   Green inline validation line ("Valid Solana address") with a leading
   check glyph. Slots wherever .form-error / .muted--field do — below a
   field, or inline in a .field-label-row right slot. Only one validation
   line shows per row. */
.field-valid {
  display: inline-flex;
  align-items: center;
  gap: var(--space-4);
  font-size: var(--text-caption);
  line-height: var(--lh-caption);
  font-weight: var(--fw-medium);
  color: var(--success);
}
.field-valid svg { width: var(--space-12); height: var(--space-12); flex: 0 0 auto; }
/* [hidden] re-assert — the self-set display:inline-flex beats the UA
   `[hidden]{display:none}` (CLAUDE.md → `[hidden]` loses to a self-set display). */
.field-valid[hidden] { display: none; }

/* ── .field-label-row + .field-counter — label row with a char counter ───
   Header row for a stacked field: label left, a live character counter
   ("32 / 64") right (the right slot also hosts an "Optional" note or a
   .field-valid success line). The counter is a quiet mono caption; add
   .field-counter--over (danger-toned) when the max is hit. */
.field-label-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-12);
  margin-bottom: var(--space-8);   /* matches the .form-label gap */
}
/* The row owns the label→input gap; neutralize the inner label's own margin
   so it doesn't add a second gap inside the flex row. */
.field-label-row .form-label { margin-bottom: 0; }
/* The counter — quiet caption on the unified value-readout recipe
   (sans caption / medium / faint / tabular-nums — same tier as
   .slider-value at the --compact size; sans since the June 2026
   no-monotype-in-controls direction). Implemented June 2026
   (the comment above had promised it since the label-row landed, but
   the rule was never written). No live consumer yet — the Theme
   Composer's meta fields wore it briefly, then moved to an at-cap
   .form-error per design review (always-on counters read as noise). */
.field-counter {
  flex-shrink: 0;
  font-family: var(--font-sans);
  font-size: var(--text-caption);
  font-weight: var(--fw-medium);
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}
.field-counter--over {
  color: var(--danger);
  font-weight: var(--fw-semibold);
}

/* ── .radio-dot — visual radio indicator (no input) ─────────────────────
   A radio "appearance" for custom card-select rows where the WHOLE row is
   the click target (role="radio" / "button") and a real <input> would be
   redundant. Filled state via [data-checked="true"] or .is-checked. Pairs
   with .list-item / .option-card select rows (e.g. the integration wizard's
   string-choice rows). */
.radio-dot {
  flex: 0 0 auto;
  width: var(--space-20); height: var(--space-20);
  border-radius: var(--r-full);
  border: 2px solid var(--field-border);
  background: var(--field-bg);
  position: relative;
  transition: border-color 0.18s ease, background 0.18s ease;
}
.radio-dot[data-checked="true"], .radio-dot.is-checked { border-color: var(--accent-primary); }
.radio-dot[data-checked="true"]::after, .radio-dot.is-checked::after {
  content: ""; position: absolute; inset: 0; margin: auto;
  width: var(--space-8); height: var(--space-8);
  border-radius: var(--r-full); background: var(--accent-primary);
}

/* ── Form card — container panel
   Slightly accent-tinted (light dose) to signal "fill this in" as a
   form context. Layered on the canonical card tier (--surface-card)
   per the May 2026 ui-card unification; previously sat on
   --surface-raised. Border drops to the system 4 % hairline. */
.form-card {
  background: color-mix(in oklch, var(--accent-primary) 3%, var(--surface-card));
  border: 1px solid color-mix(in oklch, currentColor 4%, transparent);
  border-radius: 20px;
  padding: var(--pad-chrome-xl);  /* 48 desktop → 40 tablet → 32 phone */
  box-shadow: var(--elev-1);
}
.form-card h3 {
  font-size: var(--text-body); font-weight: var(--fw-bold); color: var(--text-primary);
  margin-bottom: var(--space-32); letter-spacing: var(--tracking-snug);
}

/* ── Light theme ─────────────────────────────────────────────────────────── */
/* .input / .select / .textarea / .field-inline inherit --field-*
   tokens which flip automatically via [data-theme="light"] — no overrides
   needed here. Only .form-card needs a light-mode tweak for its
   stronger surface treatment. */
[data-theme="light"] .form-card {
  background: rgba(255,255,255,0.85);
  border-color: color-mix(in oklch, var(--accent-primary) 12%, transparent);
  /* shadow handled by var(--elev-1) on the base rule — token mode-flips */
}

/* ── .setting-row — universal "label + value/control" card row ────────────
   Each setting is its OWN card mirroring .ui-amount-card's surface
   recipe: --surface-card fill + 16 px radius + --elev-1 shadow +
   transparent 1 px border (reserves the slot for a future
   active-state ring without layout shift, per the canonical UI-card
   recipe in CLAUDE.md). No chevron on drill rows — they navigate via
   the full-row hover lift, not a directional glyph. (The --expand
   variant below DOES carry a rotating chevron: there it's the
   open/closed state indicator, a different contract.)
   Three row contracts share this chrome: SETTINGS (host a control),
   DRILL (--drill, navigate to a sub-screen), EXPAND (--expand,
   disclose controls in place). Stack siblings with .setting-stack.

   Anatomy:

     [24 px icon LEFT]  [label / sub]  [value-or-control RIGHT]

   The middle slot accepts either a bare .setting-row__label OR a
   .setting-row__main wrapper containing .setting-row__label +
   .setting-row__sub. Both shapes are first-class citizens — pick by
   row contract, not by recency:

     • Bare __label  → terse rows, navigation patterns, dense
                       developer panels, list-of-categories surfaces
                       where the label IS the affordance
     • __main shape  → descriptive settings rows where the secondary
                       line genuinely clarifies what the toggle does
                       (the canonical settings-page pattern)

   The right-side slot accepts ANY control: .switch, .seg, .input,
   .select, .btn, button group, plain text value, or a count display.
   The grid stays the same; only the slot's child changes.

   Heights — locked to the system ladder (sm 40 / default 48 / lg 56)
   via min-height. Single-line rows pin exactly to the tier; two-line
   __main rows grow past min-height (the __main wrapper is an
   explicit two-line slot, not content overflow). Each tier pairs 1:1
   with same-tier controls:

     .setting-row--sm  ↔ pairs with .switch-sm / .input--sm / .btn-sm
     (default)         ↔ pairs with .switch    / .input     / .btn
     .setting-row--lg  ↔ pairs with .switch-lg / .input--lg / .btn-lg

   Hover — canonical UI-card recipe: 3 % --accent-primary mixed into
   --surface-card + --elev-2 lift. Active deepens to 5 %. Applied to
   .setting-row--drill (renders as <button>) and any row carrying
   [data-tap="true"]. Bare settings rows that host a control don't
   hover — the control inside is the affordance. This is the SAME
   recipe used by every card in the system (see CLAUDE.md "Canonical
   UI card recipe"); dense row lists are explicitly in-scope.

   Origin: derived Apr 2026 from the v2 LI.FI widget Settings screen
   (Figma file rGJrqtE7660rPUTrtS3yqR). Re-used on:
     - playground.html widget Settings screen
     - Wallet selector modal rows
     - (planned) dashboard preferences
   May 2026: surface promoted to canonical UI-card recipe (was
   --surface-raised + visible hairline); chevron retired (drill rows
   navigate via full-row hover lift); heights locked to the system
   ladder; --compact retired (use --sm for tighter list contexts).
   Same pass also went neutral ink-tinted on the hover layer, then
   immediately reverted — dense rows compose with the canonical
   accent-tinted recipe like every other card. No divergence.
*/
.setting-row {
  display: grid;
  grid-template-columns: var(--space-24) 1fr auto;
  align-items: center;
  font-family: var(--font-sans);                     /* defensive — <button> UA default is Arial; render brand Figtree if ever given a text label / ::after glyph */
  /* Icon-row gap tightened one tier from the padding-x default — pulls
     the label closer to the leading icon. The label→value gap is
     visually masked by the 1fr label column's flex slack, so the same
     gap value reads as "icon ↔ label only" in practice. */
  gap: var(--space-8);
  /* Heights on the canonical Sizing-Ladders rung: sm 40 / default 48 /
     lg 56. Pairs 1:1 with the matching .btn / .input / .switch tier
     hosted inside the row. */
  min-height: var(--space-48);
  /* Symmetric side padding across every tier so a row's content aligns
     with the sibling card family (.ui-amount-card et al, all 16 px). The
     vertical/horizontal split tracks the height ladder; left == right.
     Per-tier: sm 8 16, default 12 16, lg 12 20. (June 2026 — left bumped
     to match right; the earlier icon-row left-tightening read 4 px off
     from neighbouring cards on no-icon / drill rows.) */
  padding: var(--space-12) var(--space-16) var(--space-12) var(--space-16);
  background: var(--surface-card);
  /* border: 0 (NOT omitted — `<button>` carries a 2 px outset default).
     The canonical UI-card recipe reserves a 1 px transparent border
     slot for the active-state accent ring, but setting-row is a
     navigation primitive — drill rows navigate, they don't select.
     With no .is-active state, the slot has no consumer; reserving it
     painted a perimeter line on hover against near-white panel
     surfaces, and omitting the declaration entirely re-exposed the
     `<button>` user-agent border. Explicit zero is the right answer. */
  border: 0;
  border-radius: var(--card-radius);
  box-shadow: var(--elev-1);
  transition: box-shadow .18s ease, background-color .12s ease;
}
.setting-row__icon {
  width: var(--space-24); height: var(--space-24);
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--text-primary);
}
.setting-row__icon svg { width: var(--space-20); height: var(--space-20); }

/* Optional middle-column wrapper — use when the row needs a secondary
   description line below the label. Without this wrapper, a bare
   .setting-row__label still works (legacy single-line use). */
.setting-row__main {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  min-width: 0;
}

.setting-row__label {
  font-size: var(--text-body);
  font-weight: var(--fw-semibold);
  color: var(--text-primary);
  letter-spacing: var(--tracking-snug);
}
.setting-row__sub {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  letter-spacing: var(--tracking-snug);
  line-height: var(--lh-sm);
}
.setting-row__value {
  /* inline-flex so the slot holds a row of .swatch--xs dots (the Theme
     Composer's live section summaries) as readily as plain text. */
  display: inline-flex;
  align-items: center;
  gap: var(--space-4);
  font-size: var(--text-body);
  font-weight: var(--fw-medium);
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.setting-row__value--muted { color: var(--text-secondary); }

/* .is-expanded + .setting-row__body retired June 2026 (zero live
   consumers) → the .setting-row--expand variant below: the same
   "sub-controls beneath the row" role, animated via the universal
   .reveal drawer instead of a display:none snap. */

/* ── Size modifiers — pair with the form-control family.
   Each tier locks min-height to a ladder step (sm 40 / default 48 /
   lg 56) and scales padding, icon, and label/sub/value typography
   together so the row reads as a coherent unit regardless of which
   controls it hosts. */
.setting-row--sm {
  min-height: var(--space-40);
  /* Symmetric 16 horizontal — left == right, matching the base rule's
     per-tier symmetric padding (see .setting-row). */
  padding: var(--space-8) var(--space-16) var(--space-8) var(--space-16);
  border-radius: var(--card-radius);
  gap: var(--space-4);
  grid-template-columns: var(--space-20) 1fr auto;
}
.setting-row--sm .setting-row__icon { width: var(--space-20); height: var(--space-20); }
.setting-row--sm .setting-row__icon svg { width: var(--space-16); height: var(--space-16); }
.setting-row--sm .setting-row__label { font-size: var(--text-sm); }
.setting-row--sm .setting-row__sub   { font-size: var(--text-caption); }
.setting-row--sm .setting-row__value { font-size: var(--text-sm); }

.setting-row--lg {
  min-height: var(--space-56);
  /* Symmetric 20 horizontal — left bumped 16 → 20 to match the right edge. */
  padding: var(--space-12) var(--space-20) var(--space-12) var(--space-20);
  border-radius: var(--card-radius);
  gap: var(--space-12);
  grid-template-columns: var(--space-32) 1fr auto;
}
.setting-row--lg .setting-row__icon { width: var(--space-32); height: var(--space-32); }
.setting-row--lg .setting-row__icon svg { width: var(--space-24); height: var(--space-24); }
.setting-row--lg .setting-row__label { font-size: var(--text-body); }
.setting-row--lg .setting-row__sub   { font-size: var(--text-sm); }
.setting-row--lg .setting-row__value { font-size: var(--text-body); }

/* ── Icon-less variant — drop the leading 24px column.
   Settings pages frequently mix iconned + iconless rows; use this
   modifier for rows where the label alone communicates the setting
   (no glyph needed). The label slot becomes the first column. */
.setting-row--no-icon {
  grid-template-columns: 1fr auto;
}
.setting-row--no-icon.setting-row--sm,
.setting-row--no-icon.setting-row--lg {
  grid-template-columns: 1fr auto;
}

/* ── Leading avatar — a settings row can carry an .avatar (provider /
   token / chain logo) as its leading element, occupying the same column
   the icon would. Auto-detected via :has() so no modifier is needed (the
   avatar's presence IS the trigger), and it overrides --no-icon's 2-col
   grid back to 3-col with a leading slot sized to the tier's icon column.
   Pair the avatar tier to the row tier so it fills the column: default →
   .avatar--xs (24), lg → .avatar--sm (32). Source order beats --no-icon. */
.setting-row:has(> .avatar)        { grid-template-columns: var(--space-24) 1fr auto; }
.setting-row--sm:has(> .avatar)    { grid-template-columns: var(--space-20) 1fr auto; }
.setting-row--lg:has(> .avatar)    { grid-template-columns: var(--space-32) 1fr auto; }
.setting-row > .avatar { grid-column: 1; justify-self: center; }

/* ── .setting-stack — vertical layout wrapper for grouped rows.
   Pairs naturally with .setting-row to build a settings panel.
   Three rhythms — tight (8px) for dense panels, default (12px) for
   most surfaces, loose (16px) for hero settings. The wrapper owns
   only spacing; each row keeps its own card chrome.

   Publishes a local --card-gap (per CLAUDE.md → "Card gap ladder")
   defaulting to --gap-card-md (12 px). Modifiers re-route the slot
   to --gap-card-sm (8 px) or --gap-card-lg (16 px). Per-instance
   overrides can set --card-gap inline without touching this rule. */
.setting-stack {
  --card-gap: var(--gap-card-md);
  display: flex;
  flex-direction: column;
  gap: var(--card-gap);
}
.setting-stack--tight { --card-gap: var(--gap-card-sm); }
.setting-stack--loose { --card-gap: var(--gap-card-lg); }

/* Optional group title + description above a stack — mirrors the
   row's own label / sub typography one tier larger so the hierarchy
   reads correctly at a glance. */
.setting-stack__title {
  font-size: var(--text-h5);
  font-weight: var(--fw-bold);
  color: var(--text-primary);
  letter-spacing: var(--tracking-tight);
  margin: 0 0 var(--space-4);
}
.setting-stack__desc {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  margin: 0 0 var(--space-12);
  line-height: var(--lh-sm);
  max-width: 56ch;
}

/* ── Drill variant — navigation row pattern.
   Anatomy: [icon] [label] [optional value]. The whole row is the
   affordance — hover lifts the surface (neutral 3 % ink tint +
   --elev-2). Renders as a <button> so keyboard + screen reader treat
   the row as what it is; this rule resets the button default chrome
   so the primitive's surface is the only chrome on screen.

   Without a __value child the row collapses naturally — the auto
   column zero-widths and the label fills the rest. No separate
   modifier needed for the value-less shape.

   Live consumers — playground.html rail (Layout / Theme /
   Developer panels), wallet selector chain-list rows, planned
   dashboard preferences drill-down list. */
.setting-row--drill {
  /* <button> reset — keep the .setting-row chrome as the only chrome */
  font: inherit;
  text-align: left;
  cursor: pointer;
  width: 100%;
}

/* Shared hover — both drill rows (always-tappable) and non-button
   rows opted-in via [data-tap="true"]. Canonical UI-card recipe:
   3 % --accent-primary into --surface-card + --elev-2 lift; active
   deepens to 5 %. Same hover/active math as .ui-amount-card and
   every other card in the system. */
.setting-row[data-tap="true"] { cursor: pointer; }
.setting-row--drill:hover,
.setting-row[data-tap="true"]:hover {
  background: color-mix(in oklch, var(--accent-primary) 3%, var(--surface-card));
  box-shadow: var(--elev-2);
}
.setting-row--drill:active,
.setting-row[data-tap="true"]:active {
  background: color-mix(in oklch, var(--accent-primary) 5%, var(--surface-card));
  box-shadow: var(--elev-2);
}
.setting-row--drill:focus-visible,
.setting-row[data-tap="true"]:focus-visible {
  outline: 2px solid color-mix(in oklch, var(--accent-primary) 50%, transparent);
  outline-offset: 2px;
}

/* ── Expand variant — collapsible-section row pattern.
   The THIRD row contract beside settings (host a control) and drill
   (navigate to a sub-screen): the row is a section header whose card
   opens IN PLACE — a .setting-row__head button (label + live value
   summary + chevron) over the universal .reveal drawer, all inside one
   card. Absorbed the retired .accordion primitive (June 2026 — the
   Theme Composer's section stack is the canonical consumer). The
   summary slot is the load-bearing idea: a CLOSED expand row still
   reads its section's current state in __value (a preset name, gap
   values, a row of .swatch--xs dots), so a stack of closed rows
   doubles as a scannable overview.

   Markup:
     <div class="setting-row setting-row--sm setting-row--expand [is-open]">
       <button type="button" class="setting-row__head" aria-expanded="false">
         <span class="setting-row__label">Corners</span>
         <span class="setting-row__value setting-row__value--muted">Standard</span>
         <svg class="setting-row__chevron">…</svg>   <!-- icons/chevron-down.svg -->
       </button>
       <div class="reveal"><div class="reveal__inner">
         <div class="setting-row__drawer">…controls…</div>
       </div></div>
     </div>

   Open/close is wired centrally by shared.js → initSettingRowExpand()
   (document-level delegation on .setting-row__head) — no per-mount JS.
   The chevron does NOT break the drill rule's no-chevron contract: an
   expand row discloses in place, and the rotating glyph is the
   open/closed STATE indicator, not a navigation affordance. Drill rows
   stay chevron-free. */
.setting-row--expand {
  /* Root flips from grid row to a flex column hosting head + drawer;
     the card chrome (surface / radius / elev-1) stays on the root so an
     open section reads as ONE card containing its controls. */
  display: flex;
  flex-direction: column;
  align-items: stretch;
  padding: 0;                 /* head + drawer own the insets */
  /* gap: 0 — the tier gaps (8 / 4 / 12) are the grid row's icon↔label
     gap; on this flex column they'd pad above the CLOSED zero-height
     drawer and push the row off its ladder rung (40 → 44). Head
     padding + drawer padding own the vertical rhythm instead. */
  gap: 0;
}
.setting-row__head {
  /* Dual-host reset — chrome stripped explicitly (CLAUDE.md → Card root
     element: a <button> leaks UA chrome if merely omitted). */
  appearance: none;
  -webkit-appearance: none;
  border: 0;
  background: transparent;
  font-family: var(--font-sans);
  width: 100%;
  display: grid;
  grid-template-columns: 1fr auto auto;   /* label · value · chevron */
  align-items: center;
  gap: var(--space-8);
  min-height: var(--space-48);
  padding: var(--space-12) var(--space-16);
  border-radius: var(--card-radius);      /* hugs the card corner for focus-visible */
  cursor: pointer;
  text-align: left;
}
.setting-row--sm .setting-row__head { min-height: var(--space-40); padding: var(--space-8) var(--space-16); }
.setting-row--lg .setting-row__head { min-height: var(--space-56); padding: var(--space-12) var(--space-20); }
/* Hover / active tint the WHOLE card via the canonical setting-row
   recipe, but only while the cursor is on the head — hovering controls
   inside the open drawer must not re-tint the section. */
.setting-row--expand:has(> .setting-row__head:hover) {
  background: color-mix(in oklch, var(--accent-primary) 3%, var(--surface-card));
  box-shadow: var(--elev-2);
}
.setting-row--expand:has(> .setting-row__head:active) {
  background: color-mix(in oklch, var(--accent-primary) 5%, var(--surface-card));
}
.setting-row__head:focus-visible {
  outline: 2px solid color-mix(in oklch, var(--accent-primary) 50%, transparent);
  outline-offset: -2px;
}
.setting-row__chevron {
  width: var(--space-16);
  height: var(--space-16);
  color: var(--text-muted);
  rotate: 0deg;
  transition: rotate 0.15s ease;
}
.setting-row--expand.is-open .setting-row__chevron { rotate: 180deg; }
/* Drawer = the universal .reveal primitive; the variant adds only its
   ancestor-state opener. The padded body lives one level inside the
   bare collapser (vertical padding on .reveal__inner would floor the
   0fr collapse — the grid-rows accordion trap). Child shadows (option
   cards, colour fields) stay inside the drawer's padding gutter since
   the collapser keeps overflow: hidden permanently. */
.setting-row--expand.is-open > .reveal { grid-template-rows: 1fr; }
.setting-row__drawer {
  display: flex;
  flex-direction: column;
  gap: var(--space-12);
  padding: var(--space-4) var(--space-16) var(--space-16);
}
.setting-row--lg .setting-row__drawer { padding: var(--space-4) var(--space-20) var(--space-16); }


/* ── .split-pane — wide-layout sibling-panel pattern ──────────────────────
   Auxiliary card that slides in beside a primary container (e.g., the
   network filter that appears next to the token list, the route detail
   panel that appears next to the form). In wide layout this is a literal
   sibling — the parent flex/grid puts the two cards side by side. In
   compact layout (out of scope for now) this collapses to a stacked card
   or a drawer; that mapping is deferred until phase 8+.

   May 2026 — composes the universal `.panel` primitive (search PANEL —
   .panel above). Surface chrome (background, shadow, radius, padding,
   internal stack + gap) flows from `.panel`, so the chain-filter sibling
   reads as the same family as the widget shell (.ui-card) and the
   playground rail (.playground-rail). The primitive adds only what's
   split-pane-specific: hidden-by-default visibility, the fixed 320 px
   width, the slide-in animation.
   Markup: <aside class="panel split-pane split-pane--chain-filter" …>

   Visibility: hidden by default. The host page (or a parent state class)
   reveals it when the right context fires. Reveal mechanism is out of
   the primitive's hands — the primitive owns chrome only.
*/
.split-pane {
  display: none;
  flex: 0 0 auto;
  width: 320px;
}
.split-pane.is-open {
  display: flex;
  animation: split-pane-slide-in .22s ease-out;
}
@keyframes split-pane-slide-in {
  from { opacity: 0; transform: translateX(-12px); }
  to   { opacity: 1; transform: translateX(0); }
}
.widget-stage:has(.ui-card[data-state="picker"]) .split-pane.split-pane--chain-filter { animation: split-pane-slide-in .22s ease-out; }
@media (prefers-reduced-motion: reduce) {
  .split-pane.is-open,
  .widget-stage:has(.ui-card[data-state="picker"]) .split-pane.split-pane--chain-filter { animation: none; }
}
.split-pane__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-8);
}
.split-pane__title {
  font-size: var(--text-body);
  font-weight: var(--fw-bold);
  color: var(--text-primary);
  letter-spacing: var(--tracking-snug);
  margin: 0;
}
.split-pane__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  max-height: 480px;
  overflow-y: auto;
  /* hide scrollbar chrome — the list is short enough that scrollbar
     visual noise hurts more than it helps */
  scrollbar-width: thin;
}


/* ── .status-row — step-status row primitive ──────────────────────────────
   Single confirmation / step row with [icon] [label] [optional trailing
   action]. Used in widget completion screens (✓ Approved · ✓ Confirmed
   · ✉ Sent), settings sub-screens, and any future tx-confirmation flow.
   Variants by state are token-driven, not hard-coded.
*/
.status-row {
  display: grid;
  grid-template-columns: 24px 1fr auto;
  align-items: center;
  gap: var(--space-12);
  padding: var(--space-12) var(--space-16);
  background: color-mix(in oklch, currentColor 4%, transparent);
  border-radius: 12px;
  font-size: var(--text-sm);
  color: var(--text-primary);
}
.status-row__icon {
  width: var(--space-24); height: var(--space-24);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  flex: 0 0 auto;
  background: color-mix(in oklch, currentColor 12%, transparent);
}
.status-row__icon svg { width: var(--space-12); height: var(--space-12); }
.status-row__icon .spinner { --spinner-size: var(--space-16); }   /* active step — live spinner in the icon slot (no filled disc) */
.status-row--success { color: var(--success); }
.status-row--success .status-row__icon { background: color-mix(in oklch, var(--success) 18%, transparent); }
.status-row--success .status-row__label { color: var(--text-primary); }
.status-row--danger  { color: var(--danger); }
.status-row--danger  .status-row__icon { background: color-mix(in oklch, var(--danger) 18%, transparent); }
.status-row--danger  .status-row__label { color: var(--text-primary); }
.status-row--info { color: var(--info); }
.status-row--info .status-row__icon { background: color-mix(in oklch, var(--info) 14%, transparent); }
.status-row--info .status-row__label { color: var(--text-primary); }
/* Active — an in-flight step: the canonical .spinner sits in the icon slot
   (transparent disc), accent-toned. Pair with a <span class="spinner"> child
   instead of an icon svg. Added June 2026 for the Checkout pipeline checklist. */
.status-row--active { color: var(--accent-primary); }
.status-row--active .status-row__icon { background: transparent; }
.status-row--active .status-row__label { color: var(--text-primary); }
/* Pending — a not-yet-started step: an empty muted ring + quiet label, so a
   full pipeline checklist can show UPCOMING steps, not just completed ones
   (the swap widget appends rows as they land; the checkout shows them upfront). */
.status-row--pending { color: var(--text-faint); }
.status-row--pending .status-row__icon { background: transparent; box-shadow: inset 0 0 0 var(--space-2) color-mix(in oklch, var(--text-primary) 16%, transparent); }
.status-row--pending .status-row__label { color: var(--text-muted); }
.status-row__label {
  font-weight: var(--fw-medium);
  color: var(--text-primary);
}
.status-row__action {
  color: var(--text-secondary);
  background: transparent;
  border: 0;
  cursor: pointer;
  font-family: var(--font-sans);                     /* defensive — <button> UA default is Arial; render brand Figtree if ever given a text label / ::after glyph */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--space-24); height: var(--space-24);
  border-radius: var(--space-8);
  transition: background-color .12s ease, color .12s ease;
}
.status-row__action:hover {
  /* Neutral hover per the list-row state recipe — ink-derived, not a black
     literal (the old oklch(0% 0 0 / 0.04) was invisible in dark mode and
     ignored the theme). */
  background: color-mix(in oklch, var(--text-primary) 6%, transparent);
  color: var(--text-primary);
}
.status-row__action svg { width: var(--space-16); height: var(--space-16); }


/* ── .detail-list — universal description-list primitive ──────────────────
   Catalyst-flavored <dl>-style primitive for any "term + supporting
   content" pattern: fee breakdowns, account details, settings summaries,
   spec sheets, transaction receipts. Replaces the legacy .kv-row primitive
   (April 2026 migration — .kv-row was single-size, single-alignment, no
   container; .detail-list adds sizes, alignments, stacked rows, dividers,
   striping, headings, and a documented composition contract for embedded
   controls).

   Anatomy
     .detail-list                container — owns rhythm, dividers, sizing
       .detail-list__heading     (optional) section break inside a list
       .detail-list__row         one row of the list
         .detail-list__term      the dt — label / key
           svg                   (optional) leading info icon
         .detail-list__main      (optional) wraps term + sub for two-line
           .detail-list__sub     supporting copy under the term
         .detail-list__desc      the dd — value, control, badge, switch…

   Composition — the desc slot accepts any control:
     plain text · .badge · .switch · .btn-sm · .icon · avatar · tooltip
     trigger. Pair sizes 1:1 between the list and the embedded control
     (.detail-list--sm + .switch-sm, .detail-list--lg + .btn).

   Layout (pick one on the container):
     (default)                     term LEFT, desc RIGHT  (fee/summary)
     .detail-list--align-left      term LEFT, desc LEFT in wider column
                                   (spec-sheet pattern — Catalyst "Account
                                    details" variant)
     .detail-list--stacked         term ABOVE desc, full-width rows
                                   (verbose copy / mobile)

   Sizes (on the container):
     .detail-list--sm   12px · sidebar / dropdown / dense panel
     (default)          14px · standard
     .detail-list--lg   16px · hero / detail page / receipt

   Typography contract — font-size lives ONLY on .detail-list__row and
   cascades to both children. Term and desc render at the SAME size; only
   weight differs (term --fw-regular, desc --fw-semibold). Never override
   font-size on the term or desc individually. Bolder type reads slightly
   larger optically, but actual heights stay equal so the row keeps a
   calm baseline. For a smaller / larger pair, switch the size modifier
   on the container, not on a single child.

   Row chrome (on the container — combine freely with size + align):
     (default)                clean rows, only inter-row gap
     .detail-list--divided    hairline between rows
     .detail-list--striped    alternating row fill (skips headings via
                              :nth-child(... of ...) — modern browsers
                              only; avoid combining with headings on IE-
                              class targets)

   Desc state modifiers (on .detail-list__desc) — sentinel vs. semantic:
     .detail-list__desc--muted     secondary text + lighter weight.
                                   SENTINEL STRINGS ONLY — "Auto", "Default",
                                   "Included", "—". Never apply to real
                                   numeric / textual data. Renderers that
                                   swap a slot between sentinel and numeric
                                   must toggle this class as the value
                                   changes (see swap.js → renderReviewModel,
                                   slippage row).
     .detail-list__desc--warning   --warn   (notable but not danger; soft
                                   warning threshold defined by consumer —
                                   yield-rate dip past alert, elevated fee
                                   tier, APR delta nearing a watch line)
     .detail-list__desc--negative  --danger (losses, error magnitudes; e.g.
                                   "-$12.40", "-1.94% yield", failed-step
                                   cost)
     .detail-list__desc--positive  --success(gains; e.g. "+$12.40",
                                   "+0.42% yield", confirmed-success)

   Price impact is NOT a consumer of these modifiers. Quote-card detail
   row, body meta, and Receive card render price impact at neutral ink
   via window.LifiQuoteList.formatPriceImpact() (single source for the
   sign convention). Severity coloring is deferred as a future opt-in
   feature. See design/components/swap.md → "Price-impact display
   contract" for the canonical formatter + dormant CSS hooks.
*/
.detail-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  /* reset UA defaults when used as <dl>; harmless on <div> hosts */
  margin: 0;
  padding: 0;
}
/* Reset UA <dt>/<dd> margins when this primitive is rendered as <dl>/<dt>/<dd>. */
.detail-list__term,
.detail-list__desc { margin: 0; }
.detail-list__heading {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  font-weight: var(--fw-medium);
  text-transform: uppercase;
  letter-spacing: var(--tracking-loose);
  color: var(--text-muted);
  padding: var(--space-8) 0 var(--space-4);
  border-bottom: 1px solid var(--border-subtle);
  margin: 0 0 var(--space-4);
}
.detail-list__heading:not(:first-child) {
  margin-top: var(--space-16);
}
.detail-list__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-12);
  padding: var(--space-4) 0;
  font-size: var(--text-sm);
  line-height: 1.4;
}
.detail-list__term {
  display: inline-flex;
  align-items: center;
  gap: var(--space-4);
  color: var(--text-secondary);
  flex: 0 1 auto;
  min-width: 0;
}
.detail-list__term svg {
  width: var(--space-12);
  height: var(--space-12);
  color: var(--text-muted);
  flex: 0 0 auto;
}
/* An icon BUTTON in a term (e.g. an inline info / tooltip trigger beside the
   label — "Network cost (i)") sizes its glyph from its own --btn-icon-size
   ladder (xs 12 / sm 16 / default 20 / lg 24), NOT the generic term-svg rule
   above (meant for a bare leading/info glyph, pinned to 12px). Same trap as
   .list-item__trail, but more acute: the term rule is 12px at every tier, so a
   nested button glyph would be wrong even at the default tier (12 vs 20). The
   leading .detail-list lifts this to (0,3,1) so it beats the (0,2,1)
   .detail-list--sm/--lg term-svg variants regardless of source order. Additive
   — bare term glyphs keep the 12px tier sizing untouched. */
.detail-list .detail-list__term .btn-icon > svg {
  width: var(--btn-icon-size);
  height: var(--btn-icon-size);
}
.detail-list__main {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  flex: 1 1 auto;
  min-width: 0;
}
.detail-list__main .detail-list__term {
  flex: 0 0 auto;
}
.detail-list__sub {
  font-size: var(--text-caption);
  color: var(--text-muted);
  font-weight: var(--fw-regular);
  font-variant-numeric: normal;
  line-height: 1.35;
}
.detail-list__desc {
  color: var(--text-primary);
  font-weight: var(--fw-semibold);
  font-variant-numeric: tabular-nums;
  text-align: right;
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-8);
  flex: 0 0 auto;
  min-width: 0;
}
.detail-list__desc--muted    { color: var(--text-secondary); font-weight: var(--fw-medium); }
.detail-list__desc--warning  { color: var(--warn); }
.detail-list__desc--negative { color: var(--danger); }
.detail-list__desc--positive { color: var(--success); }

/* ── Sizes ───────────────────────────────────────────────────────────────── */
.detail-list--sm > .detail-list__row {
  font-size: var(--text-caption);
  padding: var(--space-4) 0;
  line-height: 1.35;
}
.detail-list--sm .detail-list__sub { font-size: var(--text-caption); }
.detail-list--sm .detail-list__term svg { width: var(--space-12); height: var(--space-12); }
.detail-list--lg > .detail-list__row {
  font-size: var(--text-body);
  padding: var(--space-8) 0;
  line-height: 1.5;
}
.detail-list--lg .detail-list__sub { font-size: var(--text-sm); }
.detail-list--lg .detail-list__term svg { width: var(--space-16); height: var(--space-16); }

/* ── Alignment — left/left, desc takes the wider column ─────────────────── */
.detail-list--align-left > .detail-list__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
  gap: var(--space-16);
  align-items: start;
}
.detail-list--align-left .detail-list__desc {
  text-align: left;
  justify-content: flex-start;
  font-weight: var(--fw-medium);
}

/* ── Stacked — term/main on top, desc below ─────────────────────────────── */
.detail-list--stacked > .detail-list__row {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-4);
  padding: var(--space-8) 0;
}
.detail-list--stacked .detail-list__desc {
  text-align: left;
  justify-content: flex-start;
  font-weight: var(--fw-medium);
}

/* ── Divided — hairline between rows ────────────────────────────────────── */
.detail-list--divided {
  gap: 0;
}
.detail-list--divided > .detail-list__row + .detail-list__row {
  border-top: 1px solid var(--border-subtle);
}
.detail-list--divided > .detail-list__row {
  padding-top: var(--space-8);
  padding-bottom: var(--space-8);
}
.detail-list--divided.detail-list--sm > .detail-list__row {
  padding-top: var(--space-4);
  padding-bottom: var(--space-4);
}
.detail-list--divided.detail-list--lg > .detail-list__row {
  padding-top: var(--space-12);
  padding-bottom: var(--space-12);
}

/* ── Striped — alternating row fill (skips headings) ────────────────────── */
.detail-list--striped {
  gap: 0;
}
.detail-list--striped > .detail-list__row {
  padding-left: var(--space-8);
  padding-right: var(--space-8);
  border-radius: var(--radius-sm);
}
.detail-list--striped > .detail-list__row:nth-child(odd of .detail-list__row) {
  background: color-mix(in oklch, var(--text-primary) 3%, transparent);
}
[data-theme="light"] .detail-list--striped > .detail-list__row:nth-child(odd of .detail-list__row) {
  background: color-mix(in oklch, var(--text-primary) 4%, transparent);
}


/* ══════════════════════════════════════════════════════════════════════════
   .list — universal stacked-row list primitive
   ──────────────────────────────────────────────────────────────────────────
   Slot-based row (lead | body | trail) that stacks vertically into lists.
   Default is FLAT — no per-row chrome — which is what separates this from
   .setting-row (chrome-heavy, single-row standalone). Reach for .list when
   you have N rows that should read as one continuous list: directories,
   contacts, activity feeds, selection lists, drill navigation, simple
   settings panels.

   For domain-specific list rows with their own chrome, semantics, or
   animation, the existing primitives stay where they are:
     .menu-item            FAB / dropdown menus — single-line desc, .menu-panel containment
     .setting-row          settings panels — per-row card chrome, drill variant
     .detail-list          term/desc spec sheets — &lt;dl&gt;/&lt;dt&gt;/&lt;dd&gt;, right-aligned value column
     .ui-token-row /
     .nav-dropdown-item    marketing nav dropdowns — translateX + staggered entrance
     .cmd-palette-result-item  search / cmd-palette rows — keyboard-driven highlight
   The new .list-item is the GENERIC default for everything else, and the
   #lists catalog section cross-links to all of the above.

   Anatomy
     .list                  container — manages stacking + optional chrome
       .list-section        (optional) group header (uppercase eyebrow)
       .list-item           one row of the list
         .list-item__lead     leading slot — icon, avatar, dot, checkbox
         .list-item__body     wraps title + optional desc
           .list-item__title  primary label (one line, bold)
           .list-item__desc   supporting line (1-2 lines, muted)
         .list-item__trail    trailing slot — chevron, value, switch, btn
           .list-item__meta   (optional) small caption inside trail

   Container modifiers (.list--*)
     (default)        clean stack — minimal gap, no dividers, no chrome
     --separated      hairline divider between adjacent items
     --card           whole list wrapped as one card (Tailwind shape)
     --inset          inner vertical padding when nested in a panel

   Item modifiers (.list-item--*)
     --sm / --lg      sizing ladder — heights 40 / 48 / 56
     --interactive    hover + cursor + focus ring (use as <button>/<a>)
     --selected       persistent selected state (current page, current chain)
     --nested         use with <summary> for zero-JS collapsible groups

   Composition
     Lead accepts:    <svg>, avatar, status dot, .chip, <input type=checkbox>,
                      <input type=radio>, .avatar, chain logo.
     Trail accepts:   chevron <svg>, plain text value, .chip, .switch, .btn-sm,
                      timestamp text via .list-item__meta.
     Body collapses:  __desc optional — one-line rows center vertically,
                      two-line rows grow naturally.

   Sizing follows the system ladder — heights step +8 (40 / 48 / 56) and
   lead-slot dimensions step +4 (20 / 24 / 32). Pair sizes 1:1 with embedded
   form controls (.list-item--sm + .switch-sm + .btn-sm; --lg + .switch-lg
   + .btn-lg).
   ══════════════════════════════════════════════════════════════════════════ */
.list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.list-item {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--space-12);
  padding: var(--space-8) var(--space-16);
  min-height: var(--space-48);
  border-radius: var(--space-12);
  color: var(--text-primary);
  text-decoration: none;
  font: inherit;
  text-align: left;
}

/* Lead is a positioning wrapper — never a sizing constraint. The
   content inside owns its own dimensions:
     <svg>                 → fallback rule below scopes default 20×20
     .spot-icon            → 32×32 default, ladder up to .spot-icon-3xl
     .avatar        → 40×40 default, --sm / --md / --lg / --xl
     .avatar-stack  → cluster (sized by its own children)
     <input type=checkbox/radio>, status dots, badges  → intrinsic sizes
   The .list-item grid's leftmost `auto` column sizes itself to the
   widest cell, so rows with different lead content still align across
   one list. Don't reintroduce hardcoded width/height here — that's
   exactly what made consumers paste inline overrides. */
.list-item__lead {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  color: var(--text-secondary);
}
.list-item__lead > svg {
  width: var(--space-20);
  height: var(--space-20);
}

.list-item__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;
}
.list-item__title {
  font-size: var(--text-body);
  font-weight: var(--fw-semibold);
  color: var(--text-primary);
  letter-spacing: var(--tracking-snug);
  line-height: 1.35;
}
.list-item__desc {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  line-height: var(--lh-sm);
}

.list-item__trail {
  display: inline-flex;
  align-items: center;
  gap: var(--space-8);
  flex: 0 0 auto;
  color: var(--text-secondary);
  font-size: var(--text-sm);
}
.list-item__trail svg {
  width: var(--space-20);
  height: var(--space-20);
  color: var(--text-muted);
}
/* An icon BUTTON in the trail sizes its glyph from its own --btn-icon-size
   ladder (xs 12 / sm 16 / default 20 / lg 24), NOT the generic trail-svg rule
   above (which targets bare chevrons / value glyphs). Same specificity as
   .btn-icon > svg but declared later, .list-item__trail svg would otherwise
   force every button's icon to 20px — fine for a default-tier button, but
   oversized for sm/xs (a 20px glyph in a 24px btn-xs nearly fills it). This
   re-asserts the button's own icon size; default-tier buttons are unchanged. */
.list-item__trail .btn-icon > svg {
  width: var(--btn-icon-size);
  height: var(--btn-icon-size);
}
.list-item__meta {
  font-size: var(--text-caption);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
/* Prominent trailing value — a balance / amount / count that reads as DATA, not
   metadata: primary ink, tabular figures, medium weight (the default trail is
   secondary). Replaces bespoke per-card amount classes (the retired
   .persona-connect-card__amount). Size still follows the tier's trail size. */
.list-item__trail--value {
  color: var(--text-primary);
  /* Semibold — the row's headline value reads as data; matches the
     .data-cell-text--bold value-emphasis weight (bumped from --fw-medium
     June 2026 so the prominent value actually reads prominent). */
  font-weight: var(--fw-semibold);
  font-variant-numeric: tabular-nums;
}

/* ── Sizes ───────────────────────────────────────────────────────────── */
.list-item--sm {
  padding: var(--space-4) var(--space-12);
  min-height: var(--space-40);
  gap: var(--space-8);
  border-radius: var(--space-8);
}
.list-item--sm .list-item__lead > svg { width: var(--space-16); height: var(--space-16); }
.list-item--sm .list-item__title { font-size: var(--text-sm); }
.list-item--sm .list-item__desc  { font-size: var(--text-caption); }

.list-item--lg {
  padding: var(--space-12) var(--space-20);
  min-height: var(--space-56);
  gap: var(--space-16);
  border-radius: var(--space-16);
}
.list-item--lg .list-item__lead > svg { width: var(--space-24); height: var(--space-24); }
.list-item--lg .list-item__title { font-size: var(--text-body-lg); }
.list-item--lg .list-item__desc  { font-size: var(--text-body); }

/* ── Interactive — hover / active / selected state recipe
   ────────────────────────────────────────────────────────────────────
   CANONICAL LIST-ROW STATE RECIPE (May 2026, promoted from the token
   picker). Three states span two hue families:

     HOVER    — neutral grey  (--text-primary  4%) — passive affordance
     SELECTED — accent calm   (--accent-primary 6%) — committed pick
     ACTIVE   — accent strong (--accent-primary 10%) — mouse-down press

   Hover is neutral so the accent doesn't telegraph "selected" before
   the user commits. Selected + active share the accent hue but step up
   in intensity (6 → 10) so press feedback reads as "this is what you're
   about to commit to." All three are TRANSLUCENT mixes over transparent
   so they composite with any underlying surface — panel (--surface-raised),
   card (--surface-card), page (--surface-page), or context-tinted
   container.

   Reuse for similar primitives (table rows, menu items, dense
   selection lists) is encouraged — promote the values into shared
   tokens when a third consumer arrives.

   The token picker extends this recipe via a ::before pseudo that
   bleeds --list-hover-bleed past the row's L/R edges (see swap.css
   →  .ui-token-list .list-item--interactive::before); the COLORS
   match exactly — only the painted surface extends.

   May 2026 — replaced the prior --overlay-hover / --overlay-press /
   --accent-primary 12% selected mix. The old values were calibrated
   for buttons + cards; the new recipe is row-specific. */
.list-item--interactive {
  cursor: pointer;
  transition: background-color .12s ease, transform .18s ease;
  border: 0;
  background: transparent;
  width: 100%;
}
.list-item--interactive:hover {
  background: color-mix(in oklch, var(--text-primary) 4%, transparent);
}
.list-item--interactive:active {
  background: color-mix(in oklch, var(--accent-primary) 10%, transparent);
}
.list-item--interactive:focus-visible {
  outline: 2px solid color-mix(in oklch, var(--accent-primary) 50%, transparent);
  outline-offset: 2px;
}

/* ── Selected — persistent state (current page, current chain, etc.)
   System-wide default. Targets both naming conventions:
     .list-item--selected   — BEM modifier (catalog, marketing surfaces)
     .list-item.is-selected — state class (JS toggles this; product widgets)
   Mix uses --accent-primary at 6% so the selected hue follows the active
   palette (LI.FI 1.0 → blue, Jumper 2.0 → its primary, etc.). No
   per-widget overrides — every list-item consumer gets the same
   selection treatment so designers + engineers learn one pattern. */
.list-item--selected,
.list-item.is-selected {
  background: color-mix(in oklch, var(--accent-primary) 6%, transparent);
}
.list-item--selected .list-item__title,
.list-item.is-selected .list-item__title { color: var(--text-primary); }
.list-item--selected .list-item__lead,
.list-item.is-selected .list-item__lead  { color: var(--accent-primary); }

/* ── Press feedback on already-selected rows — restore the 10% active
   tint when the user mouse-downs a row that's already in the selected
   state. Without this, .is-selected (specificity 0,2,0) wins over the
   base :active rule (0,1,1) — selected rows stay at 6% through the
   press, no tactile feedback. Specificity (0,3,1) here beats both. */
.list-item--interactive.is-selected:active,
.list-item--interactive.list-item--selected:active {
  background: color-mix(in oklch, var(--accent-primary) 10%, transparent);
}

/* ── Container chrome — pick at most one ─────────────────────────────
   --card     wrap whole list as a card surface (Tailwind shape)
   --separated  hairline divider between adjacent items
   --inset    vertical breathing when list is nested inside a panel
   ──────────────────────────────────────────────────────────────────── */
.list--card {
  background: var(--surface-card);
  border: 1px solid color-mix(in oklch, currentColor 4%, transparent);
  border-radius: var(--space-16);
  box-shadow: var(--elev-1);
  padding: var(--space-4);
  gap: 0;
}
.list--card > .list-item { border-radius: var(--space-12); }

.list--separated > .list-item + .list-item {
  border-top: 1px solid var(--border-subtle);
}
/* When --separated is combined with --card, run dividers edge-to-edge */
.list--card.list--separated > .list-item + .list-item {
  border-radius: 0;
}
.list--card.list--separated > .list-item:first-child { border-radius: var(--space-12) var(--space-12) 0 0; }
.list--card.list--separated > .list-item:last-child  { border-radius: 0 0 var(--space-12) var(--space-12); }

/* ── .list--cards — each item is its OWN card ─────────────────────────────
   The whole list reads as a STACK OF CARDS — vs .list--card (singular), where
   the whole list is one grouped card. For selectable / navigable card lists:
   wallet + ecosystem pickers, option lists, choice groups. Composes with the
   .list-item size tiers and .list-item--interactive (the state rules below
   switch from the flush ROW recipe to the canonical UI-CARD recipe — accent
   hover/active + an accent rim on selection).

   vs .setting-row: that's a settings "label + value/control" card row with a
   24–32 px icon column; .list--cards cards a GENERIC .list-item (lead /
   title+desc / trailing), so a 40 px avatar + a trailing chip fit with no
   markup rewrite. Reach for .setting-row for settings; .list--cards for
   selection / choice lists.

   Card padding is SYMMETRIC and roomier than a flush row (rows use asymmetric
   L/R for text breathing; cards don't) — its own +4 ladder: sm 8 / default 12
   / lg 16. The small container padding is a shadow gutter: .modal-body and
   scroll containers clip paint on BOTH axes (overflow-y:auto forces overflow-x
   to auto), so flush cards would lose their --elev-1 shadow at the box edge.

   These state rules sit AFTER the canonical row-state recipe above, so the one
   equal-specificity (0,3,0) edge — .is-selected:active — resolves here. */
.list--cards {
  /* Per-card tone — drives the accent hover/active tint + the selected rim,
     routed through a token so a selection list CAN carry per-row tier colours.
     Default brand primary, so every existing .list--cards consumer (incl. the
     connect / identity rows, which deliberately stay uniform) is unchanged.
     Opt in per row to tone-code a list:
       style="--list-card-tone: var(--warn);"  (variant-aware states pattern) */
  --list-card-tone: var(--accent-primary);
  --card-gap: var(--gap-card-sm);
  gap: var(--card-gap);
  padding: var(--space-8);
}
.list--cards > .list-item {
  background: var(--surface-card);
  border: 1px solid color-mix(in oklch, currentColor 4%, transparent);
  border-radius: var(--card-radius);
  box-shadow: var(--elev-1);
  padding: var(--space-12);
}
.list--cards > .list-item--sm { padding: var(--space-8); }
.list--cards > .list-item--lg { padding: var(--space-16); }

.list--cards > .list-item--interactive {
  transition: background-color .18s ease, border-color .18s ease, box-shadow .18s ease;
}
.list--cards > .list-item--interactive:hover {
  background: color-mix(in oklab, var(--list-card-tone) 3%, var(--surface-card));
  box-shadow: var(--elev-2);
}
.list--cards > .list-item--interactive:active {
  background: color-mix(in oklab, var(--list-card-tone) 4%, var(--surface-card));
  box-shadow: var(--elev-2);
}
/* Persistent selection — 1 px tone rim + 4 % fill, no layout shift (the rest
   border is always 1 px; only its color flips). Tone defaults to brand primary
   via --list-card-tone; per-row consumers (the persona picker) override it. */
.list--cards > .list-item.is-selected,
.list--cards > .list-item--selected {
  background: color-mix(in oklab, var(--list-card-tone) 4%, var(--surface-card));
  border-color: color-mix(in oklab, var(--list-card-tone) 64%, transparent);
  box-shadow: var(--elev-2);
}

.list--inset {
  padding: var(--space-12) 0;
}

/* ── .list-section — group header (uppercase eyebrow) ─────────────────
   Used between groups of items to break a long list by category —
   A/B/C in a directory, Today/Yesterday in an activity feed, group
   names in a contact list. */
.list-section {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  font-weight: var(--fw-medium);
  text-transform: uppercase;
  letter-spacing: var(--tracking-loose);
  color: var(--text-muted);
  padding: var(--space-12) var(--space-16) var(--space-4);
  margin: 0;
}
.list-section:not(:first-child) { margin-top: var(--space-8); }

/* ── Nested — zero-JS collapsible parent + indented children ──────────
   Use a <details> wrapper with <summary class="list-item list-item--nested">
   as the parent row, followed by a nested .list of children. The trail
   chevron rotates 90° on [open]. The native disclosure marker is hidden
   so the .list-item__trail chevron is the only affordance on screen. */
.list-item--nested {
  list-style: none;
}
.list-item--nested::-webkit-details-marker { display: none; }
.list-item--nested .list-item__trail svg {
  transition: transform .18s ease;
}
details[open] > .list-item--nested .list-item__trail svg {
  transform: rotate(90deg);
}
details > .list-item--nested + .list {
  padding-left: var(--space-32);
}


/* ── .avatar-stack--aggregate — "All networks" icon-slot variant ───────
   Used as the icon-slot indicator for "All networks" / multi-chain /
   multi-provider rows inside list primitives (.list-item composites).
   Wraps the canonical .avatar-stack as a circular chip so the cluster
   reads as ONE icon — different intent from the standalone .avatar-stack
   (where each child reads as its own avatar). The canonical block above
   handles base behaviour; this variant adds the chip frame + its
   children's compressed dimensions. */
.avatar-stack--aggregate {
  width: var(--space-32);
  height: var(--space-32);
  border-radius: 50%;
  background: color-mix(in oklch, var(--accent-primary) 6%, transparent);
  align-items: center;
  justify-content: center;
  padding: 0;
}
.avatar-stack--aggregate > * {
  width: var(--space-12);
  height: var(--space-12);
  border: 1.5px solid var(--surface-page);
  border-radius: 50%;
  background: var(--ink-200);
  margin-left: calc(-1 * var(--space-4));
  display: inline-block;
  flex: 0 0 auto;
}
.avatar-stack--aggregate > *:first-child { margin-left: 0; }

/* ── Responsive ──────────────────────────────────────────────────────────── */
@media (max-width: 600px) {
  .form-row { grid-template-columns: 1fr; }
  /* .form-card padding auto-adapts via --pad-chrome-xl */
}

/* ── .form-card--auth ──────────────────────────────────────
   Modifier for auth / sign-in style floating forms.  Resets
   the outer padding to zero and clips children so two internal
   slots (.form-card__body, .form-card__foot) can run
   flush edge-to-edge with a hairline separator. Pair with a
   footer background tint that reinforces "this is an auxiliary
   action" (e.g. "new here? create an account"). */
.form-card--auth {
  padding: 0;
  overflow: hidden;
}
.form-card__body {
  padding: 48px 48px 40px;
}
.form-card__body .form-group { margin-bottom: 24px; }
.form-card__foot {
  padding: 32px 48px;
  text-align: center;
  border-top: 1px solid var(--border-subtle);
  background: color-mix(in oklch, var(--accent-primary) 2%, transparent);
}
[data-theme="light"] .form-card__foot {
  background: color-mix(in oklch, var(--accent-primary) 4%, transparent);
}
.form-card__foot-label {
  display: block;
  font-size: var(--text-body);
  color: var(--text-muted);
  margin-bottom: 16px;
}
@media (max-width: 600px) {
  .form-card__body { padding: 40px 24px 32px; }
  .form-card__body .form-group { margin-bottom: 16px; }
  .form-card__foot { padding: 24px; }
}


/* ══════════════════════════════════════════
   10. STAT BAR — reusable stats counter strip
   ══════════════════════════════════════════ */

/* ── Stat Tile — canonical metric tile primitive ──────────────────────────
   Each tile holds a `.metric-display` + `.metric-label`. Three sizes:
   default / --md / --lg. Layout is up to the consumer — drop tiles into
   any flex / grid wrapper. The previous `.stat-bar` layout helper was
   removed in May 2026; consumers compose their own grid.

   `min-width: 0` so the tile can shrink below its content width inside
   a grid. `container-type: inline-size` enables `cqi` units on the --lg
   metric so the heading auto-scales to tile width regardless of how
   many tiles share the row.
   ─────────────────────────────────────────────────────────────────────── */
/* Stat tile — three sizes (sm / default / lg). All values on design-system
   tokens (--space-* / --text-*). Default is the marketing sweet spot used
   on every public page; --sm densifies for dashboards, --lg scales the
   metric via container-query (cqi) so a 2-up row produces a bigger number
   than a 4-up row, clamped to --text-hero. */
.stat-tile {
  min-width: 0;
  container-type: inline-size;
  text-align: center;
  background: var(--surface-card);
  backdrop-filter: blur(40px) saturate(120%);
  -webkit-backdrop-filter: blur(40px) saturate(120%);
  border: 1px solid color-mix(in oklch, var(--accent-primary) 6%, transparent);
  border-radius: 20px;
  padding: var(--space-40) var(--space-24);
  position: relative; overflow: hidden;
  isolation: isolate;
  transition: background 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;
}

.stat-tile:hover {
  background: color-mix(in oklch, var(--accent-primary) 6%, transparent);
  border-color: var(--accent-primary-mid);
}
.stat-tile:active {
  background: color-mix(in oklch, var(--accent-primary) 7%, transparent);
  border-color: var(--accent-primary-mid);
  box-shadow: 0 0 18px 0 color-mix(in oklch, var(--accent-primary) 18%, transparent);
}

/* Default metric sizing */
.stat-tile .metric-display {
  font-size: var(--text-display); letter-spacing: var(--tracking-tighter);
  position: relative; z-index: 1;
}
.stat-tile .metric-label {
  margin-top: var(--space-8);
  position: relative; z-index: 1;
}

/* Small — dashboards, dense KPI rows. Tighter padding, --text-h2 metric. */
.stat-tile--sm {
  padding: var(--space-24) var(--space-20);
  border-radius: 16px;
}
.stat-tile--sm .metric-display { font-size: var(--text-h2); }
.stat-tile--sm .metric-label   { margin-top: var(--space-4); }

/* Large — top-of-landing hero KPIs. Metric uses cqi so it scales with tile
   width inside any grid layout, clamped to --text-hero so wide tiles don't
   blow up. Larger padding, larger label. */
.stat-tile--lg {
  padding: var(--space-56) var(--space-32);
  border-radius: 24px;
}
.stat-tile--lg .metric-display {
  font-size: clamp(2.5rem, 30cqi, var(--text-hero));
  letter-spacing: var(--tracking-tighter);
}
.stat-tile--lg .metric-label {
  font-size: var(--text-sm);
  margin-top: var(--space-16);
}

/* Scroll-reveal */
.stat-tile.visible { animation: fadeUp 0.8s forwards; }

/* Light theme */
[data-theme="light"] .stat-tile {
  background: color-mix(in oklch, var(--surface-page) 72%, transparent);
  border-color: color-mix(in oklch, var(--accent-primary) 10%, transparent);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.7);
}
[data-theme="light"] .stat-tile:hover {
  background: color-mix(in oklch, var(--accent-primary) 6%, #FFFFFF);
  border-color: var(--accent-primary-mid);
}
[data-theme="light"] .stat-tile:active {
  background: color-mix(in oklch, var(--accent-primary) 7%, #FFFFFF);
  border-color: var(--accent-primary-mid);
  box-shadow: 0 0 18px 0 color-mix(in oklch, var(--accent-primary) 18%, transparent);
}
[data-theme="light"] .stat-tile .metric-label {
  color: color-mix(in oklch, var(--text-primary) 40%, transparent);
}

/* Responsive metric sizing on mobile */
@media (max-width: 768px) {
  .stat-tile .metric-display     { font-size: var(--text-h1); }
  .stat-tile--lg .metric-display { font-size: var(--text-display); }
}

/* ══════════════════════════════════════════
   CONTENT TILE — flexible-content tile primitive
   ══════════════════════════════════════════
   Container for rich-content compositions: eyebrow + chip + title +
   paragraph + bullets + small/big numbers, in any order, on any
   material. Distinct from siblings:
     • .stat-tile — strict KPI (1-3 word label, hero number, no prose)
     • .tile      — marketing-led, icon + title + body + meta + CTA
     • .feature-card — icon-led feature showcase
     • .accent-card  — pricing/promo with rigid slot order

   .content-tile is a CONTAINER. Padding + material + size + accent
   live on the wrapper; content composes from existing primitives
   (.chip, .metric-display, <h3>, <p>, <ul>, etc.). Two tile-specific
   slot helpers (.content-tile-eyebrow, .content-tile-meta) and one
   embedded big-number block (.content-tile-metric) cover the
   tile-only typography that no existing class fits.

   Sizes: sm / default / lg / xl — radius + padding + gap step on the
   4 px grid. Materials ride the canonical --mat-* recipe via
   data-material. Accents resolve via data-accent on the tile, which
   sets --tile-accent and tints the eyebrow + border. Spectrum bands
   (data-accent="spectral-1..12") provide categorical / decorative
   coloring that doesn't ride the palette switcher.

   Audience: public-site sections, decks, dashboard onboarding tiles,
   product feature explainer blocks. data-scope="ui marketing".

   Catalog: #content-tiles in design-system/index.html.
   Spec:    design/components/surfaces.md → Content tile.
   ══════════════════════════════════════════ */
.content-tile {
  --tile-accent: var(--accent-primary);
  --accent-tint: var(--tile-accent);

  display: flex;
  flex-direction: column;
  gap: var(--space-12);
  min-width: 0;
  min-height: 240px;

  background: var(--mat-bg);
  border: var(--mat-border);
  border-radius: var(--space-20);
  padding: var(--space-32);
  box-shadow: var(--mat-shadow), var(--mat-highlight);
  backdrop-filter: var(--mat-backdrop);
  -webkit-backdrop-filter: var(--mat-backdrop);

  position: relative;
  overflow: hidden;
}

.content-tile--sm {
  gap: var(--space-8);
  border-radius: var(--space-16);
  padding: var(--space-20);
  min-height: 160px;
}
.content-tile--lg {
  gap: var(--space-16);
  border-radius: var(--space-24);
  padding: var(--space-40);
  min-height: 320px;
}
.content-tile--xl {
  gap: var(--space-20);
  border-radius: var(--space-28);
  padding: var(--space-48);
  min-height: 400px;
}

/* Slot helpers — tile-specific text styles. Use INSIDE .content-tile;
   outside the tile, prefer .eyebrow-code (page-scope) and inline
   caption text. */

.content-tile-eyebrow {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--accent-secondary);
}

.content-tile-meta {
  margin: auto 0 0;
  font-size: var(--text-caption);
  color: var(--text-secondary);
}

/* Big-number block — embeds .metric-display + an inline label.
   Display defaults to --text-display (56px); --lg / --xl tiles
   step up to --text-hero (80px); --sm tiles step down to
   --text-h2 (32px). */
.content-tile-metric {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  margin: 0;
}
.content-tile-metric .metric-display {
  font-size: var(--text-display);
  letter-spacing: var(--tracking-tighter);
  line-height: 1.0;
}
.content-tile--sm .content-tile-metric .metric-display { font-size: var(--text-h2); }
.content-tile--lg .content-tile-metric .metric-display,
.content-tile--xl .content-tile-metric .metric-display { font-size: var(--text-hero); }

.content-tile-metric .metric-label {
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

/* Accent system — data-accent sets --tile-accent and tints the
   eyebrow + border. Brand accents (primary/secondary/tertiary)
   ride the Theme Composer; spectrum bands (spectral-1..12) are
   stable across preset switches per the data-viz color tier rule. */
.content-tile[data-accent="primary"]   { --tile-accent: var(--accent-primary); }
.content-tile[data-accent="secondary"] { --tile-accent: var(--accent-secondary); }
.content-tile[data-accent="tertiary"]  { --tile-accent: var(--accent-tertiary); }

.content-tile[data-accent="spectral-1"]  { --tile-accent: var(--spectral-1);  }
.content-tile[data-accent="spectral-2"]  { --tile-accent: var(--spectral-2);  }
.content-tile[data-accent="spectral-3"]  { --tile-accent: var(--spectral-3);  }
.content-tile[data-accent="spectral-4"]  { --tile-accent: var(--spectral-4);  }
.content-tile[data-accent="spectral-5"]  { --tile-accent: var(--spectral-5);  }
.content-tile[data-accent="spectral-6"]  { --tile-accent: var(--spectral-6);  }
.content-tile[data-accent="spectral-7"]  { --tile-accent: var(--spectral-7);  }
.content-tile[data-accent="spectral-8"]  { --tile-accent: var(--spectral-8);  }
.content-tile[data-accent="spectral-9"]  { --tile-accent: var(--spectral-9);  }
.content-tile[data-accent="spectral-10"] { --tile-accent: var(--spectral-10); }
.content-tile[data-accent="spectral-11"] { --tile-accent: var(--spectral-11); }
.content-tile[data-accent="spectral-12"] { --tile-accent: var(--spectral-12); }

.content-tile[data-accent] .content-tile-eyebrow {
  color: var(--tile-accent);
}
.content-tile[data-accent]:not([data-material="gradient"]) {
  border-color: color-mix(in oklch, var(--tile-accent) 18%, transparent);
}

/* Gradient material — text reads on top of brand gradient fill, so
   eyebrow / meta flip to white alpha. Mirrors .stat-card--grad-bold. */
.content-tile[data-material="gradient"] {
  color: white;
}
.content-tile[data-material="gradient"] .content-tile-eyebrow,
.content-tile[data-material="gradient"] .content-tile-meta {
  color: rgba(255, 255, 255, 0.85);
}
.content-tile[data-material="gradient"] .metric-label {
  color: rgba(255, 255, 255, 0.75);
}

/* Light-theme tweaks — meta needs a slightly stronger color in light
   mode for legibility against the lighter card surface. */
[data-theme="light"] .content-tile-meta {
  color: color-mix(in oklch, var(--text-primary) 60%, transparent);
}

@media (max-width: 640px) {
  .content-tile--lg { padding: var(--space-32); min-height: 280px; }
  .content-tile--xl { padding: var(--space-32); min-height: 320px; }
  .content-tile-metric .metric-display { font-size: var(--text-h1); }
  .content-tile--lg .content-tile-metric .metric-display,
  .content-tile--xl .content-tile-metric .metric-display { font-size: var(--text-display); }
}

/* ══════════════════════════════════════════
   BRAND TILE — saturated marketing-moment tile
   ══════════════════════════════════════════
   Marketing-grade tile with a saturated solid or gradient fill,
   one hero text element, and an optional attribution slot
   (avatar + name + role). The "bright tier" complement to the
   quiet paper / glass / halo materials on .content-tile.

   Distinct from siblings:
     • .content-tile — flexible content blocks, paper/glass/halo
     • .stat-tile    — strict KPI, hero number, no prose
     • .tile         — icon-led marketing personality

   The fill IS the design. data-fill picks the surface AND the
   contrasting foreground tone in one attribute, so authors never
   set --brand-tile-fg by hand. Six solid fills (brand-blue /
   brand-teal / brand-pink / lime / lavender / ink) and six
   gradient fills (dawn / dusk / tide / citrus / aurora / ink)
   cover the marketing vocabulary. Brand fills consume
   var(--lifi-*) / var(--accent-*) and ride theme changes;
   spectrum and pastel fills are theme-stable by design.

   Sizes: sm / default / lg / xl on the same 4 px ladder as
   .content-tile — padding 20/32/40/48 · radius 16/20/24/28 ·
   min-height 160/240/320/400.

   Audience: testimonials, partner pages, keynote slides,
   marketing-section anchors. data-scope="marketing".

   Catalog: #brand-tiles in design-system/index.html.
   Spec:    design/components/surfaces.md → Brand tile.
   ══════════════════════════════════════════ */
.brand-tile {
  /* Default fill — brand primary (LI.FI blue). Foreground auto-derives
     from the background's OKLCH lightness via the relative-color
     formula. data-fill overrides --brand-tile-bg; fg recomputes
     automatically because it reads from --brand-tile-bg. */
  --brand-tile-bg: var(--accent-primary);
  --brand-tile-fg: oklch(from var(--brand-tile-bg) calc((0.65 - l) * 1000) 0 0);

  display: flex;
  flex-direction: column;
  gap: var(--space-16);
  min-width: 0;
  min-height: 240px;

  background: var(--brand-tile-bg);
  color: var(--brand-tile-fg);
  border-radius: var(--space-20);
  padding: var(--space-32);

  position: relative;
  overflow: hidden;
}

.brand-tile--sm {
  gap: var(--space-12);
  border-radius: var(--space-16);
  padding: var(--space-20);
  min-height: 160px;
}
.brand-tile--lg {
  gap: var(--space-20);
  border-radius: var(--space-24);
  padding: var(--space-40);
  min-height: 320px;
}
.brand-tile--xl {
  /* Keynote tier — diverges from the .content-tile xl ladder on
     purpose. .brand-tile xl is THE marketing-anchor moment: a
     section opener or slide hero where the typography is meant to
     stop you in the hallway. Generous padding + radius + min-height
     pair with mega-scale quote font below. */
  gap: var(--space-32);
  border-radius: var(--space-32);
  padding: var(--space-64);
  min-height: 480px;
}

/* Slot helpers — tile-specific text styles. Every slot reads
   from currentColor so the foreground auto-flows from the fill's
   --brand-tile-fg. Muted slots mix currentColor with transparent
   (safe in OKLCH — transparent has no stored hue). */

/* Eyebrow — uppercase caption-tier kicker. Pairs --text-caption
   with --lh-caption (12 / 16 = 1.333 ratio). Tracking uses the
   --tracking-wider token, which the type-scale comment explicitly
   labels "eyebrow uppercase". */
.brand-tile-eyebrow {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  line-height: var(--lh-caption);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: color-mix(in oklch, currentColor 75%, transparent);
}

/* Quote — hero text, sized per tile tier. Every fixed size pairs
   with its sibling --lh-* token (h2 / body-lg / h1 / etc.). The
   --xl clamped variant overrides with a unitless literal further
   below, per the CLAUDE.md "clamp-to-display" rule. Tracking
   follows the scale: h1/h2/display → --tracking-tight. */
.brand-tile-quote {
  margin: 0;
  font-size: var(--text-h2);
  line-height: var(--lh-h2);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-tight);
  color: currentColor;
}
.brand-tile--sm .brand-tile-quote {
  font-size: var(--text-body-lg);
  line-height: var(--lh-body);
}
.brand-tile--lg .brand-tile-quote {
  font-size: var(--text-h1);
  line-height: var(--lh-h1);
}
.brand-tile--xl .brand-tile-quote {
  /* Poster-scale keynote type. Caps at --text-poster (112 px) on
     wide viewports, falls back to --text-h1 (48 px) on phones,
     fluid via 6.5vw in between. Line-height 1.06 bridges the
     CLAUDE.md rules: 1.18 ideal for h1 (48 px), 0.94–1.0 ideal
     for mega (112 px+). 1.06 reads correctly at both ends. */
  font-size: clamp(var(--text-h1), 6.5vw, var(--text-poster));
  line-height: 1.06;
  letter-spacing: var(--tracking-tighter);
}

.brand-tile-meta {
  margin: 0;
  font-size: var(--text-caption);
  line-height: var(--lh-caption);
  color: color-mix(in oklch, currentColor 65%, transparent);
}

/* Keynote-tier eyebrow + meta — the xl tier rebalances the three
   text slots into a magazine-style hierarchy: substantial eyebrow
   kicker, hero title, readable subtitle. Ratios eyebrow:title:meta
   land near 1 : 4 : 1.2 — the title dominates but the eyebrow and
   description both hold real weight, not cramped captions. */
.brand-tile--xl .brand-tile-eyebrow {
  /* Eyebrow scales up at xl but keeps the eyebrow-uppercase tracking
     (--tracking-wider, 0.14em). Pairing: body-lg + --lh-body (24 px,
     ratio 1.33) per the design.md type-pairing table. */
  font-size: var(--text-body-lg);
  line-height: var(--lh-body);
}
.brand-tile--xl .brand-tile-meta {
  /* Body-xl pairs with --lh-body-xl (32 px, ratio 1.6 — "oversized
     lede" per the type-scale comment). 60ch reading limit keeps
     the meta line from running edge-to-edge at the 64 px-padded
     keynote tier. */
  font-size: var(--text-body-xl);
  line-height: var(--lh-body-xl);
  max-width: 60ch;
}

/* Attribution slot — flex row at the floor of the tile.
   Pin via margin-top: auto so it anchors to the bottom regardless
   of content height above. Compose with .avatar (size --sm or --md)
   on the left and the name/role stack on the right. */
.brand-tile-attribution {
  margin-top: auto;
  display: flex;
  align-items: center;
  gap: var(--space-16);
}

.brand-tile-attribution-text {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;
}

.brand-tile-attribution-name {
  margin: 0;
  font-size: var(--text-sm);
  line-height: var(--lh-sm);
  font-weight: var(--fw-semibold);
  color: currentColor;
}

.brand-tile-attribution-role {
  margin: 0;
  font-size: var(--text-caption);
  line-height: var(--lh-caption);
  color: color-mix(in oklch, currentColor 65%, transparent);
}

/* Optional vertical hairline between avatar and text — Linear vibe.
   Toggle with --ruled modifier on the .brand-tile-attribution row. */
.brand-tile-attribution--ruled .brand-tile-attribution-text {
  padding-left: var(--space-16);
  border-left: 1px solid color-mix(in oklch, currentColor 22%, transparent);
}

/* Strip the global gradient-text effect from .metric-display when it
   composes inside a .brand-tile. The tile's auto-flipping foreground
   wins over the gradient — every text element inside .brand-tile
   must read from currentColor so contrast holds across all 15 fills.
   Without this override, .metric-display paints the LI.FI brand
   gradient onto numbers, defeating the auto-contrast guarantee. */
.brand-tile .metric-display {
  background: none;
  -webkit-text-fill-color: currentColor;
  color: currentColor;
}

/* Re-tone chips composed inside a .brand-tile.
   The global .chip-accent* / .chip-success / etc. tones paint from
   fixed tokens (--accent-primary, --success, …) which fight the
   tile's saturated fill — e.g. a brand-blue chip on a magenta tile
   reads as two competing brand voices. Inside .brand-tile, every
   chip inherits from currentColor (the tile's auto-flipping fg),
   so chips harmonize with whichever fill the tile is using.
   Designers who need a semantic chip (real success / danger state)
   can re-assert with a more specific selector or inline tokens.

   Two rules — the default is (0,2,0); the [data-theme="light"]
   variant is (0,3,0), needed to beat the global
   [data-theme="light"] .chip-accent1:not(.chip--outline) selector
   which would otherwise re-paint the chip with --accent-primary
   in light mode. Both rules share the same recipe; only the
   selector specificity differs. */
.brand-tile .chip,
[data-theme="light"] .brand-tile .chip {
  background: color-mix(in oklch, currentColor 16%, transparent);
  color: currentColor;
  border-color: transparent;
}
.brand-tile .chip--outline,
[data-theme="light"] .brand-tile .chip--outline {
  background: transparent;
  border-color: color-mix(in oklch, currentColor 40%, transparent);
  color: currentColor;
}

/* ──────────────────────────────────────────
   FILL VOCABULARY — data-fill="..."

   Fifteen solid fills. Three brand fills consume the palette-driven
   --accent-* tokens (ride the Theme Composer); twelve spectrum
   fills consume --spectral-1..12 (palette-stable across preset
   switches by design — see "Three-tier color system for data viz"
   in design.md §13).

   Every fill sets --brand-tile-bg only. Foreground auto-derives
   from the bg's OKLCH lightness via the formula on the base
   .brand-tile rule:
     --brand-tile-fg: oklch(from var(--brand-tile-bg) calc((0.65 - l) * 1000) 0 0)

   No gradients. No inline OKLCH literals. Every value traces back
   to a foundational token; foreground recomputes on every token
   shift — Theme Composer, theme toggle, custom preset.
   ────────────────────────────────────────── */

/* Brand fills — palette-driven · order: primary · secondary · tertiary. */
.brand-tile[data-fill="primary"]   { --brand-tile-bg: var(--accent-primary); }
.brand-tile[data-fill="secondary"] { --brand-tile-bg: var(--accent-secondary); }
.brand-tile[data-fill="tertiary"]  { --brand-tile-bg: var(--accent-tertiary); }

/* Spectrum fills — palette-stable · order: warm → cool around the wheel. */
.brand-tile[data-fill="coral"]   { --brand-tile-bg: var(--spectral-1); }
.brand-tile[data-fill="orange"]  { --brand-tile-bg: var(--spectral-2); }
.brand-tile[data-fill="amber"]   { --brand-tile-bg: var(--spectral-3); }
.brand-tile[data-fill="lime"]    { --brand-tile-bg: var(--spectral-4); }
.brand-tile[data-fill="green"]   { --brand-tile-bg: var(--spectral-5); }
.brand-tile[data-fill="teal"]    { --brand-tile-bg: var(--spectral-6); }
.brand-tile[data-fill="cyan"]    { --brand-tile-bg: var(--spectral-7); }
.brand-tile[data-fill="sky"]     { --brand-tile-bg: var(--spectral-8); }
.brand-tile[data-fill="blue"]    { --brand-tile-bg: var(--spectral-9); }
.brand-tile[data-fill="violet"]  { --brand-tile-bg: var(--spectral-10); }
.brand-tile[data-fill="magenta"] { --brand-tile-bg: var(--spectral-11); }
.brand-tile[data-fill="pink"]    { --brand-tile-bg: var(--spectral-12); }

@media (max-width: 640px) {
  .brand-tile--lg { padding: var(--space-32); min-height: 280px; }
  .brand-tile--xl { padding: var(--space-40); min-height: 380px; }
  .brand-tile--lg .brand-tile-quote {
    font-size: var(--text-h2);
    line-height: var(--lh-h2);
  }
  /* xl quote already uses clamp() — no override needed; 6.5vw
     naturally collapses to --text-h1 on phones. xl eyebrow and
     meta drop one tier each; line-heights also re-pair so the
     ratios stay correct at the smaller sizes. */
  .brand-tile--xl .brand-tile-eyebrow {
    font-size: var(--text-body);
    line-height: var(--lh-body);
  }
  .brand-tile--xl .brand-tile-meta {
    font-size: var(--text-body-lg);
    line-height: var(--lh-body);
  }
}

/* ══════════════════════════════════════════
   9. FOOTER
   ══════════════════════════════════════════ */
.footer {
  background: var(--surface-raised);
  border-top: 1px solid var(--border-brand);
  position: relative; z-index: 2;
}
.footer-inner {
  max-width: 1200px; margin: 0 auto;
  padding: var(--space-80) var(--pad-section-x) var(--space-48);
  display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: var(--space-64);
}
.footer-col { display: flex; flex-direction: column; gap: var(--cluster-md); }
.footer-col h3 {
  font-size: var(--text-caption); font-weight: var(--fw-bold); color: var(--text-faint);
  text-transform: uppercase; letter-spacing: var(--tracking-wide); margin-bottom: var(--space-8);
}
.footer-col a {
  font-size: var(--text-sm); color: var(--text-muted);
  font-weight: var(--fw-regular); text-decoration: none; transition: color 0.2s;
}
.footer-col a:hover { color: var(--text-primary); }
.footer-col a.active { color: var(--accent-primary); font-weight: var(--fw-medium); }
.footer-bottom {
  max-width: 1200px; margin: 0 auto; padding: var(--space-24) var(--pad-section-x) var(--space-48);
  display: flex; align-items: center; gap: var(--cluster-lg);
  border-top: 1px solid var(--border-subtle);
}
.footer-bottom-left { display: flex; align-items: center; gap: var(--cluster-sm); }
.footer-logo { height: 20px; width: auto; color: var(--text-faint); flex-shrink: 0; }
.footer-copy, .footer-bottom p { font-size: var(--text-caption); color: color-mix(in oklch, var(--text-faint) 60%, transparent); margin: 0; }
.footer-legal {
  display: flex; gap: 24px; margin: 0 auto;
}
.footer-socials { display: flex; gap: 8px; }
.footer-social { --btn-icon-size: 14px; }
.footer-legal a {
  font-size: var(--text-sm); color: var(--text-faint);
  text-decoration: none; transition: color 0.2s;
}
.footer-legal a:hover { color: var(--text-secondary); }


/* ══════════════════════════════════════════
   8. THEME TOGGLE
   ══════════════════════════════════════════ */
/* Theme toggle — extends .btn-icon .btn-sm for the animated sun/moon swap */
.theme-toggle {
  position: relative; overflow: hidden;
}
.theme-toggle svg {
  width: var(--btn-icon-size) !important; height: var(--btn-icon-size) !important;
  position: absolute;
  transition: opacity 0.3s, transform 0.3s;
  fill: none; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round;
}
/* Current-mode convention (May 2026): the glyph shows the mode you're IN,
   not the one you'd switch to. Dark mode → moon; light mode → sun. */
.theme-toggle .icon-sun  { opacity: 0; transform: rotate(-90deg); stroke: currentColor; }
.theme-toggle .icon-moon { opacity: 1; transform: rotate(0deg);   stroke: currentColor; }
[data-theme="light"] .theme-toggle .icon-sun  { opacity: 1; transform: rotate(0deg); }
[data-theme="light"] .theme-toggle .icon-moon { opacity: 0; transform: rotate(90deg); }


/* ══════════════════════════════════════════
   8b. PARTICLE CONTROL BUTTON + POPOVER
   ══════════════════════════════════════════ */

/* ── Trigger button (matches .theme-toggle style) ─────────────────────── */
.particle-btn {
  width: var(--space-32); height: var(--space-32); border-radius: var(--space-8); cursor: pointer;
  border: 1px solid var(--border-brand);
  background: var(--overlay-brand);
  display: flex; align-items: center; justify-content: center;
  transition: background 0.3s, border-color 0.3s, transform 0.3s, color 0.2s;
  flex-shrink: 0;
  color: var(--text-secondary);
}
.particle-btn:hover {
  background: color-mix(in oklch, var(--accent-primary) 18%, transparent);
  border-color: color-mix(in oklch, var(--accent-primary) 40%, transparent);
  transform: scale(1.05); color: var(--text-primary);
}
.particle-btn.is-open {
  background: color-mix(in oklch, var(--accent-primary) 22%, transparent);
  border-color: color-mix(in oklch, var(--accent-primary) 55%, transparent);
  color: oklch(72% 0.22 264);
}
.particle-btn svg { width: var(--space-16); height: var(--space-16); stroke: currentColor; }

/* ══════════════════════════════════════════
   UTILITY MENU (top-right settings icon)
   ══════════════════════════════════════════ */
.nav-utility {
  position: relative; margin-left: 16px; flex-shrink: 0;
}
.utility-btn {
  width: var(--space-40); height: var(--space-40); border-radius: var(--space-8); cursor: pointer;
  border: 1px solid var(--border-subtle);
  background: transparent;
  display: flex; align-items: center; justify-content: center;
  transition: all 0.25s; color: var(--text-secondary);
}
.utility-btn:hover {
  border-color: color-mix(in oklch, var(--accent-primary) 40%, transparent);
  background: var(--overlay-brand); color: var(--text-primary);
  transform: scale(1.05);
}
.utility-btn.is-open {
  border-color: color-mix(in oklch, var(--accent-primary) 50%, transparent);
  background: color-mix(in oklch, var(--accent-primary) 15%, transparent); color: var(--text-primary);
}
.utility-btn svg { width: var(--space-16); height: var(--space-16); stroke: currentColor; }

.utility-panel {
  position: absolute; top: calc(100% + var(--space-12)); right: 0;
  width: 208px;
  background: oklch(9% 0.014 264 / 0.97);
  border: 1px solid oklch(100% 0 0 / 0.07);
  border-radius: var(--space-12);
  padding: var(--space-12);
  box-shadow: var(--elev-3);
  backdrop-filter: blur(24px);
  -webkit-backdrop-filter: blur(24px);
  z-index: 9998;
  opacity: 0; transform: translateY(-6px) scale(0.97);
  pointer-events: none;
  transition: opacity 0.18s ease, transform 0.18s ease;
}
.utility-panel.is-visible {
  opacity: 1; transform: translateY(0) scale(1); pointer-events: all;
}
.utility-panel-title {
  font-size: var(--text-caption); font-weight: var(--fw-semibold); letter-spacing: var(--tracking-loose);
  text-transform: uppercase; color: var(--text-faint);
  margin: 0 0 var(--space-8) 0;
}
.utility-row {
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--space-8) var(--space-12); border-radius: var(--space-8);
  transition: background 0.2s;
}
.utility-row--link { cursor: pointer; }
.utility-row--link:hover { background: var(--overlay-hover); }
.utility-label {
  font-size: var(--text-sm); font-weight: var(--fw-medium); color: var(--text-secondary);
}
/* divider between rows */
.utility-row + .utility-row {
  border-top: 1px solid var(--border-subtle);
  margin-top: var(--space-4); padding-top: var(--space-12);
}

/* ── Popover panel ──────────────────────────────────────────────────────── */
.particle-popover {
  position: fixed;
  top: 88px; right: 16px;
  width: 272px;
  background: oklch(9% 0.014 264 / 0.97);
  border: none;
  border-radius: var(--space-16);
  padding: var(--space-20) var(--space-20) var(--space-16);
  box-shadow: var(--elev-3);
  backdrop-filter: blur(24px);
  -webkit-backdrop-filter: blur(24px);
  z-index: 9999;
  /* hidden state */
  opacity: 0;
  transform: translateY(-6px) scale(0.975);
  pointer-events: none;
  transition: opacity 0.18s ease, transform 0.18s ease;
}
.particle-popover.is-visible {
  opacity: 1;
  transform: translateY(0) scale(1);
  pointer-events: all;
}

/* ══════════════════════════════════════════════════════════════════════
   FAB sub-panel — clip the scroll, host the scroll in the body
   ──────────────────────────────────────────────────────────────────────
   Apr 2026: the sub-panel's head used to be sticky over the panel's own
   scroll context with a transparent fill, relying on the panel's
   translucent surface for legibility. That doesn't hold in light mode —
   scrolled theme-card thumbnails (large coloured "Aa" + colour bars)
   bleed right through the transparent head and overlap the title.
   Fix: make the panel a flex column with overflow: hidden, the head
   stays in normal flow at the top, and the controls slot becomes the
   new scroll container. Content can no longer reach the head's region.

   May 2026 v18: the header primitive was promoted from .panel-head to
   .screen-header.screen-header--compact (single canonical primitive
   per the Sizing Ladders rule). The override below now scopes to
   .fab-sub-panel .screen-header--sticky-glass; everything else stays.
   See the .screen-header--compact rule in the .screen-header section
   below for the chrome dimensions + size modifier contract.
   ══════════════════════════════════════════════════════════════════════ */
.ds-doc-menu-panel.fab-sub-panel {
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.fab-sub-panel .screen-header--sticky-glass {
  /* No longer sticky — it's outside the scroll context now.
     margin-bottom dropped to 0 so the slot starts FLUSH with the head's
     border-bottom line — no white shelf below the line. The slot's own
     padding-top (below) carries the breathing room at scroll 0, and
     because that padding is INSIDE the scroll container, content passes
     through it on scroll and visually meets the line. */
  position: relative;
  top: 0;
  flex: 0 0 auto;
  margin-bottom: 0;
}
.fab-sub-panel .fab-controls-slot {
  /* New scroll container. Bleed to the panel's padding edges so the
     scrollbar sits in the gutter rather than insetting cards. The
     20px sides + 16px bottom restore the inner breathing room the
     panel's own padding used to provide; padding-top is the gap that
     used to live on the head as margin-bottom — moved here so it
     scrolls away cleanly instead of sitting as a static white shelf
     below the head's line. */
  flex: 1 1 auto;
  overflow-y: auto;
  margin: 0 calc(-1 * var(--space-20)) calc(-1 * var(--space-16));
  padding: var(--space-12) var(--space-20) var(--space-16);
}

/* Light theme adjustments */
/* ── Light theme: utility menu + buttons ────────────────────────────────── */
[data-theme="light"] .utility-btn {
  background: oklch(97% 0.008 264 / 0.9);
  border-color: oklch(0% 0 0 / 0.1);
  color: var(--text-secondary);
}
[data-theme="light"] .utility-btn:hover {
  background: color-mix(in oklch, var(--accent-primary) 10%, transparent);
  border-color: color-mix(in oklch, var(--accent-primary) 30%, transparent);
  color: var(--text-primary);
}
[data-theme="light"] .utility-btn.is-open {
  background: color-mix(in oklch, var(--accent-primary) 10%, transparent);
  border-color: color-mix(in oklch, var(--accent-primary) 35%, transparent);
  color: var(--text-primary);
}
[data-theme="light"] .utility-panel {
  background: oklch(98% 0.003 264 / 0.97);
  border-color: oklch(0% 0 0 / 0.08);
  /* shadow handled by var(--elev-3) on the base rule — token mode-flips */
}
[data-theme="light"] .particle-btn {
  background: color-mix(in oklch, var(--accent-primary) 6%, transparent);
  border-color: oklch(0% 0 0 / 0.1);
  color: var(--text-secondary);
}
[data-theme="light"] .particle-btn:hover {
  background: color-mix(in oklch, var(--accent-primary) 12%, transparent);
  border-color: color-mix(in oklch, var(--accent-primary) 30%, transparent);
  color: var(--text-primary);
}
[data-theme="light"] .particle-btn.is-open {
  background: color-mix(in oklch, var(--accent-primary) 14%, transparent);
  border-color: color-mix(in oklch, var(--accent-primary) 40%, transparent);
  color: oklch(42% 0.25 264);
}
[data-theme="light"] .utility-row--link:hover {
  background: oklch(0% 0 0 / 0.04);
}
[data-theme="light"] .utility-row + .utility-row {
  border-color: oklch(0% 0 0 / 0.06);
}

[data-theme="light"] .particle-popover {
  background: oklch(98% 0.003 264 / 0.97);
  /* shadow handled by var(--elev-3) on the base rule — token mode-flips */
}
/* Dead .pp-* light overrides removed — .btn-icon + .ds-* handle both themes */

/* ── Globe popover — mirrors particle-popover ──────────────────────────── */
.globe-popover {
  position: fixed;
  top: 88px; right: 16px;
  width: 272px;
  max-height: calc(100vh - 100px);
  overflow-y: auto;
  background: oklch(9% 0.014 264 / 0.97);
  border: none;
  border-radius: var(--space-16);
  padding: var(--space-20) var(--space-20) var(--space-16);
  box-shadow: var(--elev-3);
  backdrop-filter: blur(24px);
  -webkit-backdrop-filter: blur(24px);
  z-index: 9999;
  /* hidden state */
  opacity: 0;
  transform: translateY(-6px) scale(0.975);
  pointer-events: none;
  transition: opacity 0.18s ease, transform 0.18s ease;
}
.globe-popover.is-visible {
  opacity: 1;
  transform: translateY(0) scale(1);
  pointer-events: all;
}
[data-theme="light"] .globe-popover {
  background: oklch(98% 0.003 264 / 0.97);
  /* shadow handled by var(--elev-3) on the base rule — token mode-flips */
}


/* ══════════════════════════════════════════════════════════════════════
   .fab — Floating Action Button
   ───────────────────────────────────────────────────────────────────
   A fixed-position circular icon button, anchored to the bottom-right
   corner. Use to expose "page-level tools" — a popover menu, a
   fullscreen search, an assistant — without eating nav real estate.
   Skip the FAB on surfaces that already have a top-nav entry for the
   same function (the DS pages do this — `ds-nav.js` owns their nav).

   Composition model
   ─────────────────
   `.fab` is a POSITIONING + shadow layer on top of the existing
   button primitives — it does not redefine size, radius, or colours.
   The full markup:
     <button class="btn-primary btn-neutral btn-icon btn-lg
                    btn-circle fab" id="..." aria-expanded="false"
             aria-haspopup="true">
       <svg><!-- icon --></svg>
     </button>

   States
   ──────
   - default          — subtle drop shadow, 16px glass blur behind it
   - :hover           — 1.08 scale + stronger shadow + 4px white halo
   - .is-open         — icon rotates 45° (signals "open state"), slightly
                        reduced scale (feels "pressed"), accent halo
                        around the button

   Shadow + z-index
   ────────────────
   Sits at z-index 9997; its paired popover (`.ds-doc-menu-panel`) sits at
   9998 so it stacks on top. The button carries its own drop shadow in
   lieu of a border — borderless + depth is the LI.FI FAB signature.

   When NOT to use
   ───────────────
   - A page already has a clear nav-level entry for the same feature
     (don't duplicate — use the nav)
   - You have more than one competing floating control (don't make a
     constellation of FABs — pick one primary)
   - The action is destructive (FAB affordance suggests exploration,
     not commitment — use an inline button with explicit label)
   ══════════════════════════════════════════════════════════════════════ */
.fab {
  position: fixed; bottom: 24px; right: 24px; z-index: 9997;
  backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
  box-shadow: 0 4px 20px oklch(0% 0 0 / 0.25);
}
/* Apr 2026 — icon size dropped from 24px (inherited from .btn-lg) to
   18px so the glyph sits balanced inside the 54px circle (~33% of
   diameter, the FAB convention for circular icon buttons). The denser
   sliders-horizontal glyph (9 strokes) was reading overstuffed at the
   default size. The compound selector wins specificity over the
   `.btn-icon.btn-lg { --btn-icon-size: 24px }` rule earlier in
   the file (both 0,2,0; source order decides — this rule is later). */
.fab.btn-icon { --btn-icon-size: 18px; }
.fab:hover {
  transform: scale(1.08);
  box-shadow: 0 6px 28px oklch(0% 0 0 / 0.35), 0 0 0 4px oklch(100% 0 0 / 0.05);
}
.fab.is-open {
  box-shadow: 0 4px 24px oklch(0% 0 0 / 0.3), 0 0 0 4px oklch(100% 0 0 / 0.06);
}

/* ── FAB icon swap (idle ↔ close) ─────────────────────────────────────────
   Two SVGs stacked at the FAB's optical center. The idle icon (sliders-
   horizontal) is visible by default; on `.is-open`, it rotates out
   counter-clockwise + fades + scales down while the close icon (X)
   spins in clockwise from a small offset to full size.

   Curve: cubic-bezier(0.34, 1.56, 0.64, 1) — overshoots slightly on
   the way in, giving the swap a subtle pop without being silly. The
   opacity transition runs on its own ease so the fade resolves before
   the scale settles, masking the brief moment when both icons overlap
   mid-rotation. */
.fab .fab__icon {
  position: absolute;
  top: 50%; left: 50%;
  width: var(--btn-icon-size);
  height: var(--btn-icon-size);
  transition:
    opacity 0.18s ease,
    transform 0.32s cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* Closed (default): idle visible, close hidden + pre-rotated */
.fab .fab__icon--idle  {
  opacity: 1;
  transform: translate(-50%, -50%) rotate(0deg) scale(1);
}
.fab .fab__icon--close {
  opacity: 0;
  transform: translate(-50%, -50%) rotate(-90deg) scale(0.6);
}
/* Open: idle exits CCW, close enters CW */
.fab.is-open .fab__icon--idle {
  opacity: 0;
  transform: translate(-50%, -50%) rotate(90deg) scale(0.6);
}
.fab.is-open .fab__icon--close {
  opacity: 1;
  transform: translate(-50%, -50%) rotate(0deg) scale(1);
}

/* ══════════════════════════════════════════════════════════════════════
   .ds-doc-menu-panel — Popover surface for a .menu
   ───────────────────────────────────────────────────────────────────
   Frosted-glass floating panel, used as the container for a `.menu`
   (and, downstream, any adopted sub-panels like Theme Composer, Globe,
   Logo Stack, Reactive Coins). Sits above the FAB button (z-index 9998)
   and anchors to the same corner.

   Shared by every FAB sub-panel because the surface treatment — glass
   fill + blur + borderless depth shadow — is the same primitive in all
   cases; only the contents (menu vs. controls) change.

     1. Fill = var(--glass-base) at 70% alpha — theme-aware. Dark theme →
        dark glass, light theme → light glass. One rule, both themes.
     2. backdrop-filter: blur(40px) saturate(140%) — the actual frost.
        Saturate held at 140% so bright backgrounds don't amplify.
     3. Two-layer neutral drop shadow (ambient + ground contact) for
        depth. No accent-tinted halo; the glass fill carries the palette
        connection on its own.
     4. Inset 1px top highlight fakes a specular edge so the panel reads
        as raised, not flat.
   Intentionally borderless — the depth shadow + inset highlight carry
   the edge. */
.ds-doc-menu-panel {
  position: fixed; bottom: 88px; right: 24px; z-index: 9998;
  width: var(--w-3); max-height: 75vh; overflow-y: auto;
  /* Glass material — wired via :where() default above. The component
     reads from the resolved --mat-* API; the active material defaults
     to glass for this class. Override per-instance with data-material. */
  background: var(--mat-bg);
  box-shadow: var(--mat-shadow), var(--mat-highlight);
  backdrop-filter: var(--mat-backdrop);
  -webkit-backdrop-filter: var(--mat-backdrop);
  border: var(--mat-border);
  border-radius: var(--card-radius);
  padding: var(--space-20) var(--space-20) var(--space-16);
  opacity: 0; transform: translateY(10px) scale(0.97);
  pointer-events: none;
  transition: opacity 0.2s ease, transform 0.2s ease;
}
.ds-doc-menu-panel.is-visible {
  opacity: 1; transform: translateY(0) scale(1); pointer-events: all;
}

/* ══════════════════════════════════════════════════════════════════════
   .menu — Vertical list of menu items
   ───────────────────────────────────────────────────────────────────
   A reusable menu primitive. Lives inside any container — the FAB
   popover (`.ds-doc-menu-panel`) uses it, but so can a mobile drawer,
   a dropdown, an action sheet, or a footer jump list. The primitive
   does not carry any surrounding surface styles (no background, no
   padding, no shadow) — compose it inside whatever container the
   context calls for.

   Anatomy
   ───────
     <div class="menu">
       <a href="…" class="menu-item">
         <span class="menu-item-icon">           <!-- 32×32 tile -->
           <svg><!-- Lucide glyph --></svg>          <!-- 16×16 glyph -->
         </span>
         <span class="menu-item-text">           <!-- text column -->
           <span class="menu-item-title">Title</span>
           <span class="menu-item-desc">Short description</span>
         </span>
       </a>
       <!-- more .menu-item rows -->
       <div class="menu-divider"><span>Section label</span></div>
       <!-- more items in the next section -->
     </div>

   States (on .menu-item)
   ─────────────────────────
   - :hover           — subtle surface tint
   - .is-current      — accent-primary tint (marks the current page /
                        active selection)

   Icon tile
   ─────────
   32×32 rounded square at `color: var(--accent-primary)` — the icon
   renders in brand accent so the row reads as a navigable destination.
   The SVG slot defaults to a 16×16 square; the `--mark` modifier swaps
   that for height-constrained auto-width so the LI.FI mark (5:6 aspect)
   doesn't squash.

   Divider (`.menu-divider`)
   ────────────────────────────
   Labelled horizontal rule — mono-uppercase caption flanked by 1px
   rules. Use to separate semantic sections within a single menu
   (e.g. "ON THIS PAGE" in the FAB menu, "RECENT" / "PINNED" in a
   command palette). If you only have two sections and no label, a
   plain `<hr class="menu-divider">` renders as the rules alone.

   When NOT to use
   ───────────────
   - For radio-group / checkbox-group selection → use Segmented Control
     or Checkbox List instead; .menu items are treated as links
   - For a single destination inside a button row → use a plain button;
     an entire .menu with one item is visual overkill
   - For navigation that should always be visible → use the sidebar or
     nav dropdown; menus are intended for pop-up surfaces
   ══════════════════════════════════════════════════════════════════════ */
/* list-style: none — .menu is consumed two ways: bare <a> children (FAB,
   palette) and semantic <ul><li><a> (role="menu" consumers; catalog demos).
   An <li> stays display:list-item even as a flex child (flexbox preserves
   list-item under blockification), so its UA marker renders in the panel's
   left gutter unless suppressed here. The line-44 universal reset zeroes
   margin / padding but NOT list-style — this is the one bit it misses. */
.menu { display: flex; flex-direction: column; gap: var(--space-4); list-style: none; }
.menu-item {
  display: flex; align-items: center; gap: var(--cluster-sm);
  width: 100%;
  padding: var(--space-8) var(--space-12); border-radius: var(--space-12);
  text-decoration: none; color: var(--text-primary);
  /* Button-host reset — .menu-item renders on BOTH <a> and <button> hosts.
     Navigation menus (FAB, nav dropdowns) use <a>; text-only action / option
     menus (orders kebab, theme-actions kebab, limit-expiry dropdown) use
     <button>. There is no global <button> reset, so a <button class="menu-item">
     leaks UA chrome — a 2px outset border, buttonface background, the Arial
     UA font, and content-width sizing — none of which an <a> carries. The
     declarations below neutralize all of it so a <button> row renders
     identically to an <a> row, full-width inside its panel. `border: 0` is
     explicit, not omitted: the UA outset border leaks otherwise (same trap
     as `.setting-row--drill` and the button-rooted-card rule in CLAUDE.md).
     `font: inherit` restores Figtree (the <button> UA resets font-family). */
  appearance: none; -webkit-appearance: none;
  border: 0; background: transparent;
  font: inherit; text-align: left; cursor: pointer;
  transition: background 0.15s;
}
.menu-item:hover { background: oklch(100% 0 0 / 0.06); }
.menu-item.is-current { background: color-mix(in oklch, var(--accent-primary) 12%, transparent); }
/* Destructive tone — the danger-toned variant of .menu-item for destructive
   actions (Cancel order, Disconnect wallet, Delete workspace). Pairs visually
   with .btn-destructive (modal confirmations) and .chip-danger (status
   displays) so a destructive flow speaks one tone across menu → confirm →
   status. The TEXT carries the tone; the row's rest / hover / .is-current
   background stays neutral (inherited from the base rules above) — so the row
   reads as a "danger ACTION", not a "danger ROW". Same disambiguation
   .btn-destructive uses: the text carries the tone, the surface stays subtle.

   The `.menu-item--danger .menu-item-title` selector is REQUIRED, not
   redundant: .menu-item-title sets its own explicit color: var(--text-primary)
   (a directly-matching (0,1,0) rule) which beats the colour inherited from
   .menu-item--danger on the row. So the modifier has to recolour the title
   element itself at (0,2,0). --danger is theme-aware (the semantic offsets
   flip in light mode), so no per-theme override is needed. Don't author
   --success / --warn / --info siblings until a real consumer needs one —
   destructive menu actions are real; the others are hypothetical. */
.menu-item--danger,
.menu-item--danger .menu-item-title { color: var(--danger); }
/* Active-row check — right-aligned glyph that reads as "this is the
   selected option." Sources the canonical Lucide check from the icon
   library at /icons/check.svg (not an inline data URI) — the library
   is the source of truth, and any future tweak there auto-propagates
   to every menu / dropdown / cmd-palette consumer. Mask-image picks
   up var(--accent-primary) via background-color so the check auto-
   flips light/dark with the brand palette. `margin-left: auto`
   flushes it to the row's right edge regardless of item content
   (icon + label, label only, label + description). */
.menu-item.is-current::after {
  content: '';
  width: var(--space-16);
  height: var(--space-16);
  margin-left: auto;
  flex-shrink: 0;
  background-color: var(--accent-primary);
  -webkit-mask-image: url("/icons/check.svg");
          mask-image: url("/icons/check.svg");
  -webkit-mask-size: contain;
          mask-size: contain;
  -webkit-mask-position: center;
          mask-position: center;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
}
/* ── Size variant — .menu--sm (compact tier) ─────────────────────────
   The smaller tier of the menu component, for DENSE / NAVIGATION-overflow
   surfaces where the default 16px bare-text row reads too large beside the
   chrome around it. Built for the DS top-nav "More" priority+ overflow
   (retired June 2026 — More now reuses the standard .nav-dropdown mega panel,
   one expansion paradigm; ds-nav.js); the tier remains for any compact popover
   menu whose rows must read at the .nav-link-item voice (--text-sm /
   --fw-medium). The tier is about the TEXT voice, not a cramped box: rows keep
   a comfortable 8×16 pad (roomier than the base 8×12 horizontally) so the
   smaller text still reads spacious, with a radius step down (8px).

   Container-level modifier (on .menu), NOT per-row (.menu-item--sm) — a menu
   is one cohesive tier; you never mix sm + default rows in a single menu. Same
   shape as .seg--borderless cascading to .seg-item. It only re-sizes the BARE-
   text row: title/desc rows keep their own explicit sizes (.menu-item-title is
   already --text-sm, .menu-item-desc --text-caption — already compact), so the
   variant composes cleanly with structured rows too. */
.menu--sm .menu-item {
  font-size: var(--text-sm);
  font-weight: var(--fw-medium);
  padding: var(--space-8) var(--space-16);
  border-radius: var(--space-8);
}
/* ──────────────────────────────────────────────────────────────────
   SPOT ICON — universal cross-surface icon-tile primitive
   ──────────────────────────────────────────────────────────────────
   A small rounded tile with an SVG icon centered inside. The icon
   renders in the brand accent against a subtle alpha-tinted surface,
   so the tile reads as a "navigable destination" — the visual that
   makes a menu item feel clickable, an action card actionable, a
   nav row interactive.

   Used in: menu items, action cards, nav rows, settings rows, any
   composition that pairs a focal icon with text.

   Tier: universal (no prefix). Composes anywhere.

   Size ladder (7 tiers — +8 step on tile through 3xl, +16 step from
   4xl onward; icon +4 then +8; radius +4 throughout):
     .spot-icon        32×32 tile · 16×16 icon · --space-8 radius (default)
     .spot-icon-lg     40×40 tile · 20×20 icon · --space-12 radius
     .spot-icon-xl     48×48 tile · 24×24 icon · --space-16 radius
     .spot-icon-2xl    56×56 tile · 28×28 icon · --space-20 radius
     .spot-icon-3xl    64×64 tile · 32×32 icon · --space-24 radius
     .spot-icon-4xl    80×80 tile · 40×40 icon · --space-28 radius
     .spot-icon-5xl    96×96 tile · 48×48 icon · --space-32 radius

   The 4xl + 5xl tiers carry empty-state / hero-canvas / poster-scale
   marks where the icon needs to anchor a full panel or section, not
   just a row. Reach for 5xl when 4xl still reads small against the
   panel's vertical extent (e.g. the Limit-mode Market price empty
   state in a 600+ px receive panel).

   The .menu-item-icon class is grouped here as a backward-compat
   alias — same default-size styling, no size modifier support. New
   consumers should use .spot-icon directly. */
.spot-icon, .menu-item-icon {
  width: var(--space-32); height: var(--space-32); border-radius: var(--space-8);
  background: oklch(100% 0 0 / 0.05);
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0; color: var(--text-primary);
}
.spot-icon svg, .menu-item-icon svg { width: var(--space-16); height: var(--space-16); }

/* Size variants — only on .spot-icon, not on .menu-item-icon */
.spot-icon-lg { width: var(--space-40); height: var(--space-40); border-radius: var(--space-12); }
.spot-icon-lg svg { width: var(--space-20); height: var(--space-20); }
.spot-icon-xl { width: var(--space-48); height: var(--space-48); border-radius: var(--space-16); }
.spot-icon-xl svg { width: var(--space-24); height: var(--space-24); }
.spot-icon-2xl { width: var(--space-56); height: var(--space-56); border-radius: var(--space-20); }
.spot-icon-2xl svg { width: var(--space-28); height: var(--space-28); }
.spot-icon-3xl { width: var(--space-64); height: var(--space-64); border-radius: var(--space-24); }
.spot-icon-3xl svg { width: var(--space-32); height: var(--space-32); }
.spot-icon-4xl { width: var(--space-80); height: var(--space-80); border-radius: var(--space-28); }
.spot-icon-4xl svg { width: var(--space-40); height: var(--space-40); }
.spot-icon-5xl { width: var(--space-96); height: var(--space-96); border-radius: var(--space-32); }
.spot-icon-5xl svg { width: var(--space-48); height: var(--space-48); }

/* ── Semantic tone variants ──────────────────────────────────────
   Opt-in modifiers that drive BOTH the icon color and the bg-tint
   from a single semantic token (--si-tone). Default spot-icon stays
   on the existing 5% white bg + hardcoded brand-blue icon for back-
   compat with consumers that haven't migrated. Tone variants
   override both background and color in one rule.

   Use:
     .spot-icon--success    --success (route confirmed, paid)
     .spot-icon--info       --info    (system info, route available)
     .spot-icon--warn       --warn    (high slippage, pending sig)
     .spot-icon--danger     --danger  (rejected, failed, insufficient)

   Each variant routes its tone through the matching semantic token —
   palette-independent. (Before May 2026 info routed through
   --accent-primary, which made info-variant icons silently shift
   with the active brand palette instead of staying info-blue.)

   Compose with size: <div class="spot-icon spot-icon--success">,
   <div class="spot-icon spot-icon-lg spot-icon--warn">, etc. */
.spot-icon--success,
.spot-icon--info,
.spot-icon--warn,
.spot-icon--danger {
  background: color-mix(in oklch, var(--si-tone) 14%, transparent);
  color: var(--si-tone);
}
.spot-icon--success { --si-tone: var(--success); }
.spot-icon--info    { --si-tone: var(--info); }
.spot-icon--warn    { --si-tone: var(--warn); }
.spot-icon--danger  { --si-tone: var(--danger); }

/* Neutral tone — a quiet grey plate with a primary-ink glyph, for
   non-semantic icons where the default brand-blue spot-icon doesn't fit
   (a list-lead, a funding-method row). Not --si-tone driven: neutral has
   no hue to carry a 14% tint, so it gets a softer 8% primary-ink plate +
   a full primary-ink glyph. Promoted June 2026 from a Checkout-scoped
   override. */
.spot-icon--neutral {
  background: color-mix(in oklch, var(--text-primary) 8%, transparent);
  color: var(--text-primary);
}

/* ── Shape modifier — fully circular ────────────────────────────
   Default .spot-icon stays on the --space-* radius ladder (8 / 12 /
   16 / 20 / 24 / 28 px) for the rounded-tile look. .spot-icon--round
   flips to a perfect circle for confirmation stamps, profile-style
   marks, and hero status marks where the circular shape reads as
   state rather than chrome. For sub-32 px state markers (presence
   dot, unread dot, step bullet), use .status-dot — that's the
   dedicated below-32-px primitive. Composes with any size + tone:
   <span class="spot-icon spot-icon-lg spot-icon--success spot-icon--round">.

   Source order matters: the size variants set border-radius from
   --space-*, this rule comes after to win at parity (specificity 10).
   Don't mix square and round spot-icons in the same row — pick one
   shape per surface so the eye reads them as one family. */
.spot-icon--round { border-radius: 999px; }

/* ── Step-state vocabulary — for stepped / checklist consumers ────
   data-step-state on a .spot-icon drives its appearance across
   complete / current / upcoming states. The same attribute the
   .steps consumer uses, so a step disc declared as
   .spot-icon spot-icon--round + data-step-state="…" gets:

     complete  → light --si-tone fill + accent check glyph (filled, no ring)
     current   → light --si-tone fill + accent number glyph (filled, no ring)
     upcoming  → muted neutral fill + darker-grey number (filled, no ring)

   Default tone = --accent-primary. Override per-instance with
   the existing .spot-icon--success / --info / --warn / --danger
   tone variants — those drive --si-tone, and these step-state
   rules consume it.

   Consumers beyond .steps: onboarding checklists, settings
   completeness rails, activity feed timelines, any "stepped
   progression" UI. The contract is single-attribute — one
   declaration carries the whole visual state. */
.spot-icon[data-step-state] { --si-tone: var(--accent-primary); }
/* Filled discs, no outlines (June 2026). COMPLETE maps to the PRIMARY button
   (.btn-primary): a SOLID accent fill + an on-accent (auto-contrast) glyph, so
   finished steps stand out as solid progress markers. CURRENT maps to the
   SECONDARY button (.btn-secondary): a 10% accent-tint fill + a 60%-accent/white
   glyph — present but quieter than complete. Both drive off --si-tone so tone
   overrides + theming apply. Upcoming takes a muted neutral fill + a darker-grey
   number so it reads inactive. */
.spot-icon[data-step-state="complete"] {
  background: var(--si-tone);
  color: var(--on-accent-primary);
}
.spot-icon[data-step-state="current"] {
  background: color-mix(in oklch, var(--si-tone) 10%, transparent);
  color: color-mix(in oklch, var(--si-tone) 60%, #fff);
}
.spot-icon[data-step-state="upcoming"] {
  background: color-mix(in oklch, var(--text-primary) 8%, transparent);
  color: var(--text-secondary);
}

/* LI.FI mark variant — the mark's native 5:6 aspect (40×48 viewBox)
   doesn't fit the 16×16 square the other Lucide glyphs occupy, so the
   default rule would squash it. Constrain by height and let width
   auto-compute — renders at ~13×16 inside the tile, which reads at the
   same visual weight as the square icons without distortion. The
   .menu-item-icon--mark form is a backward-compat alias; new consumers
   compose .spot-icon with .spot-icon--mark. */
.spot-icon--mark svg,
.menu-item-icon--mark svg { width: auto; height: var(--space-16); }

/* ──────────────────────────────────────────────────────────────────
   STATE BLOCK — .state-block  (+ .empty-state sibling)
   Universal centered focal-mark stack: a focal mark (avatar / spot-icon /
   spinner) over an optional title, a message, and an optional action /
   trailing element — all centered, with generous top + bottom breathing
   so the composition never crowds the content above or below it. The
   shape communicates a STATE or a PROMPT: a connect-wallet prompt, a
   "Waiting for {wallet}" status, an onboarding nudge, an error, a
   success confirmation.

     [focal mark]            — .avatar / .spot-icon / .spinner (first child)
     .state-block__title     — short heading (optional)
     .state-block__message   — the supporting line (the common slot)
     .state-block__action    — a CTA button (optional)

   Omit the slots you don't need — mark + message is the minimal shape;
   mark + title + message + action is the fullest. Gaps cascade
   (mark→text 16, title↔message 8, text→action 20) so an omitted slot
   never leaves an orphaned gap. The block padding (40) is the built-in
   breathing room; it stacks on top of any container inset (e.g. the
   .panel padding inside a modal) so the stack always has air.

   .empty-state is a SIBLING that shares this exact recipe (grouped
   selectors below) and adds two deltas of its own:
     1. it FILLS its region (flex: 1 1 auto) so it vertically centers
        inside a sized panel / card / canvas (the Limit-mode
        .ui-price-chart "Select a token" placeholder is the reference);
     2. it MUTES the focal spot-icon — "nothing here", not an alert.
   .state-block is natural-height by default (no flex grow) — right for a
   stack that sits ABOVE other content, e.g. the wallet-connect wizard's
   ecosystem intro that sits over the ecosystem list inside a flex
   .modal-body. The wallet-connect wizard (ecosystem intro + "Waiting
   for…" connecting screen) is the canonical .state-block consumer.

   Tier: universal (no prefix) — product, marketing, dashboards, modals. */
.state-block,
.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 100%;
  box-sizing: border-box;
  gap: var(--space-8);                          /* title ↔ message (the tight pair) */
  padding: var(--space-40) var(--space-32);     /* generous top/bottom breathing */
  text-align: center;
}
/* focal mark → text: 8 (base gap) + 8 (margin) = 16 */
.state-block > .avatar,
.state-block > .spot-icon,
.state-block > .spinner,
.empty-state > .spot-icon { margin-bottom: var(--space-8); }
/* A trailing spinner / mark (status indicator AFTER the message — the
   "Waiting for…" connecting screen) sits in the action position: 20px
   below the message, no extra space beneath. */
.state-block > .spinner:last-child:not(:first-child) {
  margin-top: var(--space-12);
  margin-bottom: 0;
}
.state-block__title,
.empty-state__title {
  margin: 0;
  max-width: var(--w-4);
  font-size: var(--text-body-lg);
  font-weight: var(--fw-semibold);
  line-height: var(--lh-body-lg);
  letter-spacing: var(--tracking-tight);
  color: var(--text-primary);
}
.state-block__message,
.empty-state__message {
  margin: 0;
  max-width: var(--w-4);
  font-size: var(--text-sm);
  font-weight: var(--fw-regular);
  line-height: var(--lh-sm);
  color: var(--text-muted);
}
.state-block__message a,
.empty-state__message a {
  color: var(--accent-primary-mid);
  text-decoration: underline;
}
.state-block__action,
.empty-state__action { margin-top: var(--space-12); }          /* text → action: 8 gap + 12 = 20 */

/* .empty-state deltas — fill the region + mute the focal icon. The muted
   icon uses :where() (zero specificity) so a consumer tone modifier
   (.spot-icon--danger on a failed-load empty state, etc.) still wins. */
.empty-state { flex: 1 1 auto; }
.empty-state :where(.spot-icon) {
  background: color-mix(in oklch, var(--text-primary) 6%, transparent);
  color: var(--text-muted);
}

/* ──────────────────────────────────────────────────────────────────
   MARKER — universal cross-surface sequential-identifier primitive
   ──────────────────────────────────────────────────────────────────
   A small rounded tile with a text identifier (1 / A / 01) centered
   inside. Sibling of .spot-icon — same DNA (size ladder, tone slot,
   round shape modifier), different content contract. .spot-icon hosts
   a decorative SVG; .marker hosts a SEQUENTIAL CHARACTER (number,
   letter, or zero-padded number) used as a row anchor or step bullet.

   Content forms:
     1 / 2 / 3     plain numbers — in-product step lists, short ≤9
                   sequences, dense rails
     A / B / C     letters — alternate sequences, sub-items below a
                   numbered parent
     01 / 02 / 03  zero-padded numbers — DEFAULT for marketing decks
                   and proposals; the leading 0 keeps "09 / 10 / 11"
                   and "19 / 20 / 21" column-aligned across the rail.
                   Reads as branded, intentional, ledger-like.

   The CSS recipe (tabular-nums + bold + tight tracking + 1.0 line-
   height) makes mixed-character lists align as one family. Content
   choice is the consumer's; the primitive is content-agnostic.

   Used in: variant-comparison rails (workshop cards), step rails in
   proposals, numbered features in decks, lettered bullets in long-
   form documents, ordered list anchors anywhere a "1 / 2 / 3" or
   "A / B / C" reads cleaner than an SVG glyph.

   Tier: universal (no prefix). Composes on product, marketing, decks,
   proposals, and the catalog itself.

   Pairs SIZE-FOR-SIZE with .spot-icon — drop a .marker-lg next to a
   .spot-icon-lg and they sit on the same baseline at the same tile
   size, so mixed rows (numbered step + status icon) line up.

   Size ladder (5 tiers, +8 step on tile, mirrors .spot-icon).
   Type sizes are tuned ONE TIER QUIETER than the obvious 50%
   ratio — the disc carries the visual weight, the character is
   the labeling content, not the headline. Generous whitespace
   inside the disc keeps the marker reading as a quiet anchor.

     .marker         32×32 tile · --text-caption (12) · --space-8 radius (default)
     .marker-lg      40×40 tile · --text-sm      (14) · --space-12 radius
     .marker-xl      48×48 tile · --text-body    (16) · --space-16 radius
     .marker-2xl     56×56 tile · --text-body-lg (18) · --space-20 radius
     .marker-3xl     64×64 tile · --text-h3      (24) · --space-24 radius

   Tone slot — same --*-tone pattern as .tile / .accent-card / .spinner.
   The default tone is --accent-primary; tone modifiers route through
   --marker-tone so per-variant :hover / .is-active overrides on
   consumer components win against [data-theme="light"] base rules
   without fighting specificity. (See CLAUDE.md → "Variant-aware
   states" for the why.)

   Default surface is brand-accent-tinted (NOT 5% white like a default
   .spot-icon). The character content is the recognition signal — a
   tinted disc telegraphs "this is the anchor" the way a spot-icon's
   tile telegraphs "this is the focal glyph". For a quiet anchor on
   a heavily-toned surface, use .marker--neutral. */
.marker {
  --marker-tone: var(--accent-primary);
  width: var(--space-32); height: var(--space-32);
  border-radius: var(--space-8);
  background: color-mix(in oklch, var(--marker-tone) 14%, transparent);
  color: var(--marker-tone);
  display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0;
  font-family: var(--font-sans);
  font-size: var(--text-caption);
  font-weight: var(--fw-bold);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--tracking-tight);
  line-height: 1;
  user-select: none;
}

/* Size variants — bump tile, radius, and font-size together */
.marker-lg  { width: var(--space-40); height: var(--space-40); border-radius: var(--space-12); font-size: var(--text-sm); }
.marker-xl  { width: var(--space-48); height: var(--space-48); border-radius: var(--space-16); font-size: var(--text-body); }
.marker-2xl { width: var(--space-56); height: var(--space-56); border-radius: var(--space-20); font-size: var(--text-body-lg); }
.marker-3xl { width: var(--space-64); height: var(--space-64); border-radius: var(--space-24); font-size: var(--text-h3); }

/* ── Semantic tone variants ──────────────────────────────────────
   Same vocabulary + recipe as .spot-icon. Each tone routes through
   --marker-tone so a per-variant :hover override on a consumer
   composes cleanly. Default --marker-tone = --accent-primary,
   pulling through whichever palette is active.

   Use:
     .marker--success    --success    (completed step, paid invoice)
     .marker--info       --info       (informational anchor, callout)
     .marker--warn       --warn       (review-required step)
     .marker--danger     --danger     (failed / blocking step)
     .marker--neutral    --text-secondary (quiet anchor — letters in
                                       a long-form proposal, lettered
                                       sub-items that shouldn't compete
                                       with brand-toned steps above) */
.marker--success { --marker-tone: var(--success); }
.marker--info    { --marker-tone: var(--info); }
.marker--warn    { --marker-tone: var(--warn); }
.marker--danger  { --marker-tone: var(--danger); }
.marker--neutral { --marker-tone: var(--text-secondary); }

/* ── Shape modifier — fully circular ────────────────────────────
   Defaults to a rounded square (matches the .spot-icon family). The
   round shape reads as "callout / anchor" rather than "container",
   useful for hero-tier numbered features in decks and proposals.
   Composes with any size + tone. */
.marker--round { border-radius: 999px; }

/* ── Fill modifier — solid tone, contrast text ───────────────────
   Defaults to tinted bg + tone-colored text. .marker--solid flips to
   a solid tone fill with white text — for high-emphasis hero anchors
   (the "1 / 2 / 3" in a marketing deck's three-step feature row, the
   "A / B / C" in a proposal's headline comparison). Combines with
   any size, tone, and shape modifier. */
.marker--solid {
  background: var(--marker-tone);
  color: oklch(100% 0 0);
}

/* ── .spinner ──────────────────────────────────────────────
   Loading indicator. Three types — circular ring (default),
   bouncing dots, scaling bars — share one wrapper API.
   Inherits color from the parent (currentColor) by default,
   or routes through --spinner-tone for brand tones.

   Sizes (sm/default/lg/xl ladder per the Component Sizing
   Ladders rule — pairs visually with .btn-*, .seg-*, .input--*):
     .spinner--sm        16 × 16 px   — inside buttons, table rows,
                                        inline with body text
     .spinner            20 × 20 px   — DEFAULT — toolbars, form rows,
                                        default loading states
     .spinner--lg        24 × 24 px   — empty states, route-quote
                                        cards, modal bodies
     .spinner--xl        32 × 32 px   — full-screen loading, page-
                                        level fetches; stroke bumps
                                        to 4 px for visual weight

   Tones (same --*-tone pattern as .tile / .accent-card —
   routes through --spinner-tone so future state styles never
   bake var(--accent-primary)):
     (default)              currentColor — inherits parent's color
     .spinner--primary      --accent-primary
     .spinner--secondary    --accent-secondary
     .spinner--tertiary     --accent-tertiary
     .spinner--alpha        theme-neutral ink overlay — matches
                            the .btn-neutral vocabulary used on
                            quiet, in-card affordances. Routes
                            BOTH the arc and the track via the
                            --spinner-track-color override hook
                            so the double-mix-to-invisible problem
                            the other tones don't have (their
                            18% track-of-tone reads fine) doesn't
                            wash out the alpha track.

   Types:
     (default)              circular ring — universal loading
                            gesture
     .spinner--progress     DETERMINATE ring — fills an arc from
                            12 o'clock clockwise based on
                            style="--spinner-progress: 0..1".
                            Suppresses the indeterminate spin
                            animation. The high-tier
                            .refresh-timer primitive composes
                            this with the indeterminate ring to
                            communicate "countdown → refreshing".
                            SVG-based anatomy — the .spinner__ring
                            child is an &lt;svg&gt; containing two
                            &lt;circle&gt;s (track + arc). The CSS
                            sets r = calc(50% - --spinner-stroke / 2)
                            so the stroke centerline lands exactly
                            where the CSS border centerline lands
                            on the default ring — pixel-perfect
                            visual parity. (An earlier conic-
                            gradient + radial-mask implementation
                            put the antialias band outside the
                            visible stroke, making the ring read
                            as slightly thicker than the default.)
     .spinner--dots         three bouncing dots — for inline text
                            and button labels where a ring feels
                            heavy
     .spinner--bars         three scaling bars — for "working /
                            processing" gestures, equalizer-like

   Label modifier:
     .spinner--with-label   marks intent on the wrapper; the
                            adjacent .spinner__label child is
                            styled quiet (--text-secondary, sm).
                            Composition only — the gap is on
                            the .spinner wrapper.

   Accessibility — every spinner consumer should set
   role="status" on the wrapper and either an aria-label or a
   visible .spinner__label. Purely decorative spinners (next to
   text that already says "Loading…") set aria-hidden="true". */
.spinner {
  --spinner-tone: currentColor;
  --spinner-size: var(--space-20);
  --spinner-stroke: var(--space-2);
  display: inline-flex;
  align-items: center;
  gap: var(--space-12);
  vertical-align: middle;
}

.spinner--sm { --spinner-size: var(--space-16); }
.spinner--lg { --spinner-size: var(--space-24); }
.spinner--xl { --spinner-size: var(--space-32); --spinner-stroke: var(--space-4); }

.spinner--primary   { --spinner-tone: var(--accent-primary); }
.spinner--secondary { --spinner-tone: var(--accent-secondary); }
.spinner--tertiary  { --spinner-tone: var(--accent-tertiary, var(--accent-primary-mid)); }

/* .spinner--alpha — theme-neutral ink overlay tone. The other
   tones derive the track from --spinner-tone via the 18%-of-tone
   color-mix in .spinner__ring below; that works when the tone is
   a saturated accent, but compounding it with an already-low-
   opacity alpha mix washes out to invisibility. Set the track
   explicitly via --spinner-track-color (the ring rule consumes
   it with a fallback so primary/secondary/tertiary stay on the
   tone-derived track). Arc 35% of ink, track 8% of ink — the
   same 8% the .btn-neutral rest state uses for its fill. */
.spinner--alpha {
  --spinner-tone:        color-mix(in oklch, var(--text-primary) 35%, transparent);
  --spinner-track-color: color-mix(in oklch, var(--text-primary) 8%,  transparent);
}

/* Default type — circular ring. Top-edge holds the tone, the
   rest of the ring uses an 18% alpha track (or
   --spinner-track-color if the tone explicitly sets one — see
   .spinner--alpha). Spins clockwise. */
.spinner__ring {
  width: var(--spinner-size);
  height: var(--spinner-size);
  border-radius: 50%;
  border: var(--spinner-stroke) solid var(--spinner-track-color, color-mix(in oklch, var(--spinner-tone) 18%, transparent));
  border-top-color: var(--spinner-tone);
  animation: spinner-spin 0.8s linear infinite;
  flex-shrink: 0;
}

@keyframes spinner-spin {
  to { transform: rotate(360deg); }
}

/* .spinner--dots — three bouncing dots, staggered 160 ms apart.
   Dot diameter and inter-dot gap scale proportionally with
   --spinner-size so the dots feel right across the ladder. */
.spinner--dots {
  gap: calc(var(--spinner-size) * 0.2);
}
.spinner--dots .spinner__dot {
  width: calc(var(--spinner-size) * 0.32);
  height: calc(var(--spinner-size) * 0.32);
  border-radius: 50%;
  background: var(--spinner-tone);
  animation: spinner-bounce 1.2s ease-in-out infinite both;
  flex-shrink: 0;
}
.spinner--dots .spinner__dot:nth-child(1) { animation-delay: -0.32s; }
.spinner--dots .spinner__dot:nth-child(2) { animation-delay: -0.16s; }

@keyframes spinner-bounce {
  0%, 80%, 100% { transform: scale(0.4); opacity: 0.4; }
  40%           { transform: scale(1);   opacity: 1; }
}

/* .spinner--bars — three scaling vertical bars, staggered
   200 ms apart. Bar width + gap scale with --spinner-size. */
.spinner--bars {
  gap: calc(var(--spinner-size) * 0.15);
  height: var(--spinner-size);
  align-items: stretch;
}
.spinner--bars .spinner__bar {
  width: calc(var(--spinner-size) * 0.18);
  height: 100%;
  border-radius: 999px;
  background: var(--spinner-tone);
  animation: spinner-bar 1s ease-in-out infinite;
  transform-origin: center;
  flex-shrink: 0;
}
.spinner--bars .spinner__bar:nth-child(1) { animation-delay: -0.4s; }
.spinner--bars .spinner__bar:nth-child(2) { animation-delay: -0.2s; }

@keyframes spinner-bar {
  0%, 40%, 100% { transform: scaleY(0.35); opacity: 0.5; }
  20%           { transform: scaleY(1);    opacity: 1; }
}

/* .spinner--progress — determinate ring. Arc fills from
   12 o'clock clockwise as `style="--spinner-progress: 0..1"`
   ticks up. Composes with every tone (default currentColor; the
   typical pairing is .spinner--alpha for the quiet "auto-refresh
   countdown" use case). Implementation is SVG-based — the
   .spinner__ring child is an <svg> containing two <circle>s
   (track + arc). CSS sets cx/cy/r so the stroke centerline lands
   exactly where the default border centerline lands; visual
   weight matches the indeterminate ring pixel-for-pixel.

   The arc is rotated -90deg so progress=0 leaves the visible
   track empty and progress=1 wraps a full circle of tone.
   stroke-dasharray=100 paired with pathLength=100 on the arc
   <circle> normalizes the circumference to 100 user-space units
   so stroke-dashoffset directly consumes
   calc(100 - --spinner-progress * 100). (CSS parses unitless
   `1` as `1px` which broke the pathLength="1" normalization in
   the first revision; the 100-based form is the safe one.)

   Track color routes through --spinner-track-color with the same
   18%-of-tone fallback the indeterminate ring uses, so
   primary/secondary/tertiary keep their existing track recipe
   while .spinner--alpha (which sets --spinner-track-color
   explicitly) wins via the cascade.

   The indeterminate spin animation is suppressed when this
   variant is in effect — the determinate arc IS the message;
   adding rotation would dilute it. Consumers driving progress
   live (e.g., the .refresh-timer primitive) update
   --spinner-progress via rAF; the SVG attribute transitions
   smoothly via CSS, but rAF cadence gives the visually
   continuous fill.

   Markup contract (consumers swap when toggling --progress
   on/off — controlled by refresh-timer.js for the canonical
   countdown→refreshing case):
     indeterminate → <span class="spinner__ring"></span>
     determinate   → <svg class="spinner__ring"><circle class="spinner__ring-track"/><circle class="spinner__ring-arc" pathLength="100"/></svg>

   Reduced-motion: the variant doesn't animate on its own; the
   rAF driver in .refresh-timer respects prefers-reduced-motion
   by stepping less frequently. */
.spinner--progress {
  --spinner-progress: 0;
}
.spinner--progress .spinner__ring {
  width: var(--spinner-size);
  height: var(--spinner-size);
  border: 0;
  background: transparent;
  border-radius: 0; /* SVG renders its own circle — element box-radius irrelevant */
  animation: none;
  display: block;
  flex-shrink: 0;
}
.spinner--progress .spinner__ring-track,
.spinner--progress .spinner__ring-arc {
  cx: 50%;
  cy: 50%;
  /* Stroke centerline at (50% - stroke/2) → outer edge at 50%, inner edge
     at (50% - stroke). Matches the default .spinner__ring border anatomy
     exactly (border-radius: 50% clips the border to a circle of the same
     dimensions). */
  r: calc(50% - var(--spinner-stroke) / 2);
  fill: none;
  stroke-width: var(--spinner-stroke);
}
.spinner--progress .spinner__ring-track {
  stroke: var(--spinner-track-color, color-mix(in oklch, var(--spinner-tone) 18%, transparent));
}
.spinner--progress .spinner__ring-arc {
  stroke: var(--spinner-tone);
  stroke-linecap: butt;
  /* pathLength="100" on the <circle> normalizes the circumference to 100
     user-space units, so stroke-dasharray:100 + stroke-dashoffset
     :(100 - progress * 100) renders a fill covering exactly `progress`
     portion of the circle from start. (CSS interprets unitless `1` as
     `1px`, which breaks the pathLength="1" normalization — using 100
     gives the browser values it accepts in user-space units.) The
     -90deg rotation moves the start point from 3 o'clock (SVG default)
     to 12 o'clock so the fill reads clockwise from the top. */
  stroke-dasharray: 100;
  stroke-dashoffset: calc(100 - var(--spinner-progress, 0) * 100);
  transform: rotate(-90deg);
  transform-origin: 50% 50%;
}

/* .spinner--with-label — paired text caption. The .spinner
   wrapper already carries the gap; this rule styles the label
   side of the pair. Single-line by contract — long copy
   belongs in a sibling .ds-doc-text, not in the label. */
.spinner__label {
  font: 500 var(--text-sm)/1 var(--font-sans);
  color: var(--text-secondary);
  white-space: nowrap;
}

/* .spinner--vertical — orientation modifier. Stacks the label
   below the indicator instead of beside it. Use for empty
   states, full-screen loaders, and onboarding moments where
   the spinner is the focus, not chrome alongside other content.
   Composes with --with-label (the label is what gets stacked);
   the wrapper still owns the gap via the shared --space-12.
   For larger empty-state headings, render a sibling <h3> ABOVE
   the spinner — don't grow the label. */
.spinner--vertical {
  flex-direction: column;
  text-align: center;
}

/* Reduced motion — slow the loops dramatically so the
   gesture still signals "active" without spinning at 0.8 s/
   turn. The spinner doesn't disappear; the motion softens. */
@media (prefers-reduced-motion: reduce) {
  .spinner__ring                  { animation-duration: 4s; }
  .spinner--dots .spinner__dot    { animation-duration: 3.5s; }
  .spinner--bars .spinner__bar    { animation-duration: 3s; }
}

/* Catalog easter-egg — the spinner-spin keyframe also drives
   a hover effect on the Components → Spinner sidebar link:
   hovering the link spins its loader-circle icon. Decorative
   only; gated behind prefers-reduced-motion: no-preference so
   motion-sensitive users don't see it. */
@media (prefers-reduced-motion: no-preference) {
  .side-nav__item[href="#spinner"]:hover .side-nav__icon {
    animation: spinner-spin 0.8s linear infinite;
  }
}

/* ── .skeleton ──────────────────────────────────────────────
   Universal loading-state primitive. Renders a token-tinted
   shape with a subtle shimmer sweep across it so the user
   sees "the UI is working, content is on its way." Composes
   into any slot — replace a text node with .skeleton.--text,
   an avatar with .skeleton.--circle, a chip with .skeleton.--pill,
   a card body with the bare .skeleton.

   Sibling of .spinner. Pick by what the loading communicates:
     - Whole NEW CONTENT arriving (list, card, page body) →
       .skeleton in the slots where the content will land.
       Preserves layout; no jump on data arrival.
     - Existing UI is "WORKING ON SOMETHING" (signature
       pending, refreshing, polling) → .spinner. Compact;
       doesn't claim slot space.

   Shapes (mutually exclusive):
     (default)              block — 12 px radius. Consumer
                            sets width/height via style="...".
                            For card-body or panel placeholders.
     .skeleton--text        text-line — 5 px radius, inline-
                            block, height tracks 1em of the
                            host's font-size. Consumer sets
                            width via style="width: Nch | N%".
                            For replacing text nodes.
     .skeleton--circle      circle — equal w/h via
                            --skeleton-size (defaults to
                            --avatar-size when an avatar slot
                            hosts it). Inline-block; sits in
                            text runs cleanly. For replacing
                            avatars / circular icons.
     .skeleton--pill        pill — full radius, inline-block,
                            chip-sized (24 px tall by default).
                            For replacing chips / tags.

   Text sizes (.skeleton--text only — pair with the host's
   typography tier):
     .skeleton--sm          12 px tall — caption tier
     (default)              1em — tracks host font-size
     .skeleton--lg          18 px tall — body-lg tier
     .skeleton--xl          24 px tall — h3 tier (amount slots)

   Custom dimensions (consumer-owned, inline-style):
     style="width: 8ch"                — relative to text content
     style="--skeleton-size: 40px"     — sets both w/h (circles)
     style="height: 120px"             — block height (card bodies)

   Theming — the bg + sweep both derive from --text-primary, so
   the skeleton auto-flips light/dark with the rest of the system.
   The sweep peaks at +6 % over the base tint — enough to read as
   motion in either theme without competing with real content.

   Accessibility — every skeleton element renders as chrome.
   Set aria-hidden="true" on the element itself; mark loading
   STATE on the container (data-state="loading" + aria-busy="true").
   The container's loading status is what screen readers should
   announce; the skeleton atoms are decoration of that state.

   Source-of-truth: this primitive (no opt-in animation modifier).
   .skeleton ALWAYS shimmers. Reduced-motion users get a static
   tinted block via prefers-reduced-motion: reduce (no animation,
   bg stays so the slot still reads as "content pending"). If a
   consumer ever needs an explicit static skeleton (rare —
   documentation thumbnails, print), add .skeleton--static as a
   targeted opt-out — but it's not the default.
*/
.skeleton {
  --skeleton-radius: var(--space-12);
  --skeleton-bg:     color-mix(in oklch, var(--text-primary) 8%,  transparent);
  --skeleton-sweep:  color-mix(in oklch, var(--text-primary) 14%, transparent);

  display: block;
  width: 100%;
  height: var(--skeleton-size, var(--space-16));
  position: relative;
  overflow: hidden;
  border-radius: var(--skeleton-radius);
  background: var(--skeleton-bg);
  pointer-events: none;
  /* Stacks the sweep pseudo above the bg without leaking out of
     the rounded corners — same reason cards isolate themselves
     when hosting overflow children. */
  isolation: isolate;
}

/* Sweep — a soft diagonal gradient that translates left → right.
   transform is GPU-accelerated; cheaper than animating
   background-position. 100deg (not 90) tilts the gradient
   slightly so wide text-line skeletons get a visible sweep along
   their entire length instead of a vertical bar racing past. */
.skeleton::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    100deg,
    transparent 0%,
    var(--skeleton-sweep) 50%,
    transparent 100%
  );
  transform: translateX(-100%);
  animation: skeleton-shimmer 1.6s ease-in-out infinite;
}

@keyframes skeleton-shimmer {
  to { transform: translateX(100%); }
}

/* Shape modifier — text-line. Inline-block so it sits in a text
   run; height tracks the host's font-size via 1em so a skeleton
   placed inside a 36 px heading reads at 36 px without per-
   consumer height overrides. Smaller radius than the block form
   so the line reads as type, not as a chip. */
.skeleton--text {
  --skeleton-radius: var(--space-5, 5px);
  display: inline-block;
  height: 1em;
  vertical-align: -0.15em;
}

/* Text-line size modifiers — explicit pixel heights that pair
   with caption / body-lg / h3 typography tiers. Use when the
   host's font-size isn't the right driver (e.g., a layout
   placeholder that needs to read at amount-display weight even
   inside a parent at body tier). */
.skeleton--text.skeleton--sm { height: var(--text-caption); }     /* 12 px */
.skeleton--text.skeleton--lg { height: var(--text-body-lg); }     /* 18 px */
.skeleton--text.skeleton--xl { height: var(--text-h3); }          /* 24 px */

/* Shape modifier — circle. Width-height pinned to --skeleton-size
   so consumers size with one variable. Defaults to --avatar-size
   per the Avatar / tandem sizing cascades via --avatar-size rule,
   so dropping a .skeleton--circle inside an avatar slot Just
   Works. Inline-block + flex-shrink: 0 so it survives in flex
   row contexts. */
.skeleton--circle {
  --skeleton-radius: 50%;
  --skeleton-size: var(--avatar-size, var(--space-32));
  display: inline-block;
  width: var(--skeleton-size);
  height: var(--skeleton-size);
  vertical-align: middle;
  flex-shrink: 0;
}

/* Shape modifier — pill (chip-shaped). Full radius, inline-block,
   24 px tall to match the default chip height. Consumer sets
   width via style="width: 60px" or similar. */
.skeleton--pill {
  --skeleton-radius: var(--r-full);
  display: inline-block;
  height: var(--space-24);
  vertical-align: middle;
}

/* Reduced motion — drop the sweep, keep the tinted surface. The
   static block still reads as "content pending" without the
   continuous motion that triggers vestibular discomfort. Industry-
   standard fallback (shadcn, MUI, Carbon all kill the shimmer
   under reduced-motion; none of them hide the skeleton entirely). */
@media (prefers-reduced-motion: reduce) {
  .skeleton::after { animation: none; display: none; }
}

/* ── .refresh-timer ──────────────────────────────────────────
   High-tier composite that combines a .spinner (initially
   `--progress` + `--alpha`) with timer state to communicate
   "auto-refresh countdown → refreshing → reset". Lives at the
   trailing edge of a panel header (Receive routes today; Review
   state tomorrow).

   Composition contract — the wrapper button composes existing
   icon-button chrome: `.btn-icon.btn-borderless.btn-circle
   .refresh-timer`. That gives:
     • 40×40 px hit target (--space-40, default btn-icon)
       — matches the adjacent avatar-toggle in the Receive
       header so the two affordances pair visually
     • borderless ghost background
     • icon-button hover/active wash (8%/12% of --icon-primary)
     • circle border-radius
     • bg fade transition (0.2 s ease)
   .refresh-timer adds ONLY the state-specific concerns the
   button family doesn't have (refreshing-state pointer guard).
   No parallel chrome math — alpha treatment, sizing, hover, all
   come from existing classes.

   States — driven by data-state on the .refresh-timer:
     "countdown"  — inner spinner is .spinner--alpha.spinner--
                    progress with --spinner-progress driven by
                    the JS controller. Quiet ink tone; arc
                    fills 0 → 1 over data-interval ms (60 s
                    default).
     "refreshing" — inner spinner swaps to .spinner--primary
                    (no --progress) for the indeterminate spin
                    while the consumer's fetch is in flight.
                    Louder accent tone signals "active work."

   Click contract — the wrapper IS a <button>. Clicking expires
   the countdown immediately (the user-facing "refresh now"
   affordance). pointer-events: none while data-state=
   "refreshing" so a double-click can't queue a second fetch.

   Full behavior + event contract live in refresh-timer.js. */
.refresh-timer[data-state="refreshing"] {
  cursor: progress;
  pointer-events: none;
}

/* ── .status-dot ──────────────────────────────────────────────
   Small colored circle without content. Sibling of .spot-icon
   (the tile-with-content primitive); .status-dot covers the
   cases below .spot-icon's 32-px floor where there's no glyph
   room — step bullets, presence indicators ("online / offline /
   busy"), unread notification marks, "saving / saved" indicators,
   activity-feed timeline bullets.

   Sizes (lowest of the system — below .spot-icon's 32-px floor):
     .status-dot--xs        8 × 8 px   — unread dots, presence indicators
     .status-dot           12 × 12 px  — DEFAULT — step bullets, dirty marks
     .status-dot--lg       16 × 16 px  — small overlays on avatars

   Tones (parallel to .spot-icon, --sd-tone mirrors --si-tone):
     .status-dot--success / --info / --warn / --danger / --neutral

   Step-state vocabulary (parallel to .spot-icon[data-step-state]):
     [data-step-state="complete"]  filled accent
     [data-step-state="current"]   transparent + accent ring
     [data-step-state="upcoming"]  transparent + quiet ring

   Filled by default (no step-state attribute = pure colored dot).
   Reach for .spot-icon if you need a glyph or text inside; reach
   for .status-dot if a colored circle is the entire payload. */
.status-dot {
  --sd-tone: var(--accent-primary);
  --sd-track: color-mix(in oklch, var(--text-primary) 12%, transparent);

  display: inline-block;
  width: var(--space-12);
  height: var(--space-12);
  border-radius: 50%;
  background: var(--sd-tone);
  flex-shrink: 0;
  transition: background 0.2s ease, box-shadow 0.2s ease;
}
.status-dot--xs { width: var(--space-8);  height: var(--space-8);  }
.status-dot--lg { width: var(--space-16); height: var(--space-16); }

/* Tones */
.status-dot--success { --sd-tone: var(--success); }
.status-dot--info    { --sd-tone: var(--info); }
.status-dot--warn    { --sd-tone: var(--warn); }
.status-dot--danger  { --sd-tone: var(--danger); }
.status-dot--neutral { --sd-tone: var(--text-muted); }

/* Step-state vocabulary — same API as .spot-icon */
.status-dot[data-step-state="complete"] {
  background: var(--sd-tone);
}
.status-dot[data-step-state="current"] {
  background: transparent;
  box-shadow: inset 0 0 0 3px var(--sd-tone);
}
.status-dot[data-step-state="upcoming"] {
  background: transparent;
  box-shadow: inset 0 0 0 2px var(--sd-track);
}

/* ── Halo modifier — soft outward "spotlight" wash ─────────────
   Adds a tinted ring extending outward from the dot. Use to
   draw the eye to ONE dot in a row — the active item on a
   horizontal changelog timeline ("you are here"), a live
   indicator on a status display, a hero-level "look at this"
   marker. The halo extends outward via box-shadow so it
   doesn't change layout — adjacent text and sibling dots
   stay put.

   Halo geometry is two concentric rings at decreasing alpha
   (inner 18% / outer 8%) so the falloff reads as soft without
   relying on browser blur (which is GPU-dependent). Halo tone
   follows --sd-tone, so tone variants (--success / --warn /
   etc.) and the default accent all compose naturally.

   Mutually exclusive with [data-step-state="current"] — that
   uses an INSET shadow for the ring and the halo's OUTER shadow
   overrides it. They answer different questions:
     --halo                       → eye attraction / spotlight
     [data-step-state="current"]  → positional state in a stepper
   If a designer needs both visuals, they author a custom
   multi-stop box-shadow on the consuming element. Don't
   compose --halo + step-state on the primitive. */
.status-dot--halo {
  box-shadow:
    0 0 0 3px color-mix(in oklch, var(--sd-tone) 18%, transparent),
    0 0 0 6px color-mix(in oklch, var(--sd-tone)  8%, transparent);
}
.status-dot--xs.status-dot--halo {
  box-shadow:
    0 0 0 2px color-mix(in oklch, var(--sd-tone) 18%, transparent),
    0 0 0 4px color-mix(in oklch, var(--sd-tone)  8%, transparent);
}
.status-dot--lg.status-dot--halo {
  box-shadow:
    0 0 0 4px color-mix(in oklch, var(--sd-tone) 18%, transparent),
    0 0 0 8px color-mix(in oklch, var(--sd-tone)  8%, transparent);
}

/* ── Pulse modifier — animated outward sonar-ping ─────────────
   Animated counterpart to --halo. Use for live events where
   the motion itself is the signal: route arrival notifications,
   real-time MEV alerts, "this is live right now" markers.
   The dot stays solid; a tinted ring expands outward and fades
   on a 1.6-second loop.

   Built by animating box-shadow spread (start 0 px @ 50% tone,
   end 10 px @ 0%). Single property, no pseudo elements, no
   stacking-context headaches. Three keyframes — one per size —
   so the ring's terminal radius is proportional to the dot's
   diameter (xs → 7 px / default → 10 px / lg → 14 px).

   Accessibility: gated behind prefers-reduced-motion:
   no-preference. The default state for motion-sensitive users
   is a static dot (no animation, no shadow). This follows the
   accessibility-first pattern — opt-in to motion, not opt-out.

   Don't compose --pulse with --halo or [data-step-state] —
   all three occupy box-shadow. --pulse stands alone. */
@keyframes status-dot-pulse {
  0%   { box-shadow: 0 0 0  0px color-mix(in oklch, var(--sd-tone) 50%, transparent); }
  100% { box-shadow: 0 0 0 10px color-mix(in oklch, var(--sd-tone)  0%, transparent); }
}
@keyframes status-dot-pulse-xs {
  0%   { box-shadow: 0 0 0 0px color-mix(in oklch, var(--sd-tone) 50%, transparent); }
  100% { box-shadow: 0 0 0 7px color-mix(in oklch, var(--sd-tone)  0%, transparent); }
}
@keyframes status-dot-pulse-lg {
  0%   { box-shadow: 0 0 0  0px color-mix(in oklch, var(--sd-tone) 50%, transparent); }
  100% { box-shadow: 0 0 0 14px color-mix(in oklch, var(--sd-tone)  0%, transparent); }
}
@media (prefers-reduced-motion: no-preference) {
  .status-dot--pulse {
    animation: status-dot-pulse 1.6s ease-out infinite;
  }
  .status-dot--xs.status-dot--pulse {
    animation-name: status-dot-pulse-xs;
  }
  .status-dot--lg.status-dot--pulse {
    animation-name: status-dot-pulse-lg;
  }
}
/* min-width:0 is required so ellipsis on the desc child can shrink
   below its content size inside the parent flex row. */
.menu-item-text { display: flex; flex-direction: column; gap: 0; min-width: 0; }
.menu-item-title { font-size: var(--text-sm); font-weight: var(--fw-semibold); color: var(--text-primary); }
/* Single-line description rule — see CLAUDE.md → "Single-line
   descriptions in menus and navigation". Same pattern as .dd-desc. */
.menu-item-desc {
  font-size: var(--text-caption);
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Apr 2026 v17 — `.fab-sub-header` removed. Was a near-duplicate of
   `.pp-header`; both consolidated into a universal panel-head primitive.
   May 2026 v18 — panel-head itself retired into the .screen-header
   family (search `.screen-header--compact` below). FAB sub-panels now
   compose `.screen-header.screen-header--compact.screen-header--sticky-glass`. */

/* ── Light mode FAB overrides ────────────────────────────────────────────── */
[data-theme="light"] .fab {
  box-shadow: 0 4px 20px oklch(0% 0 0 / 0.08);
}
[data-theme="light"] .fab:hover {
  box-shadow: 0 6px 28px oklch(0% 0 0 / 0.12), 0 0 0 4px oklch(0% 0 0 / 0.03);
}
/* Light-mode shadow now token-driven — --shadow-panel-glass auto-flips
   via [data-theme="light"]. No per-component override needed. */
[data-theme="light"] .menu-item:hover { background: oklch(0% 0 0 / 0.04); }
[data-theme="light"] .menu-item.is-current { background: color-mix(in oklch, var(--accent-primary) 8%, transparent); }
/* Light-mode default — scoped with explicit :not(.spot-icon--<tone>)
   so the tone variants keep specificity parity and continue to drive
   bg + color from --si-tone. Don't use [class*="spot-icon--"] here —
   that's over-broad and would also exclude shape modifiers like
   .spot-icon--round (which DOES want the default bg/color, just a
   different border-radius). Without the :not(), the attribute+class
   selector here (specificity 0,2,0) beats the tone variant's single
   class (0,1,0) and the tone wash never paints. */
[data-theme="light"] .spot-icon:not(.spot-icon--success):not(.spot-icon--info):not(.spot-icon--warn):not(.spot-icon--danger):not([data-step-state]),
[data-theme="light"] .menu-item-icon { background: oklch(0% 0 0 / 0.04); color: var(--text-primary); }

/* ── FAB panel section divider ───────────────────────────────────────────
   Separates the Design System section (hero + 5 links) from the
   "On this page" on-page controls below. Only rendered when the FAB
   has at least one `[data-fab-controls]` source on the current page —
   otherwise the panel is DS-only and no divider is needed. */
.menu-divider {
  display: flex;
  align-items: center;
  gap: var(--space-12);
  margin: var(--space-12) 0 var(--space-8);
  color: color-mix(in oklch, var(--text-primary) 40%, transparent);
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
}
.menu-divider::before,
.menu-divider::after {
  content: '';
  flex: 1;
  height: 1px;
  background: color-mix(in oklch, var(--text-primary) 12%, transparent);
}

/* Menu footer action — a full-width button placed as the LAST child of a
   `.menu` or `.dropdown__panel` (e.g. "+ Create organization" in the Portal
   org switcher). The button's own chrome (`.btn-neutral` fill + reserved
   border slot) sets it apart from the borderless item rows, so it needs NO
   `.menu-divider` above it — just breathing room. The list above stays the
   choices; the button reads as the distinct action. Opt-in via `.btn-block`:
   only a full-width footer button picks up the separation, so item-row
   buttons (if any) are untouched. The 4px `.menu` flex-gap stacks under the
   8px here (≈12px in a `.menu`); the non-flex `.dropdown__panel` gets the
   8px alone (the last row's 8px bottom-padding makes ≈16px visually). */
.menu > .btn-block,
.dropdown__panel > .btn-block {
  margin-top: var(--space-8);
}

/* ══════════════════════════════════════════════════════════════════════
   .dropdown — anchored menu trigger + floating panel
   ══════════════════════════════════════════════════════════════════════
   A canonical dropdown: a button trigger that toggles a glass-panel
   menu anchored beneath it. The trigger composes with any `.btn-*` for
   styling (so it inherits the full sm / default / lg / xl height ladder
   without forking sizes); the panel uses the canonical Panel Surface
   recipe (`--surface-panel-glass` + `--shadow-panel-glass` +
   `--backdrop-panel-glass`); the items inside reuse the `.menu` family
   so descriptions, icons, sections, and active states already work.

   When NOT to use
   ───────────────
   - Native `<select>` for forms with browser-native a11y → use `.select`
   - 3+ mutually exclusive options that fit inline → segmented control
   - Searchable / fuzzy command launch → `.cmd-palette`
   - Multi-select with checkboxes → checkbox list, not a dropdown

   Anatomy
   ───────
     <div class="dropdown">
       <button class="dropdown__trigger btn-secondary"
               type="button"
               aria-haspopup="menu"
               aria-expanded="false">
         <span class="dropdown__label">Sort by</span>
         <svg class="dropdown__chevron"><!-- caret --></svg>
       </button>
       <div class="dropdown__panel" role="menu">
         <ul class="menu">
           <li><a href="…" class="menu-item" role="menuitem">…</a></li>
           …
         </ul>
       </div>
     </div>

   States
   ──────
   - `[data-open]` on `.dropdown` opens the panel (CSS handles the
     fade/translate transition + chevron rotation).
   - `aria-expanded` on the trigger mirrors the open state for a11y.
   - JS handler in `shared.js → initDropdown()` toggles `[data-open]`
     on click, closes on outside-click + Escape, and closes on item
     click.

   Modifiers
   ─────────
   - `.dropdown__panel--align-end`  — right-edge anchored (panel right
                                       aligns with the trigger right);
                                       use when the trigger is right-
                                       most in its row and a left-
                                       anchored panel would clip the
                                       viewport.
   ══════════════════════════════════════════════════════════════════════ */
.dropdown {
  position: relative;
  /* inline-FLEX, not inline-block: an inline-block wrapper reserves line-box
     space below the baseline for descenders, so it renders ~2px taller than
     an icon-only trigger inside it (a 24px btn → a 26px wrapper). Flex emits
     no baseline gap, so the wrapper hugs the trigger exactly. The panel is
     position:fixed + popover (top-layer) — out of flow, never a flex item —
     and consumers have a single in-flow trigger child, so the wrapper sizes
     to the trigger. */
  display: inline-flex;
}
.dropdown__trigger {
  /* Pairs visually with `.btn-neutral` — the trigger sits alongside
     buttons in product UI (form rows, toolbars, action footers) and
     reads as one family with them. Same translucent-overlay surface,
     same hover/active bg progression (8 % → 14 % → 18 %), same button
     family typography (`--fw-semibold`, `--tracking-normal`). State
     transitions ride the bg alpha; there's no border-color flip or
     focus-ring inflation — the same affordance recipe `.btn-neutral`
     uses. Two sizes only — default and `.btn-sm`. The dropdown
     intentionally doesn't ship lg / xl tiers; a 48–56 px dropdown
     trigger reads as a primary CTA, which is the wrong role for a
     quiet content-adjacent control.

     Border stays `1 px solid transparent` so the height math lands on
     the button family ladder (sm 32 / default 40), not the alpha
     button's missing-border 30/38. The transparent slot is invisible
     at every state — it just reserves the 2 px that brings the height
     onto the canonical ladder.

     The trigger never lifts on hover, which is exactly what we want — the
     popover panel is anchored to the trigger's bounding rect via JS and
     only repositions on click/scroll/resize, not on hover. Lifting the
     trigger would visually disconnect it from the panel for 200 ms while
     the JS catches up. Since the May 2026 inversion every button is static
     by default (lift is the opt-in `.btn-lift`), so the trigger gets the
     bg-progression-only affordance for free — no suppression needed.

     Migration note (May 2026): retired the prior `--field-*` token
     recipe (visible border, 3 px focus ring, --fw-medium). The trigger
     used to read as a "selectable input" pairing with `.input` /
     `.select`; the new pairing is `.btn-neutral`, matching the user's
     directive that dropdowns sit alongside buttons in product UI as
     one family. See CLAUDE.md → "Dropdown trigger pairs with .btn-neutral".

     IMPORTANT — size-related properties (padding, border-radius,
     font-size, line-height) live in the `:where(.dropdown__trigger)`
     block below. `:where()` strips selector specificity to 0 so the
     `.btn-sm` modifier (specificity 0,1,0) always wins on tied
     properties. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-8);
  font-family: var(--font-sans);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-normal);
  background: color-mix(in oklch, var(--text-primary) 8%, transparent);
  color: var(--text-primary);
  border: 1px solid transparent;
  cursor: pointer;
  text-decoration: none;
  box-sizing: border-box;
  transition: background-color 0.18s ease;
}
/* Default size values — radius and typography track the default `.btn`
   so a default dropdown trigger next to a default button shares the
   same corner and label rhythm. Padding is INTENTIONALLY tighter than
   the button family's 12 / 24 (matches buttons): a label-and-chevron
   trigger doesn't carry text-button width semantics. Symmetric 12 px
   all around hugs the label-and-chevron content with even breathing,
   keeps the 40 px height (12+12+14+2 = 40, on the canonical ladder),
   and trims ~24 px of unmotivated width per trigger. Specificity 0
   (via `:where()`) so any composed `.btn-sm` modifier wins on
   border-radius, font-size, and line-height — but NOT on padding,
   which the next rule re-asserts for the sm tier (`.btn-sm`'s 8 / 20
   asymmetric padding would otherwise leak through). */
:where(.dropdown__trigger) {
  padding: var(--space-12);
  border-radius: var(--space-16);
  font-size: var(--text-sm);
  line-height: 1;
}
/* Sm tier — symmetric 8 px all around. `.btn-sm`'s padding is 8 / 20
   (asymmetric for text buttons); the dropdown trigger overrides to
   8 / 8 for the same "hug the label and chevron" reason as default.
   Math: 8+8+14+2 = 32 px (on the canonical sm ladder). Specificity
   (0, 2, 0) — beats `.btn-sm` (0, 1, 0). */
.dropdown__trigger.btn-sm {
  padding: var(--space-8);
}
.dropdown__trigger:hover {
  background: color-mix(in oklch, var(--text-primary) 14%, transparent);
}
.dropdown__trigger:active,
.dropdown[data-open] .dropdown__trigger {
  background: color-mix(in oklch, var(--text-primary) 18%, transparent);
}
.dropdown__trigger:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Bare variant — pairs with `.btn-borderless` icon buttons. No fill,
   no border, only a subtle text-tint hover. Reach for it when the
   dropdown sits inside a busy chrome row (table-header action menus,
   toolbars next to `.btn-borderless` icons, navbar utility clusters)
   where the default card-style trigger would compete visually with
   surrounding interactive elements. */
.dropdown__trigger--bare {
  background: transparent;
  border-color: transparent;
  color: var(--text-muted);
}
.dropdown__trigger--bare:hover {
  background: color-mix(in oklch, var(--text-primary) 8%, transparent);
  border-color: transparent;
  color: var(--text-primary);
}
.dropdown__trigger--bare:focus-visible,
.dropdown[data-open] .dropdown__trigger--bare {
  background: color-mix(in oklch, var(--text-primary) 12%, transparent);
  border-color: transparent;
  box-shadow: none;
  color: var(--text-primary);
}
[data-theme="light"] .dropdown__trigger--bare:hover {
  background: color-mix(in oklch, var(--text-primary) 6%, transparent);
}
[data-theme="light"] .dropdown[data-open] .dropdown__trigger--bare {
  background: color-mix(in oklch, var(--text-primary) 9%, transparent);
}

/* Flush variant — chromeless trigger that sits inside a content card
   row (canonical case: a .setting-row hosting a dropdown in its right
   slot) and aligns its chevron to the parent row's right padding.
   Differs from --bare in two ways:
     (1) padding: 0 — the parent row's horizontal padding owns the
         column system. The chevron sits flush to the trigger's own
         right edge, which means it aligns to the row's right-content
         column. With --bare's 8 px internal padding the chevron would
         sit 8 px inboard of the row's right padding line and break
         the column alignment that's the whole point here.
     (2) No hover bg progression — the parent row carries hover state
         if any (drill rows lift; settings rows stay flat), and the
         chevron rotation on `[data-open]` is the affordance cue. A bg
         tint on a chromeless trigger would compete with the row's own
         surface.
   Chevron bumps to 16 × 16 (vs the trigger's default 12 × 12). The
   chrome'd trigger's chevron is small on purpose — it has to keep the
   trigger on the 32 / 40 px height ladder. Flush has no chrome carrying
   height-ladder math (the parent row's `min-height` does); the bigger
   chevron reads as the proper right-column anchor.
   Reach for --flush when the dropdown sits inside a .setting-row,
   summary row, or any card-internal toolbar where the trigger should
   read as the row's value column, not a standalone control. */
.dropdown__trigger--flush {
  background: transparent;
  border-color: transparent;
  color: var(--text-primary);
}
.dropdown__trigger.dropdown__trigger--flush {
  /* padding: 0 — specificity (0, 2, 0) beats both the :where()-stripped
     default and .dropdown__trigger.btn-sm (also 0, 2, 0) by source
     order. Same shape covers default + .btn-sm tier with one rule. */
  padding: 0;
}
.dropdown__trigger--flush:hover,
.dropdown__trigger--flush:focus-visible,
.dropdown[data-open] .dropdown__trigger--flush {
  background: transparent;
  border-color: transparent;
  box-shadow: none;
}
.dropdown__trigger--flush > .dropdown__chevron {
  width: var(--space-16);
  height: var(--space-16);
}

/* Seg variant — chromeless trigger composed inside a .seg.
   The canonical use is the "···" overflow trigger (.seg-overflow's
   .seg-item--more): a .seg-item that ALSO wires up Popover behavior
   via .dropdown__trigger. The default trigger ships chrome (bg,
   border, color, shadow) for its standalone toolbar role; inside a
   seg we want the host .seg-item to fully own visuals so the seg's
   state recipe (rest --on-alpha-muted, hover --on-alpha-default,
   active --text-primary, borderless fills) and tier sizing cascade
   through unimpeded.

   Sibling to --bare (toolbar context, sets its own --text-muted color)
   and --flush (setting-row context, padding-0). Same shape: one named
   modifier per host context.

   Chrome is suppressed at every state (rest / hover / focus-visible /
   [data-open]) so the default trigger's bg + border-color + shadow
   can't bleed through. The seg's own borderless rules
   (.seg--borderless .seg-item:hover at (0,3,1), .is-active at (0,3,0))
   then paint the affordance. Padding and border-radius DON'T need
   overrides — the default trigger's `:where()`-stripped values
   (specificity 0) lose to .seg-item's (0,1,0) padding+radius and to
   .seg--borderless's (0,2,0) padding-block: 0. The border slot stays
   `1px solid transparent` (inherited from .dropdown__trigger's base
   rule) so the trigger lands on the 40 px height ladder via border-
   box sizing; only the visible chrome is dropped, per CLAUDE.md →
   "Reserved border slots — kill the paint, keep the slot."

   For color: at (0,1,0) my rest+hover declarations land later in
   source than .dropdown__trigger's color, so they win on tied
   specificity. The seg-item's active state (.seg-item.is-active at
   (0,2,0)) still beats my rest color, so when the overflow trigger
   carries .is-active (because the active mode lives inside the
   menu — see swap.js → applyMode), the canonical --text-primary
   color cascades through naturally. */
.dropdown__trigger--seg,
.dropdown__trigger--seg:hover,
.dropdown__trigger--seg:focus-visible,
.dropdown[data-open] .dropdown__trigger--seg {
  background: transparent;
  border-color: transparent;
  box-shadow: none;
}
.dropdown__trigger--seg {
  color: var(--on-alpha-muted);
}
.dropdown__trigger--seg:hover {
  color: var(--on-alpha-default);
}

/* Card variant — ADOPTS the canonical UI card recipe (the .option-card
   look) instead of suppressing chrome. The --bare / --flush / --seg
   variants above all STRIP chrome so a host primitive's recipe shows
   through; --card is the opposite — it dresses the trigger as a card and
   spans its column by default. Reach for it when the trigger is a
   prominent, content-bearing selector that fills a rail or panel column
   (an org / workspace switcher, an account picker, a "pick one" control
   that should read as a card, not a compact toolbar chip). The Portal
   left-rail org switcher is the reference consumer.

   Surface recipe mirrors .option-card exactly so the two read as one
   family: --surface-card + --elev-1 at rest, a 1 px transparent rim that
   flips to a softened 64 %-accent rim when open, accent-tinted hover wash
   (3 % hover / 4 % open), no halo glow, no layout shift. Hover / open
   route through --dropdown-card-tone (default --accent-primary) per
   CLAUDE.md → "Variant-aware states — route through a --*-tone token" so
   a themed switcher can retint with one override. The avatar/preview the
   trigger carries can push it taller than the 40 px ladder — that's by
   design (it's a card, like .option-card), not a tier break to fix.

   `.dropdown:has(.dropdown__trigger--card)` upgrades the wrapper from the
   default inline-flex (which hugs a compact trigger) to block-level flex
   so the full-width trigger has a column to span. */
.dropdown:has(.dropdown__trigger--card) {
  display: flex;
}
.dropdown__trigger--card {
  --dropdown-card-tone: var(--accent-primary);
  width: 100%;
  justify-content: flex-start;
  gap: var(--space-12);
  background: var(--surface-card);
  border-color: transparent;
  color: var(--text-primary);
  box-shadow: var(--elev-1);
  transition: background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
}
.dropdown__trigger--card:hover {
  background: color-mix(in oklch, var(--dropdown-card-tone) 3%, var(--surface-card));
  box-shadow: var(--elev-2);
}
.dropdown__trigger--card:active,
.dropdown[data-open] .dropdown__trigger--card {
  background: color-mix(in oklch, var(--dropdown-card-tone) 4%, var(--surface-card));
  border-color: color-mix(in oklch, var(--dropdown-card-tone) 64%, transparent);
  box-shadow: var(--elev-2);
}
/* Avatar sizes to the official 32 px (sm) tier — set on the avatar host
   via --avatar-size per the avatar-size cascade rule, never w/h on the
   inner mark. (0,2,0) beats a consumer's own --avatar-size knob like
   .portal-org-mark, so the card switcher's leading mark is a uniform
   32 px wherever the variant is used.) */
.dropdown__trigger--card .avatar {
  --avatar-size: var(--space-32);
}
/* Label fills the space between the leading content and the chevron and
   truncates rather than wrapping, so the trigger stays single-row. One
   tier up from the default trigger label (--text-sm → --text-body, 16 px)
   so the switcher's primary identity reads at body scale. */
.dropdown__trigger--card .dropdown__label {
  flex: 1 1 auto;
  min-width: 0;
  text-align: left;
  font-size: var(--text-body);
  font-weight: var(--fw-semibold);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Chevron at the 16 px tier (the same size the --flush variant uses),
   muted, pinned to the far right (margin-left:auto is the backstop when
   there's no .dropdown__label to push it over). Rotation on [data-open]
   is inherited from the base .dropdown[data-open] .dropdown__chevron rule. */
.dropdown__trigger--card .dropdown__chevron {
  width: var(--space-16);
  height: var(--space-16);
  margin-left: auto;
  color: var(--text-muted);
}

/* `.dropdown__trigger > .dropdown__chevron` outranks the global
   `[class*="btn-"] > svg { display: none }` rule (specificity 0,2,1
   vs 0,1,1) — keeps the chevron rendered when the trigger composes
   with the `.btn-sm` size class (or, generically, when the consumer
   composes any `.btn-*` size modifier). */
.dropdown__trigger > .dropdown__chevron {
  display: inline-block;
}
.dropdown__chevron {
  /* 12 px square — sized smaller than the 14 px text line-box so the
     text drives the flex container's cross-axis size, NOT the chevron.
     If the chevron were sized to the line-box or larger (16 px through
     May 2026), `align-items: center` would size the flex cross-axis to
     the chevron and push the trigger off the canonical 32 / 40 px
     height ladder (default rendered at 42 px instead of 40 px,
     breaking pixel-pairing with .btn-primary / .btn-secondary at the
     same tier). Picked `--space-12` over a literal 14 px because the
     spacing ladder skips 14 — 12 is the closest tier and gives the
     same on-ladder height result. */
  width: var(--space-12);
  height: var(--space-12);
  flex-shrink: 0;
  /* Muted-tier ink so the chevron reads as a quiet structural marker,
     not a competing signal. Same ink as .muted / placeholder copy. The
     trigger's label keeps full ink (currentColor on the trigger); the
     chevron stays muted across rest / hover / open so the bg-tint hover
     wash carries the affordance cue, not the chevron. May 2026. */
  color: var(--text-faint);
  transition: transform 0.18s ease;
}
.dropdown[data-open] .dropdown__chevron { transform: rotate(180deg); }

.dropdown__panel {
  /* Renders in the browser's top-layer via the Popover API (HTML
     attribute `popover="manual"` on the panel element). Top-layer
     escapes any ancestor `overflow: hidden | clip` — without this
     the panel would be clipped at the .ui-card boundary when the
     dropdown sits inside a viewport-capped widget. Migration ran
     May 2026 after the limit-expiry dropdown was 113 px clipped at
     the panel's bottom. See CLAUDE.md → tooltip/dropdown clipping
     for the diagnostic + decision history.

     Position is `fixed` + driven by JS — shared.js → initDropdown()
     reads the trigger's bounding rect on each open / scroll / resize
     and writes top + left (or right, for --align-end). Anchor
     Positioning would be cleaner but isn't GA across Firefox yet
     (May 2026); JS positioning is the universal fallback.

     Glass-by-class default replaced with Paper-by-class registry
     (Same May 2026 sweep) — surface is var(--surface-card) + elev-1,
     no backdrop blur. */
  position: fixed;
  /* top + left set by JS (initDropdown → positionPanel) */
  margin: 0;                            /* cancel UA popover margin */
  /* Content-sized panel — width hugs the longest .menu-item's
     intrinsic content (max-content), floored at 10rem (160 px) so
     short-label panels still read as "menu" not "strip," and ceilinged
     at var(--w-3) (288 px) so long-label panels stay within the
     canonical menu tier. Pre-May-2026 the panel was a flat
     `width: var(--w-3)` — too wide for short-label lists like USD /
     EUR / GBP / JPY (panel rendered at 288 px for ~30 px of label).
     Content-sized lets each panel earn its width from its own items. */
  width: max-content;
  min-width: 10rem;
  max-width: var(--w-3);                /* 288 px — canonical menu tier ceiling */
  max-height: 75vh;
  overflow-y: auto;
  background: var(--mat-bg);
  /* Shadow steps the panel up to --elev-3 — the floating-surface tier
     (per styles.css line 500: "floating surfaces — modals, popovers,
     dropdowns"). Paper material's default --mat-shadow is --elev-1
     which is the resting-cards tier and reads as too anchored for a
     floating overlay. The surface tokens (--mat-bg) stay paper —
     only the elevation tier changes. --mat-highlight is the inset
     rim that the material registry composes; preserve it so dark-
     mode keeps its top edge highlight. */
  box-shadow: var(--elev-3), var(--mat-highlight);
  backdrop-filter: var(--mat-backdrop);
  -webkit-backdrop-filter: var(--mat-backdrop);
  border: 0;                            /* cancel UA popover border */
  border-radius: var(--card-radius);
  padding: var(--space-8);
  /* Open / close transition — :popover-open matches when the popover
     is in the top-layer. @starting-style provides the entry-from
     values (the popover is display:none before being shown, so a
     standard `from → to` transition wouldn't fire without it). */
  opacity: 1;
  transform: translateY(0);
  transition:
    opacity 0.18s ease,
    transform 0.18s ease,
    display 0.18s allow-discrete,
    overlay 0.18s allow-discrete;
}
.dropdown__panel:not(:popover-open) {
  opacity: 0;
  transform: translateY(-4px);
}
@starting-style {
  .dropdown__panel:popover-open {
    opacity: 0;
    transform: translateY(-4px);
  }
}
/* --align-end is now a JS flag (positionPanel reads the class) —
   the CSS `right: 0` no longer applies because position is fixed and
   set programmatically. The selector remains for backwards-compat /
   discoverability; declaring `right: auto` here is intentional to
   neutralize any inherited UA right value. */
.dropdown__panel--align-end {
  right: auto;
}

/* Match-trigger width — the panel's width is set to the trigger's
   rendered width by JS (initDropdown → positionPanel reads the class and
   writes panel.style.width). Pairs with `.dropdown__trigger--card`: a
   full-width card trigger reads incomplete above a max-content panel, so
   the panel spans the same column left-to-right. `max-width: none` lifts
   the default --w-3 (288 px) menu ceiling, since a rail/panel column can
   legitimately run wider than the canonical menu tier. */
.dropdown__panel--match-trigger {
  max-width: none;
}
/* Row labels match the card trigger's bumped label (--text-body, 16 px)
   so the open menu reads at the same scale as the trigger it spans from.
   Scoped to the match-trigger panel — the global .menu-item-title stays
   --text-sm everywhere else. */
.dropdown__panel--match-trigger .menu-item-title {
  font-size: var(--text-body);
}

/* ── Mobile FAB ──────────────────────────────────────────────────────────── */
@media (max-width: 600px) {
  .fab { bottom: 16px; right: 16px; }
  .ds-doc-menu-panel { width: calc(100vw - 32px); right: 16px; bottom: 74px; }
  /* Floating Theme Composer matches the glass sub-panel's mobile footprint —
     fill the viewport gutter, clear the FAB. max-height tightens to the
     available height above the bottom anchor. */
  .theme-composer--floating {
    width: calc(100vw - 32px);
    right: 16px;
    bottom: 74px;
    max-height: min(80vh, calc(100vh - 90px));
  }
}


/* ══════════════════════════════════════════
   FAB PANEL CONTROLS — Unified .ds-* component library
   ──────────────────────────────────────────
   Shared by Theme Composer, Globe, Logo Stack, and Reactive Coins panels.
   All colors use palette tokens so they respond to the Theme Composer.
   ══════════════════════════════════════════ */

/* Panel slider modifier — compact version of .slider-range for FAB panels */
/* Panel slider — uses the default slider-range sizing (6px track, 20px thumb)
   but ensures full-width layout without needing the .slider wrapper. */
.slider-range--panel {
  width: 100%;
}

/* 1. Control Row — label + value readout + slider */
.ds-doc-control-row { margin-bottom: var(--space-8); }
.ds-doc-control-head {
  display: flex; justify-content: space-between; align-items: center;
  margin-bottom: var(--space-4);
}
.ds-doc-control-label {
  font-size: var(--text-sm); font-weight: var(--fw-medium); color: var(--text-secondary);
}
.ds-doc-control-val {
  font-family: var(--font-mono, 'Geist Mono', monospace);
  font-size: var(--text-caption); font-weight: var(--fw-semibold); color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  min-width: 3ch; text-align: right;
}

/* 2. Color Picker — label + native <input type="color"> */
.ds-doc-color-picker {
  display: flex; justify-content: space-between; align-items: center;
  margin-bottom: var(--space-8);
}
.ds-doc-color-input {
  -webkit-appearance: none; appearance: none;
  width: 36px; height: var(--space-24); border: none; border-radius: var(--space-8);
  cursor: pointer; background: none; padding: 0; flex-shrink: 0;
  font-family: inherit;                              /* defensive — <input> UA default is Arial; inherit brand Figtree (input equivalent of the button reset) */
}
.ds-doc-color-input::-webkit-color-swatch-wrapper { padding: 0; }
.ds-doc-color-input::-webkit-color-swatch {
  border-radius: var(--space-4);
  border: 2px solid color-mix(in oklch, var(--text-primary) 12%, transparent);
}
.ds-doc-color-input::-moz-color-swatch {
  border-radius: var(--space-4);
  border: 2px solid color-mix(in oklch, var(--text-primary) 12%, transparent);
}

/* Theme Composer swatch row + Preset Row — RETIRED May 2026.
   The theme-composer's row picker now uses .swatch-row + a
   <label class="swatch swatch--sm swatch--circle swatch--interactive">
   with an absolutely-positioned .swatch-input child. The preset row
   uses .swatch-row + <button class="swatch swatch--sm swatch--circle
   swatch--interactive">. Both produce the same visual at 32 px (was
   24 px for pc-swatch / 32 px for ds-doc-preset) — the new
   .swatch--sm tier consolidates them into one canonical size. */

/* 4. Segmented Toggle — retired. All panels now use the shared Segmented
   Control (.seg / .seg-item / .seg-sm) from section 1b. The old .ds-doc-toggle
   was a near-duplicate with a different class API; keeping both produced
   two sources of truth for the same widget. */

/* 5. Accordion — RETIRED June 2026. The legacy FAB-panel collapsible
   (.accordion + .accordion-header / -chev / -body, single-dash children,
   mono uppercase header, display:none snap) had ZERO product consumers —
   the Globe / Logo Stack / Reactive Coins panels all run flat
   .ds-doc-control-row sections with .divider separators, and its only
   remaining reference was the catalog card documenting it. A FAB panel
   that needs a collapsible section reaches for .setting-row--expand
   (the Theme Composer's FAB mount is the in-context precedent). */

/* 6. Divider — 1px visual separator */
.divider {
  height: 1px; margin: var(--space-16) 0;
  background: color-mix(in oklch, var(--text-primary) 7%, transparent);
}

/* Labeled variant — a centered label flanked by hairlines ("or", "then").
   Markup: <div class="divider divider--label"><span>or</span></div>. The
   base .divider supplies the margin; --label drops the single-hairline bg
   and becomes a flex row with the hairlines on ::before/::after. Promoted
   June 2026 from a Checkout-scoped one-off. */
.divider--label {
  height: auto;
  background: none;
  display: flex;
  align-items: center;
  gap: var(--space-12);
  color: var(--text-muted);
  font-size: var(--text-sm);
}
.divider--label::before,
.divider--label::after {
  content: "";
  flex: 1 1 auto;
  height: 1px;
  background: color-mix(in oklch, var(--text-primary) 7%, transparent);
}


/* ══════════════════════════════════════════════════════════════════════
   APP SHELL — .app-shell / .app-rail / .app-canvas (shared product shell)
   ══════════════════════════════════════════════════════════════════════
   The standardized product shell: a fixed glass rail (.app-rail) over a
   padded, scrollable content canvas (.app-canvas), with a multi-screen
   drill-in system and a slide-out collapse. Generalized from the Playground
   rail recipe (swap.css -> .playground-rail / .playground-canvas) so the
   Portal -- and, in a follow-up, the Playground -- share one shell.

   The rail COMPOSES the universal .panel primitive for surface chrome;
   .app-rail adds only fixed geometry + the collapse transform. The
   .drawer-handle (below) is the OPTIONAL collapse affordance: consumers
   that want a manual notch wire it; the Portal hides it (collapse is
   responsive-only for now).

   CSS vars live on .app-shell (put it on <body>):
     --rail-width / --rail-left / --rail-gap / --canvas-top / --canvas-bottom
     --rail-tx (slide transform -- 0 expanded, -(width+left) collapsed)

   JS: shared.js -> window.lifiInitAppRailScreens(rail)
                  + window.lifiInitAppRailCollapse(rail, opts)
   Markup:
     <body class="app-shell">
       <aside class="panel app-rail">
         <div class="app-rail__screen is-active" data-screen-id="root"> … </div> …
       </aside>
       <main class="app-canvas"> … </main>
       <aside class="panel app-rightrail"> … </aside>   (optional, reserved)
*/
.app-shell {
  --rail-width: var(--w-6);
  --rail-mini:  var(--space-72);   /* collapsed width of the .app-rail--mini variant */
  --rail-left: var(--space-24);
  --rail-gap: var(--space-24);
  --canvas-top: var(--space-24);
  --canvas-bottom: var(--space-24);
  --rail-tx: 0px;
  background: var(--surface-sunk);
}
.app-shell.is-rail-collapsed { --rail-tx: calc(-1 * (var(--rail-width) + var(--rail-left))); }
.app-shell.is-rail-collapsed .app-canvas { padding-left: var(--rail-gap); }
.app-shell.is-rail-collapsed .app-rail   { pointer-events: none; }

.app-canvas {
  position: relative;
  min-height: 100vh;
  padding: var(--canvas-top) var(--rail-gap) var(--canvas-bottom)
           calc(var(--rail-width) + var(--rail-left) + var(--rail-gap));
  background: var(--surface-sunk);
  transition: padding 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
}

.app-rail {
  position: fixed;
  left: var(--rail-left);
  top: var(--canvas-top);
  bottom: var(--canvas-bottom);
  width: var(--rail-width);
  display: flex;
  flex-direction: column;
  /* squared top-right corner so a docked .drawer-handle meets it flush */
  border-radius: var(--space-16) 0 var(--space-16) var(--space-16);
  z-index: 100;
  overflow-x: hidden;
  transform: translateX(var(--rail-tx));
  transition: transform 0.4s cubic-bezier(0.2, 0.8, 0.2, 1),
              padding 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
}

/* ── .app-rail--mini — hover-expand collapsed rail (shared variant). ───────────
   A collapsed icon rail (--rail-mini, 72) that expands to the full --rail-width
   on hover OR keyboard focus. DESKTOP-ONLY (≥901px); below that the rail's
   offscreen .is-rail-collapsed state takes over (this variant yields via the media
   query + :not(.is-rail-collapsed)). OVERLAY model — the canvas reserves only the
   mini width; the expanded rail floats over it (no reflow). Marks stay left-
   anchored and centre collapsed via small transform offsets that animate to 0, so
   they GLIDE (no justify-flip snap); trailing labels clip to zero width (height
   kept) and wipe in via overflow-x:hidden as the width blooms. Full expand/
   collapse motion contract: design/components/navigation.md → App rail. Promoted
   from the Portal (June 2026); Portal-specific content slots (.portal-rail__*)
   compose on top in portal.css. The slot rules below target SHARED primitives
   (brand-lockup, search-trigger, side-nav, dropdown__trigger--card, ui-wallet-
   mount) so any app-rail built from them collapses for free. */
@media (min-width: 901px) {
  /* Canvas reserves the MINI width only (rail overlays when expanded). */
  .app-shell:has(.app-rail--mini):not(.is-rail-collapsed) .app-canvas {
    padding-left: calc(var(--rail-mini) + var(--rail-left) + var(--rail-gap));
  }

  /* Resting (collapsed) — width animates; horizontal padding tightens (vertical
     constant). The −1px compensates the 1px panel border so inner content is a
     true 48 (not 46). transition-delay HERE = close-linger (forgive a quick exit). */
  .app-rail--mini {
    width: var(--rail-mini);
    padding: var(--space-20) calc(var(--space-12) - 1px);
    transition: width 0.26s cubic-bezier(0.2, 0.8, 0.2, 1) 0.18s,
                padding 0.26s cubic-bezier(0.2, 0.8, 0.2, 1) 0.18s,
                transform 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
  }
  /* Expanded — hover OR keyboard focus. delay HERE = open-friction (graze guard). */
  .app-rail--mini:hover,
  .app-rail--mini:focus-within {
    width: var(--rail-width);
    padding: var(--space-20);
    transition: width 0.26s cubic-bezier(0.2, 0.8, 0.2, 1) 0.09s,
                padding 0.26s cubic-bezier(0.2, 0.8, 0.2, 1) 0.09s;
  }

  /* Trailing content → zero width (keep height) collapsed; wipes in on expand. */
  .app-rail--mini:not(:hover):not(:focus-within) :is(
    .brand-lockup__wordmark, .brand-lockup__label,
    .search-trigger__label, .dropdown__label,
    .dropdown__chevron, .side-nav__label, .side-nav__trail,
    .ui-wallet-mount__empty,
    .ui-wallet-mount .list-item__body, .ui-wallet-mount .list-item__trail
  ) {
    max-width: 0;
    min-width: 0;
    margin-inline: 0;
    padding-inline: 0;
    overflow: hidden;
    white-space: nowrap;
  }
  .app-rail--mini:not(:hover):not(:focus-within) .side-nav__item--parent::after { display: none; }

  /* Uniform 48px slots so the collapsed column reads as an even grid. */
  .app-rail--mini .app-rail__head,
  .app-rail--mini .search-trigger,
  .app-rail--mini .side-nav__group > .side-nav__item {
    min-height: var(--space-48);
  }

  /* Brand mark — centre collapsed via a transform offset that animates to 0 (it
     cancels the content-left drift from the padding), so the mark holds still
     while the wordmark fades in beside it — a smooth logo→lockup reveal. */
  .app-rail--mini .brand-lockup__glyph {
    transition: transform 0.26s cubic-bezier(0.2, 0.8, 0.2, 1) 0.18s;
  }
  .app-rail--mini:hover .brand-lockup__glyph,
  .app-rail--mini:focus-within .brand-lockup__glyph {
    transition: transform 0.26s cubic-bezier(0.2, 0.8, 0.2, 1) 0.09s;
  }
  .app-rail--mini:not(:hover):not(:focus-within) .brand-lockup__glyph {
    transform: translateX(8px);   /* centres the 24px mark in the 48 collapsed head */
  }
  .app-rail--mini :is(.brand-lockup__wordmark, .brand-lockup__label) {
    transition: opacity 0.18s ease 0.26s;   /* fade in after the rail opens */
  }
  .app-rail--mini:not(:hover):not(:focus-within) :is(.brand-lockup__wordmark, .brand-lockup__label) {
    opacity: 0;
    transition: opacity 0.1s ease;
  }

  /* Search / nav icons / account avatar — no-snap: centre via a small transform
     offset animating to 0 (measured: search/account +3, nav −2). */
  .app-rail--mini .search-trigger__icon {
    transition: color 0.18s ease, background 0.18s ease, box-shadow 0.18s ease,
                transform 0.26s cubic-bezier(0.2, 0.8, 0.2, 1) 0.18s;
  }
  .app-rail--mini .side-nav__icon {
    transition: color 0.2s ease, transform 0.26s cubic-bezier(0.2, 0.8, 0.2, 1) 0.18s;
  }
  .app-rail--mini .ui-wallet-mount .avatar-tandem {
    transition: transform 0.26s cubic-bezier(0.2, 0.8, 0.2, 1) 0.18s;
  }
  .app-rail--mini:hover :is(.search-trigger__icon, .side-nav__icon, .ui-wallet-mount .avatar-tandem),
  .app-rail--mini:focus-within :is(.search-trigger__icon, .side-nav__icon, .ui-wallet-mount .avatar-tandem) {
    transition-delay: 0.09s;
  }
  .app-rail--mini:not(:hover):not(:focus-within) .search-trigger__icon,
  .app-rail--mini:not(:hover):not(:focus-within) .ui-wallet-mount .avatar-tandem {
    transform: translateX(3px);
  }
  .app-rail--mini:not(:hover):not(:focus-within) .side-nav__icon {
    transform: translateX(-2px);
  }

  /* Org switcher card — centred collapsed (flex-start centres it; its 8px padding +
     32 avatar sit dead-centre in the 48 box). */
  .app-rail--mini:not(:hover):not(:focus-within) .dropdown__trigger--card {
    justify-content: center;
    gap: 0;
    width: 100%;
    height: var(--space-48);
    min-width: 0;
    padding: var(--space-8);
    border-radius: var(--space-12);
  }
  /* SMOOTH expand — grow height/padding/radius in lockstep with the width (CSS
     can't transition to auto, so the expanded height is explicit = 58). */
  .app-rail--mini .dropdown__trigger--card {
    transition: background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease,
                height 0.26s cubic-bezier(0.2, 0.8, 0.2, 1) 0.18s,
                padding 0.26s cubic-bezier(0.2, 0.8, 0.2, 1) 0.18s,
                border-radius 0.26s cubic-bezier(0.2, 0.8, 0.2, 1) 0.18s;
  }
  .app-rail--mini:hover .dropdown__trigger--card,
  .app-rail--mini:focus-within .dropdown__trigger--card {
    height: calc(var(--space-32) + var(--space-24) + 2px);   /* 58 — natural expanded */
    transition: background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease,
                height 0.26s cubic-bezier(0.2, 0.8, 0.2, 1) 0.09s,
                padding 0.26s cubic-bezier(0.2, 0.8, 0.2, 1) 0.09s,
                border-radius 0.26s cubic-bezier(0.2, 0.8, 0.2, 1) 0.09s;
  }

  /* Account (.ui-wallet-mount, shared) — chrome animates away → bare avatar; the
     list-item stays flex in both states (no grid→flex snap) and its padding
     TRANSITIONS (0→~13) so the avatar glides to the card lead; body stays nowrap
     so it can't wrap-and-bounce the bottom-pinned footer. */
  .app-rail--mini:not(:hover):not(:focus-within) .ui-wallet-mount {
    min-height: 86px;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .app-rail--mini:not(:hover):not(:focus-within) .ui-wallet-mount .list-item {
    width: var(--space-48);
    height: var(--space-48);
    padding: 0;
    background: transparent;
    border-color: transparent;
    box-shadow: none;
  }
  .app-rail--mini .ui-wallet-mount .list-item {
    display: flex;
    align-items: center;
    transition: background 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease,
                padding 0.26s cubic-bezier(0.2, 0.8, 0.2, 1) 0.18s;
  }
  .app-rail--mini:hover .ui-wallet-mount .list-item,
  .app-rail--mini:focus-within .ui-wallet-mount .list-item {
    transition-delay: 0.09s;
  }
  .app-rail--mini .ui-wallet-mount .list-item__body {
    flex: 1 1 auto;
    white-space: nowrap;
  }
  .app-rail--mini:not(:hover):not(:focus-within) .ui-wallet-mount .list-item__body,
  .app-rail--mini:not(:hover):not(:focus-within) .ui-wallet-mount .list-item__trail {
    display: none;
  }
}

/* Head — brand lockup (left) + top-action cluster (right). */
.app-rail__head {
  flex: 0 0 auto;
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-12);
  padding: 0 var(--space-4);
}

/* Screen — one view (root or a drill-in sub-panel). Only .is-active shows;
   it fills the remaining height and scrolls its own content. */
.app-rail__screen { display: none; }
.app-rail__screen.is-active {
  display: flex;
  flex-direction: column;
  gap: var(--space-12);
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
}

/* Section — a group of rows under an eyebrow caption (card-gap ladder). */
.app-rail__section {
  --card-gap: var(--gap-card-sm);
  display: flex;
  flex-direction: column;
  gap: var(--card-gap);
}
.app-rail__section + .app-rail__section {
  margin-top: var(--space-8);
  padding-top: var(--space-8);
}
.app-rail__section-title {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: color-mix(in oklch, var(--text-primary) 38%, transparent);
  padding: var(--space-8) var(--space-12) var(--space-4);
}

/* Footer — pinned action/identity row; root screen only. */
.app-rail__footer { flex: 0 0 auto; }
.app-rail:not([data-screen="root"]) .app-rail__footer { display: none; }

/* Right rail — reserved scaffold; hidden until a consumer opts in via
   .app-shell[data-rightrail="on"]. Mirror geometry of the left rail. */
.app-rightrail {
  position: fixed;
  right: var(--rail-left);
  top: var(--canvas-top);
  bottom: var(--canvas-bottom);
  width: var(--rail-width);
  display: none;
  flex-direction: column;
  border-radius: 0 var(--space-16) var(--space-16) var(--space-16);
  z-index: 100;
  overflow-x: hidden;
}
.app-shell[data-rightrail="on"] .app-rightrail { display: flex; }
.app-shell[data-rightrail="on"] .app-canvas {
  padding-right: calc(var(--rail-width) + var(--rail-left) + var(--rail-gap));
}


/* ══════════════════════════════════════════════════════════════════════
   DRAWER HANDLE — .drawer-handle (universal)
   ══════════════════════════════════════════════════════════════════════
   Drawer-pull tab for any side-docked panel — playground rail today;
   future FAB drawer, side sheet, settings drawer use the same shape.
   Glass surface matches --surface-panel-glass so the handle reads as
   an extension of the panel it docks against.

   Anatomy:
     28 × 72 px · square-left rounded-right silhouette · glass surface
     · 16 px chevron · color-shift on hover · focus ring on focus-visible

   Markup:
     <button class="drawer-handle" aria-label="Toggle drawer">
       <svg class="drawer-handle__chevron" …><path d="m15 18-6-6 6-6"/></svg>
     </button>

   The primitive owns CHROME only — size, surface, border-radius, focus,
   hover, chevron sizing/transition. Consumer owns POSITIONING (where it
   sits on the page) and STATE BINDING (which class flips the chevron
   for collapsed/expanded).

   Example consumer wiring (the playground rail uses this shape):
     #playgroundRailHandle {
       position: fixed;
       top: var(--canvas-top);
       left: calc(var(--rail-left) + var(--rail-width));
       z-index: 99;
       transform: translateX(var(--rail-tx));
       transition: transform 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
     }
     body.rail-collapsed #playgroundRailHandle .drawer-handle__chevron {
       transform: rotate(180deg);
     }

   May 2026 v15 — promoted from .ui-rail-handle (tier-3 product-only)
   to universal. Same playbook as v8/v9/v10/v11/v13/v14: when a primitive
   shows up outside its original tier, the prefix drops. Positioning was
   intentionally NOT promoted with chrome — every drawer docks somewhere
   different (left edge, right edge, bottom sheet). The chrome is the
   universal contract.
*/
.drawer-handle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--space-28);
  height: var(--space-72);
  background: var(--surface-panel-glass);
  box-shadow: var(--shadow-panel-glass);
  backdrop-filter: var(--backdrop-panel-glass);
  -webkit-backdrop-filter: var(--backdrop-panel-glass);
  border: none;
  /* Square left edge (joins the parent panel), rounded right edge
     (drawer-pull silhouette). Mirror these radii (round-left / square-
     right) for a right-edge-docked drawer. */
  border-radius: 0 var(--space-12) var(--space-12) 0;
  color: var(--text-secondary);
  cursor: pointer;
  font-family: var(--font-sans);                      /* defensive — <button> UA default is Arial; render brand Figtree if ever given a text label / ::after glyph */
  transition: color 0.18s ease;
}
.drawer-handle:hover { color: var(--text-primary); }
.drawer-handle:focus-visible {
  outline: 2px solid var(--accent-primary);
  outline-offset: 2px;
}
.drawer-handle__chevron {
  width: var(--space-16);
  height: var(--space-16);
  flex: 0 0 auto;
  color: inherit;
  /* Rotation is consumer-applied — see comment above. */
  transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
}


/* ══════════════════════════════════════════════════════════════════════
   VARIANTS RAIL — .ds-doc-variants-rail / .ds-doc-variant-card (May 2026 v17)
   ══════════════════════════════════════════════════════════════════════
   Migration note (May 2026): originally authored as `.ds-variants-rail`
   and `.ds-variant-card` (v17). Renamed to the canonical tier-1
   `.ds-doc-*` prefix in the May 2026 atomic-retirement sweep —
   surfaced by the nightly DS audit (B2 check). Every consumer (CSS,
   playground, catalog markup + Rules-pane prose, JS event delegation,
   skill writing-style + design-feedback spec) migrated in one commit
   per the atomic-retirement contract. No aliases.
   ══════════════════════════════════════════════════════════════════════
   Workshop tooling for design feedback sessions. A right-docked glass
   panel that hosts a list of `.ds-doc-variant-card` items — each one a
   labeled option that swaps the live widget on click. Used to A/B
   component variants in front of designers, PMs, and engineers without
   leaving the playground.

   Composition contract — same shape as `.playground-rail`:
     .panel.ds-doc-variants-rail               glass panel, slides in from right
     .ds-doc-variants-rail__head               heading row at top of rail
     .ds-doc-variants-rail__title              h2 — variant set name
     .ds-doc-variants-rail__caption            short paragraph — what's being tested
     .ds-doc-variants-rail__group              eyebrow + cards group
     .ds-doc-variants-rail__group-label        uppercase mono eyebrow
     .ds-doc-variants-rail__cards              column of .ds-doc-variant-card items
     .ds-doc-variant-card                      one option (label + caption + chip)
     .ds-doc-variant-card__label               h3 — option name (designer-scannable)
     .ds-doc-variant-card__caption             one-line description
     .ds-doc-variant-card__chip                class chip footer (data-variant flag)
     .ds-doc-variant-card.is-active            currently applied option

   Positioning is consumer-owned (same contract as `.drawer-handle`).
   The consumer fixed-positions the rail, sets its width, owns the
   --rail-tx-style slide-in transform, and wires the `.drawer-handle`
   to its left edge. See `swap.css → .ds-doc-variants-rail consumer wiring`
   for the playground binding.

   Card primitive: the canonical UI-card recipe (--surface-card +
   --elev-1 + 1 px transparent border that flips to accent on .is-active),
   parallel to `.theme-card` but slimmer — no swatch strip, no mockup
   lines, just label + caption + class-chip. Workshop-ready: write the
   option's name + 1-line "why" once, the card is done.
*/

.ds-doc-variants-rail {
  /* Surface chrome flows from `.panel` (composed in markup). The rail adds
     only its specific geometry; consumer adds position + width + transform. */
  display: flex;
  flex-direction: column;
  gap: var(--space-24);
  /* Mirror the left rail's squared corner where the drawer-handle docks —
     here the handle docks on the LEFT edge, so the top-left corner is
     squared and the other three are rounded. */
  border-radius: 0 var(--space-16) var(--space-16) var(--space-16);
  overflow-x: hidden;
}
.ds-doc-variants-rail__head {
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
}
.ds-doc-variants-rail__title {
  font-size: var(--text-h3);
  font-weight: var(--fw-bold);
  color: var(--text-primary);
  letter-spacing: var(--tracking-snug);
  line-height: 1.2;
  margin: 0;
}
.ds-doc-variants-rail__caption {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  line-height: var(--lh-sm, 1.45);
  margin: 0;
}
/* Brand segmented control — sits at the bottom of the rail head as a
   sticky workshop switcher. Composes the canonical .seg + .seg-sm tier;
   stretches to fill the rail width with two equal-width tabs. The seg
   gains a slim top margin to separate from the caption while staying
   inside the head's flex column rhythm. */
.ds-doc-variants-rail__brand-seg {
  width: 100%;
  margin-top: var(--space-4);
}
.ds-doc-variants-rail__brand-seg .seg-item {
  flex: 1 1 0;
}
.ds-doc-variants-rail__group {
  display: flex;
  flex-direction: column;
  gap: var(--space-12);
}
/* Re-assert hidden — display:flex above beats the UA [hidden] rule, so
   the Design Workshop's per-brand visibility toggle (data-brand-only +
   hidden attribute) needs this explicit override. See CLAUDE.md memory
   feedback_hidden-overridden-by-display. */
.ds-doc-variants-rail__group[hidden] { display: none; }
.ds-doc-variants-rail__group-label {
  font-size: var(--text-caption);
  font-weight: var(--fw-semibold);
  color: var(--text-faint);
  letter-spacing: var(--tracking-loose);
  text-transform: uppercase;
  font-family: var(--font-mono);
  margin: 0;
}
.ds-doc-variants-rail__cards {
  --card-gap: var(--gap-card-md);
  display: flex;
  flex-direction: column;
  gap: var(--card-gap);
}

/* ── .ds-doc-variant-card — one option in the rail ───────────────────────────
   Grid layout (May 2026 v2 update): three columns — marker · body ·
   toggle. The body column hosts a TITLE (semibold), a SUBTITLE
   (1-line summary, sm secondary), and an optional DESCRIPTION
   paragraph hidden by default and revealed when .is-expanded is set
   on the card (chevron toggle).

   Compact-by-default — title + subtitle is enough for most workshop
   comparisons; the description expands for designers who want more
   context without leaving the rail.

   Anatomy:
     .ds-doc-variant-card                          <button> click target
       .marker.marker-lg                       leading anchor — required
       .ds-doc-variant-card__body                  body column wrapper
         .ds-doc-variant-card__title               option name (body, 600)
         .ds-doc-variant-card__subtitle            1-line elaboration (sm)
         .ds-doc-variant-card__description         opt-in paragraph (display:none default)
       .ds-doc-variant-card__toggle                chevron toggle (.btn-neutral .btn-xs etc)

   Active state: marker tone swaps from --text-secondary (alpha) to
   --accent-primary (tinted); title color flips to --accent-primary.
   Expand state (.is-expanded): description shows, chevron rotates 180°.

   The toggle composes the canonical .btn-icon.btn-neutral.btn-
   circle.btn-xs button recipe via class composition (static by default
   since the May 2026 inversion — no modifier needed for the no-lift
   feel); the .ds-doc-variant-card__toggle class only adds the grid-cell
   alignment and the rotation hook. */
.ds-doc-variant-card {
  /* Canonical UI-card recipe. Same surface + active-state shape as
     `.theme-card` — see styles.css §"Canonical UI card recipe" comment. */
  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: start;
  gap: var(--space-12);
  padding: var(--space-16);
  margin: 0;
  background: var(--surface-card);
  border: 1px solid transparent;
  border-radius: var(--card-radius);
  color: var(--text-primary);
  /* Button elements reset font-family to user-agent default (Arial in
     most browsers) and don't inherit through. Set the brand font
     explicitly so all body descendants resolve to Figtree (this card's
     <button> drawer toggle is the button-rooted case). */
  font-family: var(--font-sans);
  box-shadow: var(--elev-1);
  cursor: pointer;
  text-align: left;
  /* Match the canonical button transition list (hover bg/border/shadow
     fade at 0.2s ease) so the card hover feel matches the system. */
  transition: background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
}
.ds-doc-variant-card:hover {
  background: color-mix(in oklch, var(--accent-primary) 3%, var(--surface-card));
  box-shadow: var(--elev-2);
}
.ds-doc-variant-card:active,
.ds-doc-variant-card.is-active {
  background: color-mix(in oklch, var(--accent-primary) 4%, var(--surface-card));
  border-color: color-mix(in oklch, var(--accent-primary) 64%, transparent);
  box-shadow: var(--elev-2);
}
.ds-doc-variant-card:focus-visible {
  outline: 2px solid var(--accent-primary);
  outline-offset: 2px;
}

/* Body column — vertical stack of title + subtitle + optional description */
.ds-doc-variant-card__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;
}

/* Title — primary heading of the variant; short noun phrase.
   Color stays --text-primary regardless of selection state. The
   active-state signal is carried by the chrome (accent rim + 4 %
   bg-mix + elev-2 shadow) and by the marker tone swap (alpha →
   tinted) — that's enough. Adding a title color flip layers a
   fourth signal on top, which makes the active state read as
   over-decorated, AND breaks the canonical UI-card recipe rule
   (CLAUDE.md → "Canonical UI card recipe": active state is chrome
   only, never a content-typography shift). */
.ds-doc-variant-card__title {
  font-size: var(--text-body);
  font-weight: var(--fw-semibold);
  color: var(--text-primary);
  letter-spacing: var(--tracking-snug);
  line-height: 1.3;
  margin: 0;
}

/* Subtitle — 1-line elaboration of the title; succinct. Typography
   tokens per CLAUDE.md "Typography tokens — every type property
   reaches for a variable, never a literal" — including the regular
   weight (even though 400 is the inherited default, the token form
   reads as intent). */
.ds-doc-variant-card__subtitle {
  font-size: var(--text-sm);
  font-weight: var(--fw-regular);
  color: var(--text-secondary);
  line-height: var(--lh-sm);
  margin: 0;
}

/* Description — opt-in paragraph, hidden by default. The chevron
   toggle (data-action="variant-toggle") flips .is-expanded on the
   card, which reveals this element. Sits below the subtitle in the
   body column.

   Ink intensity is one step QUIETER than the subtitle (--text-muted
   vs. --text-secondary) — same typography otherwise. This creates a
   clean three-tier ink hierarchy across the body:

     Title       → --text-primary    (full ink, the heading)
     Subtitle    → --text-secondary  (medium, paired with title)
     Description → --text-muted      (quietest, opt-in detail)

   The description is supporting context the designer explicitly
   opted into by expanding the chevron — fainter ink reads as
   "more detail if you want it" without competing with the
   always-visible title + subtitle pair. */
.ds-doc-variant-card__description {
  font-size: var(--text-sm);
  font-weight: var(--fw-regular);
  color: var(--text-muted);
  /* Unitless 1.55 — no --lh-* token matches; the closest pair is
     --lh-sm (1.43 ratio, too tight for a paragraph) and --lh-body
     (1.71 ratio, too loose). 1.55 falls inside the acceptable 1.1-1.6
     range per the rule. */
  line-height: 1.55;
  margin: var(--space-12) 0 0;
  display: none;
}
.ds-doc-variant-card.is-expanded .ds-doc-variant-card__description {
  display: block;
}

/* Chevron toggle — composes .btn-icon.btn-neutral.btn-circle.btn-
   xs for chrome (static by default since the May 2026 inversion). This
   rule only handles grid-cell alignment and the rotation hook. The
   standalone `rotate:` property
   composes with the button's existing transitions (per CLAUDE.md
   "Use standalone rotate: for stateful icon toggles").

   Vertical alignment — the chevron's 24 px body centers vertically
   with the marker's 40 px tile via margin-top: (40 − 24) / 2 = 8 px.
   Both icons sit on the same horizontal centerline; the body column
   between them is top-aligned so title + subtitle stack from the
   top regardless of how much description content expands below. */
.ds-doc-variant-card__toggle {
  align-self: start;
  margin-top: var(--space-8);
}
.ds-doc-variant-card.is-expanded .ds-doc-variant-card__toggle {
  rotate: 180deg;
}

/* Tone-slot recipe for the leading marker. Both states stay in the
   marker's natural TINTED fill mode (14 % wash + tone-colored char);
   the contrast between active and inactive is HUE, not fill density.

     Inactive → ALPHA tone (--text-secondary) — quiet neutral wash
                so the row reads as a calm sequence anchor.
     Active   → TINTED tone (--accent-primary) — brand-accent wash
                so the selected row carries the brand identity.

   Same recipe shape — two tone-slot overrides — so palette swaps
   cascade automatically. Solid fill (.marker--solid behavior) is
   reserved for hero / deck anchors where the marker IS the headline
   element; the variant-card recipe doesn't reach for it because the
   accent rim + title color shift already carry the selection signal,
   and a solid fill would compete with both. Specificity: inactive
   rule (0,2,0); active rule (0,3,0). */
.ds-doc-variant-card > .marker {
  --marker-tone: var(--text-secondary);
}
.ds-doc-variant-card.is-active > .marker {
  --marker-tone: var(--accent-primary);
}


/* 7. Panel Button Row — standardized button layout for panel footers.
   Uses the standard .btn-primary / .btn-neutral at .btn-sm size.
   .ds-doc-panel-btn-row provides the flex container.
   Gap uses --cluster-sm (8px) — one step above the original --cluster-xs
   (4px, which read as bonded) while staying tight enough for a 280px
   panel footer. Primary and secondary actions read as separate targets
   without feeling disjoint. */
.ds-doc-panel-btn-row {
  display: flex; gap: var(--cluster-sm); margin-top: var(--space-8);
}
.ds-doc-panel-btn-row > * { flex: 1; }

/* 8. Section Label — uppercase mono grouping header */
.ds-doc-section-label {
  font-size: var(--text-caption); font-weight: var(--fw-semibold); color: var(--text-faint);
  letter-spacing: var(--tracking-loose); text-transform: uppercase;
  font-family: var(--font-mono, 'Geist Mono', monospace);
  margin: var(--space-16) 0 var(--space-8);
}

/* Light mode — adjust track empty portion + divider + toggle */
[data-theme="light"] .divider {
  background: color-mix(in oklch, var(--text-primary) 8%, transparent);
}
/* ── 9. Modal — .modal ────────────────────────────────────
   Overlay + centered shell that hosts a screen-header on top, a
   scrollable body in the middle, and an optional action-bar at
   the bottom. Universal primitive — used on marketing AND product
   surfaces. May 2026 v13 rebuilt from a self-contained head/title/
   close/foot composition to a SHELL that composes the canonical
   `.screen-header` and `.action-bar` primitives. May 2026 follow-up:
   the `.modal` shell itself now composes `.panel` (markup-level) —
   the floating-tier "panel look" (--surface-raised + --elev-3 +
   --space-16 radius + --space-24 padding + 12 px internal flex gap)
   that the playground rail and the swap widget already share. The
   `.modal` class itself owns ONLY constraints (size, max-height,
   slide-up animation, positioning context). Markup MUST include
   both classes: <div class="modal panel">.

   Anatomy (canonical):
     <div class="modal-overlay">                        ← scrim + centering
       <div class="modal panel" role="dialog" aria-modal="true">
         <header class="screen-header screen-header--main">
           <h2 class="screen-header__title">Confirm action</h2>
           <div class="screen-header__trailing">
             <button class="btn-icon btn-borderless btn-circle"
                     aria-label="Close">…X…</button>
           </div>
         </header>
         <div class="modal-body">
           <p class="modal-desc">Optional descriptive copy.</p>
           …body content (forms, lists, anything)…
         </div>
         <div class="action-bar">                       ← optional footer
           <button class="btn-secondary">Cancel</button>
           <button class="btn-primary">Confirm</button>
         </div>
       </div>
     </div>

   Header variants (from .screen-header):
     .screen-header--main         single-screen modal (title left, no back)
     (default centered)            multi-screen modal sub-view (back returns to Main)

   Footer variants (from .action-bar):
     .action-bar                   horizontal, equal-width — confirm/cancel
     .action-bar.action-bar--stack vertical, primary on top — for long labels

   Headerless variant — true shell experience:
     The .screen-header is OPTIONAL. Omit it when the modal's content
     speaks for itself (a media preview, a CTA tile, a marketing
     announcement, a fully-custom layout). In that case, the dismiss
     affordance moves to a FLOATING button positioned above the
     modal's top-right corner — see .modal-close-float below.

       <div class="modal-overlay">
         <div class="modal panel">
           <button class="modal-close-float btn-icon btn-borderless
                          btn-circle btn-neutral btn-neutral-light"
                   aria-label="Close">…X…</button>
           <div class="modal-body">…fully-custom content…</div>
           <div class="action-bar">…optional…</div>
         </div>
       </div>

   Sizes (sm / default / lg — rungs of the --w-* width ladder, even 96px gaps):
     .modal--sm                   --w-6  (384 px)  confirm dialog, single-question
     (default)                    --w-9  (480 px)  forms, settings
     .modal--lg                   --w-12 (576 px)  multi-column / dense / wide forms
   Very wide workshop modals (palette export 720, chart playground 1080) stay
   OFF-ladder — they inline max-width with a code-comment (see the --w-* rules).

   The v13 legacy slot classes (.modal-head / .modal-title /
   .modal-close / .modal-foot / .modal-foot--right / .modal-foot--split
   / .modal-copy / .modal-copy-status) were RETIRED (June 2026) once
   both in-tree consumers migrated to the .screen-header + .action-bar
   shell above: the export-palette modal (shared.js) and the Chart
   Playground modal (design-system/*.html). No aliases — all modal
   markup now uses the canonical shell.
   ─────────────────────────────────────────────────────────── */
.modal-overlay {
  position: fixed; inset: 0; z-index: 10000;
  background: oklch(0% 0 0 / 0.6);
  display: flex; align-items: center; justify-content: center;
  padding: 24px;
  backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
  animation: modal-fade-in 0.2s ease-out;
}
/* Ensure the HTML `hidden` attribute actually hides the overlay —
   the default `display: none` from `hidden` is beaten by our
   `display: flex` rule above, so we re-assert it explicitly. */
.modal-overlay[hidden] { display: none; }
@keyframes modal-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes modal-slide-up {
  from { opacity: 0; transform: translateY(12px) scale(0.97); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}

/* Shell — composes .panel for surface + shadow + radius + padding + internal flex.
   The .modal class itself owns ONLY modal-specific constraints (size, max-
   height, slide-up animation, positioning context for .modal-close-float).
   Markup MUST include both classes: <div class="modal panel">…</div>.
   Why composition instead of duplicate CSS: the playground rail uses
   <aside class="panel playground-rail">, the swap widget uses
   <article class="panel ui-card">. Same idiom for every panel-tier
   surface — modal joins the family. Zero drift risk between modal and
   panel because they consume the same tokens via the same rule. */
.modal {
  position: relative;            /* containing block for .modal-close-float */
  width: 100%; max-width: var(--w-9); max-height: 80vh;
  animation: modal-slide-up 0.25s ease-out;
  /* Auto-resize (step transitions) — when a flow modal swaps its content
     between steps (the integration / wallet-connect wizards do this via
     flow.innerHTML = …), the panel's natural height changes in one frame and
     would snap. shared.js → LifiModalResize FLIP-tweens it: it pins the OLD
     height, reflows, then transitions to the NEW height (the rule below, gated
     on [data-modal-resizing]) and reverts to `auto` on transitionend. These two
     slots own the timing + curve — the JS only orchestrates, so corners /
     spacing-style theming could drive them later. HEIGHT AXIS ONLY: width is
     owned by the --w-* size modifier and never changes across a flow. Reduced
     motion → the JS skips the tween (snaps); the media guard below is
     defensive. Spec: design-system #modal-stepped + feedback.md → Modal. */
  --modal-resize-duration: 0.28s;
  --modal-resize-ease: cubic-bezier(0.4, 0, 0.2, 1);
}
.modal--sm { max-width: var(--w-6); padding: var(--space-20); }
.modal--lg { max-width: var(--w-12); }

/* The resizing state — set by LifiModalResize for the duration of ONE tween.
   overflow:hidden clips the taller incoming content on a GROW so it reveals as
   the panel expands (instead of spilling past the rounded edge); freezing the
   body's own scroll stops a scrollbar flashing mid-tween. Caveat: a modal that
   uses .modal-close-float AND swaps content would clip the float during the
   tween — but stepper flows dismiss via a header/footer button, never the
   float, so the two never coincide today (documented contract, not a trap). */
.modal[data-modal-resizing] {
  overflow: hidden;
  transition: height var(--modal-resize-duration) var(--modal-resize-ease);
}
.modal[data-modal-resizing] .modal-body { overflow: hidden; }
@media (prefers-reduced-motion: reduce) {
  .modal[data-modal-resizing] { transition: none; }
}

/* Body — scrollable when content overflows.
   Canonical slot; new code uses this. */
.modal-body {
  flex: 1; overflow-y: auto; min-height: 0;
  display: flex; flex-direction: column; gap: var(--cluster-sm);
}
.modal-desc {
  font-size: var(--text-sm); color: var(--text-muted); line-height: var(--lh-sm);
}
.modal-desc code {
  font-family: var(--font-mono, monospace); font-size: var(--text-caption);
  background: var(--overlay-press); padding: 0 var(--space-4); border-radius: var(--space-4);
}

/* Divider — optional separator between body and action-bar.
   Canonical slot; reach for it when the action-bar carries
   destructive/irreversible actions and you want a visible boundary
   between body content and the action region. */
.modal-divider {
  height: 1px; background: var(--border-subtle); margin: var(--space-4) 0;
}

/* ── .modal-close-float — floating close affordance for headerless modals ─
   When a modal omits the `.screen-header` to become a "true shell"
   (body + optional action-bar only), the dismiss affordance moves
   to a FLOATING button positioned above the modal's top-right
   corner — anchored to the modal so it tracks the panel, not the
   viewport.

   Markup (composed from existing button primitives):
     <button class="modal-close-float btn-icon btn-borderless
                    btn-circle btn-neutral btn-neutral-light"
             aria-label="Close">
       <svg>…X…</svg>
     </button>

   Why `.btn-neutral-light` (forced light) and not bare `.btn-neutral`:
   the scrim is ALWAYS dark (oklch(0% 0 0 / 0.6) — 60% black, the
   same on every theme). Bare .btn-neutral is page-theme-adaptive: in
   light mode it would render as a dark overlay sitting on the dark
   scrim — dark-on-dark, illegible. The forced-light variant uses
   white overlay regardless of theme, which is the right pairing
   for the scrim context.

   The .modal-overlay :has() rule below bumps the overlay's top
   padding when this button is present, so the button can't be
   clipped by the overlay's edge on short viewports. */
.modal-close-float {
  position: absolute;
  /* 40 px button height + 12 px gap = 52 px above the modal's top edge. */
  top: calc(-1 * (var(--space-40) + var(--space-12)));
  right: 0;
  z-index: 1;                    /* above modal's isolation context */
}

/* Headerless-modal viewport clearance — when a .modal-close-float
   is present in the overlay, give the overlay enough top padding
   to host the button without clipping on short viewports.
   Calculation: 40 (button) + 12 (gap to modal) + 24 (overlay's own
   breathing room above the button) = 76 px. */
.modal-overlay:has(.modal-close-float) {
  padding-top: calc(var(--space-40) + var(--space-12) + var(--space-24));
}

/* Common content slots */
.modal-textarea {
  flex: 1; min-height: 280px;
  background: var(--overlay-press); color: var(--text-primary);
  border: 1px solid var(--border-subtle); border-radius: 8px;
  padding: 12px; font: 500 11px var(--font-mono, monospace);
  line-height: var(--lh-body); resize: vertical;
}
.modal-input {
  width: 100%; padding: var(--space-12) var(--space-12); border-radius: var(--space-12);
  background: var(--overlay-hover); border: 1.5px solid var(--border-subtle);
  color: var(--text-primary); font-weight: var(--fw-medium); font-size: 13px; font-family: var(--font-sans);
  outline: none; transition: border-color 0.2s;
}
.modal-input:focus { border-color: var(--accent-primary); }
.modal-input::placeholder { color: var(--text-faint); }

/* Responsive */
@media (max-width: 480px) {
  .modal-overlay { padding: var(--space-16); }
  .modal { padding: var(--space-20); border-radius: var(--space-12); max-height: 90vh; }
}

/* ── .lightbox — enlarge + navigate a gallery of assets ──────────────────
   Composes .modal-overlay (the scrim: fixed inset, dark + blur, fade-in, the
   `[hidden]` re-assert) for the backdrop; adds a centered viewer (enlarged
   asset + caption) plus absolutely-positioned close + prev/next chrome that
   REUSES .btn-icon. Generic: any [data-lightbox] gallery opts in
   (shared.js → initLightbox) — its items are [data-lightbox-item] (or its
   direct <figure> children), and the enlarged node is each item's first
   <svg>/<img>, cloned (so self-contained CSS-keyframe animations keep
   looping). Navigate with the arrows, ←/→ keys, the scroll wheel, or swipe;
   ✕ / Esc / scrim-click closes. tier-2 universal — built for the catalog
   asset libraries (iso illustrations first) but usable on any gallery.
   The chrome is white on the fixed-dark scrim regardless of page theme,
   so its colour is pinned here (not theme-driven) — same intent as
   navbar-on-glass chrome. */
.lightbox { cursor: zoom-out; }            /* clicking the bare scrim closes */
.lightbox__viewer {
  display: flex; flex-direction: column; align-items: center; gap: var(--space-20);
  max-width: min(92vw, 960px); margin: 0; cursor: default;
}
.lightbox__frame {
  display: flex; align-items: center; justify-content: center;
  padding: var(--space-32);
  border-radius: var(--card-radius);
  background: var(--surface-card);
  box-shadow: var(--elev-3);
}
.lightbox[data-frame="ink"] .lightbox__frame { background: var(--text-primary); }  /* white-on-ink assets (iso) */
.lightbox__frame > svg,
.lightbox__frame > img {
  display: block; overflow: visible;
  width: min(72vmin, 620px); height: auto; max-height: 64vh;
}
.lightbox__cap {
  display: flex; flex-direction: column; align-items: center; gap: var(--space-2);
  text-align: center;
}
.lightbox__name { font-size: var(--text-body); font-weight: var(--fw-semibold); color: oklch(100% 0 0 / 0.92); }
.lightbox__meta {
  font-family: var(--font-mono); font-size: var(--text-caption);
  letter-spacing: var(--tracking-snug); color: oklch(100% 0 0 / 0.55);
}
.lightbox__counter {
  font-family: var(--font-mono); font-size: var(--text-caption);
  color: oklch(100% 0 0 / 0.4); margin-top: var(--space-4);
}
/* Chrome — close + prev/next, absolute to the scrim, white regardless of theme.
   A visible translucent rounded base at REST (not just on hover) so the arrows
   read as buttons, not bare glyphs (these compose .btn-borderless, which is
   transparent until hover — overridden here). */
.lightbox .lightbox__close,
.lightbox .lightbox__nav {
  position: absolute; z-index: 1;
  color: oklch(100% 0 0 / 0.92);
  background: oklch(100% 0 0 / 0.1);
}
.lightbox .lightbox__close:hover,  .lightbox .lightbox__nav:hover,
.lightbox .lightbox__close:active, .lightbox .lightbox__nav:active {
  background: oklch(100% 0 0 / 0.22); color: #fff;
}
.lightbox__close { top: var(--space-20); right: var(--space-20); }
.lightbox__nav { top: 50%; }
/* Centre the side arrows with transform — and RE-ASSERT it through hover/active.
   .btn-borderless:hover/:active set `transform: none`, which would wipe the
   translateY(-50%) centring and drop the button half its height on hover
   (the "jump"). Re-stating it here (later in source, equal specificity) wins. */
.lightbox .lightbox__nav,
.lightbox .lightbox__nav:hover,
.lightbox .lightbox__nav:active { transform: translateY(-50%); }
.lightbox__nav--prev { left: var(--space-16); }
.lightbox__nav--next { right: var(--space-16); }
.lightbox[data-single] .lightbox__nav { display: none; }   /* one item → no arrows */

/* Gallery item — the clickable affordance the controller adds to each tile */
.is-lightbox-item { cursor: zoom-in; }
.is-lightbox-item:focus-visible {
  outline: 2px solid var(--accent-primary); outline-offset: 2px;
}

@media (max-width: 480px) {
  .lightbox__frame { padding: var(--space-16); }
  .lightbox__frame > svg, .lightbox__frame > img { width: min(82vmin, 420px); }
  .lightbox__nav--prev { left: var(--space-4); }
  .lightbox__nav--next { right: var(--space-4); }
}
@media (prefers-reduced-motion: reduce) {
  .lightbox { animation: none; }
}

/* ── .action-bar — bottom-action region for modals, drawers, sheets ───
   Row of action buttons at the bottom of a container surface. Default
   is equal-width horizontal — each child gets `flex: 1` so the row
   fills its container and both (or N) buttons share the width evenly.
   That's the iOS / widget pattern, and it reads cleanly inside narrow
   modal panels where right-aligned content-width buttons look adrift.

   `.action-bar--stack` flips to vertical, full-width buttons, primary
   on top. DOM order = visual order — author writes primary first.

   Default (horizontal, equal-width):
     <div class="action-bar">
       <button class="btn-secondary">Cancel</button>
       <button class="btn-primary">Confirm</button>
     </div>

   Stacked (vertical, full-width, primary first):
     <div class="action-bar action-bar--stack">
       <button class="btn-primary">Continue</button>
       <button class="btn-secondary">Not now</button>
     </div>

   Single-button works in either mode — fills the full row / stack:
     <div class="action-bar">
       <button class="btn-primary">OK</button>
     </div>

   Universal primitive (May 2026 v13). Initial consumer is `.modal`;
   drawers, bottom sheets, side panels, full-page flows all share the
   same shape. Margin above the bar is the container's job — the bar
   itself owns only its internal layout and gap. */
.action-bar {
  display: flex;
  width: 100%;
  align-items: stretch;
  gap: var(--cluster-sm);
}
/* Each child fills equal width. `min-width: 0` lets long labels
   truncate via the button's own overflow handling instead of forcing
   one button wider than its siblings. */
.action-bar > * {
  flex: 1 1 0;
  min-width: 0;
}

/* Lead-primary + trailing-affordance shape — an icon-only button keeps its
   square size instead of taking an equal-width share (a 40 px glyph blown
   up to half the row reads as broken). The text button(s) beside it stay
   flex: 1 1 0 and fill the remaining width, so a single .btn-primary becomes
   the lead CTA with the icon pinned at its natural size. Auto-detected off
   the icon-only child — no modifier needed. Canonical consumer: the swap
   widget's action footer (Review + Wallet icon). May 2026 — absorbed the
   retired .widget-stage__actions pattern; see CLAUDE.md → Primitive retirement
   is atomic.
     <div class="action-bar">
       <button class="btn-primary">Review</button>
       <button class="btn-secondary btn-icon" aria-label="Wallet">…</button>
     </div> */
.action-bar > .btn-icon {
  flex: 0 0 auto;
}

/* ── .action-bar--stack — vertical, full-width, primary first ───
   Same gap (--cluster-sm, 8 px) as horizontal — keeps the bar's
   internal rhythm consistent across both orientations. Each child
   takes its own intrinsic height; width is 100 % so the column
   reads as a stack of equal-width buttons. */
.action-bar--stack {
  flex-direction: column;
}
.action-bar--stack > * {
  flex: 0 0 auto;
  width: 100%;
}

/* ── .action-bar--footer — panel-footer separator chrome (May 2026 v16) ──
   Adds the "pinned-to-bottom-of-panel" treatment to the default
   horizontal equal-width layout: a top hairline separating the bar
   from the panel's scroll content, `padding-top` to give the buttons
   air, and `margin-top: auto` so the bar pins to the bottom when the
   parent is a flex column (which most panels are — `.panel` itself is
   `display: flex; flex-direction: column`).

   USAGE CONVENTION (not enforced via CSS): both buttons should be
   equal-weight peers (.btn-secondary, .btn-tertiary, .btn-tertiary
   .btn-neutral, etc.) — no .btn-primary. Use this variant when neither
   action is the clear primary path (Copy code · Read docs, View · Share,
   Export · Import). When one action IS primary, use the bare .action-bar
   default and put the .btn-primary inside.

   The variant doesn't override flex direction or child sizing — those
   stay at the default `row` + `flex: 1 1 0` from `.action-bar`. Stacks
   the modifier with the parent's flex-column behavior, so on a flex
   column parent the bar lands at the bottom; on a non-flex parent the
   `margin-top: auto` is a harmless no-op. */
.action-bar--footer {
  flex: 0 0 auto;
  margin-top: auto;
  padding-top: var(--space-12);
  border-top: 1px solid color-mix(in oklch, var(--text-primary) 8%, transparent);
}

/* .tc-chip retired June 2026 (Theme Composer redesign) → the grouped
   .theme-editor-preview click-to-copy .swatch--xs dots. */

/* ════════════════════════════════════════════════════════════════════
   .alert — inline tone-tinted message banner
   ════════════════════════════════════════════════════════════════════
   Sibling of .notification (the floating toast). Same tone vocabulary
   (info · success · warn · danger), same atomic composition (leading
   .spot-icon mark + content + optional actions + dismiss). Different
   placement contract: alerts sit IN PAGE FLOW — page-level banners,
   widget panels, modal bodies. No animation, no max-width cap, no
   auto-dismiss; full width of its parent container.

   Visually, alerts use a TINTED SURFACE — the whole banner reads as
   the tone, driven by the existing semantic token triplets
   (--{tone}-bg / --{tone}-border / --{tone}-fg from design.md §02
   "Semantic state pairs"). Notifications, by contrast, stay neutral
   (paper material) and let the spot-icon carry the tone alone. The
   difference is deliberate: inline alerts compete with surrounding
   page content for "look here", so the tint earns its keep; floating
   toasts already pop because they're floating, so a neutral surface
   keeps them quiet against any backdrop.

   Anatomy:
     .alert                      outer banner
       .spot-icon.spot-icon--<tone>   leading mark (tone-tinted disc)
       .alert-content              text stack
         .alert-title                headline (text-body, weight 600)
         .alert-message              body (text-sm, text-secondary)
         .alert-list                 optional inline list (bullet · tone)
         .alert-actions              optional action row (buttons)
       .alert-close                  optional top-right close X

   Status tones — surface + border auto-flip light/dark via tokens:
     .alert--default  --accent-primary tone over --info-* triplet (palette-dependent)
     .alert--info     --info tone over --info-* triplet (palette-independent semantic)
     .alert--success  --success-* triplet
     .alert--warn     --warn-* triplet
     .alert--danger   --danger-* triplet

   Size ladder — three tiers on the sm / default / lg pattern:
     .alert--sm   compact single-line — widget panels, modal bodies
     .alert       default — title + body, page-level (full width)
     .alert--lg   hero — title + body + actions, larger spot-icon

   Composition modifiers:
     .alert--no-dismiss   tightens right padding when no close X is used

   When to use vs. siblings:
     · Page banner ("Trial expires in 3 days")        → .alert / --lg
     · Widget message ("Insufficient balance")         → .alert--sm
     · Floating toast ("Route confirmed")              → .notification
     · Form-field error ("Email required")             → .form-error
     · Multi-step status row                           → .status-row

   Spec doc: design/components/feedback.md → Alert.
   ════════════════════════════════════════════════════════════════════ */

.alert {
  /* Tone token routes through --a-tone so any future variant-aware
     state (hover on action button, dismiss-hover tint) consumes the
     right hue (CLAUDE.md → Variant-aware states). Default is info
     when no tone modifier is present. */
  --a-tone: var(--accent-primary);
  --a-bg: var(--info-bg);
  --a-border: var(--info-border);
  --a-fg: var(--info);

  position: relative;
  display: flex;
  align-items: flex-start;
  gap: var(--space-12);
  width: 100%;
  /* Padding + radius standardised to the canonical card recipe (May 2026):
     all tiers reach for --space-* on the spacing ladder, all radii pin
     to --space-16 (matching .ui-amount-card / .panel / canonical UI
     card). Per-tier padding steps +4 (sm 16, default 20, lg 24) so the
     three sizes read clearly as different envelopes — same as the card
     family's own size ladders. The 48 px right reserve persists when a
     dismiss button is present (32 px button + 8 px corner + 8 px
     breathing); .alert--no-dismiss collapses that to the tier's
     uniform padding. */
  padding: var(--space-20) var(--space-48) var(--space-20) var(--space-20);
  background: var(--a-bg);
  /* May 2026 — border dropped. The pastel bg (now at 14 % tone-over-
     transparent, up from 10 %) carries enough identity on its own; the
     1 px border was reading as visual noise against an already-tinted
     surface. The --a-border slot stays defined for any future variant
     that wants to reintroduce a hairline. */
  border: 0;
  border-radius: var(--card-radius);
  color: var(--text-primary);
}
.alert.alert--no-dismiss { padding-right: var(--space-20); }

/* Status tone — surface, border, and the --a-tone slot. The leading
   .spot-icon child consumes its tone via .spot-icon--<tone> directly,
   so both layers are explicit and decoupled (mirrors the .notification
   pattern). */
/* .alert--default = "no tone modifier" — routes through brand
   --accent-primary (palette-dependent) over the info-tinted bg.
   .alert--info = the semantic info role — palette-independent,
   routes through --info so the icon container stays info-blue
   regardless of which brand palette is active.
   (Before May 2026 these shared --accent-primary; the icon
   container silently shifted with the active brand palette.) */
.alert--default { --a-tone: var(--accent-primary); --a-bg: var(--info-bg);    --a-border: var(--info-border);    --a-fg: var(--info); }
.alert--info    { --a-tone: var(--info);           --a-bg: var(--info-bg);    --a-border: var(--info-border);    --a-fg: var(--info); }
.alert--success { --a-tone: var(--success);        --a-bg: var(--success-bg); --a-border: var(--success-border); --a-fg: var(--success); }
.alert--warn    { --a-tone: var(--warn);           --a-bg: var(--warn-bg);    --a-border: var(--warn-border);    --a-fg: var(--warn); }
.alert--danger  { --a-tone: var(--danger);         --a-bg: var(--danger-bg);  --a-border: var(--danger-border);  --a-fg: var(--danger); }

/* Content stack — title + message + (optional) list + (optional) actions */
.alert-content {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.alert-title {
  font-size: var(--text-body);
  font-weight: var(--fw-semibold);
  color: var(--text-primary);
  line-height: var(--lh-body);
  margin: 0;
}
.alert-message {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  line-height: var(--lh-sm);
  margin: 0;
  overflow-wrap: break-word;
}

/* Optional inline list — bullet marker picks up the tone fg so the
   list visually inherits the alert's identity. Padding-left matches
   the canonical list scale. */
.alert-list {
  list-style: disc;
  padding-left: var(--space-20);
  margin: var(--space-8) 0 0;
  color: var(--text-secondary);
}
.alert-list li {
  font-size: var(--text-sm);
  line-height: var(--lh-sm);
}
.alert-list li::marker { color: var(--a-fg); }

.alert-actions {
  margin-top: var(--space-12);
  display: flex;
  gap: var(--space-12);
  align-items: center;
  flex-wrap: wrap;
}

/* Close button — position-only modifier on top of the canonical DS
   button stack `.btn-neutral.btn-icon.btn-borderless.btn-sm
   .btn-circle` (May 2026: migrated from .btn-tertiary so the dismiss
   inherits the alert's tone, not brand-blue). Visuals (size, hover
   fill, focus ring) come from the DS button primitives. The 48px right
   padding on .alert reserves the X's column (32px button + 8px corner
   + 8px breathing). */
.alert-close {
  position: absolute;
  top: var(--space-8);
  right: var(--space-8);
}

/* Semantic-color X — the dismiss icon picks up the alert's own tone
   via --a-tone (set per alert variant above). Info → blue X, success →
   green X, warn → amber X, danger → red X. Neutral alerts (no variant)
   fall back to --text-primary.

   Specificity dance: the upstream `[data-theme="light"] .btn-icon
   .btn-borderless` rule sets color to --text-muted at (0,3,0). To
   override in BOTH themes, we ship two parallel selectors — one bare
   for dark mode (later in source than the base .btn-icon rule, so
   it wins on tied specificity), one [data-theme="light"]-qualified for
   light mode (matches the override's specificity, later in source so it
   wins). Same applies to the hover bg below. */
.alert .alert-close { color: var(--a-tone, var(--text-primary)); }
[data-theme="light"] .alert .alert-close { color: var(--a-tone, var(--text-primary)); }

.alert .alert-close:hover {
  background: color-mix(in oklch, var(--a-tone, var(--text-primary)) 14%, transparent);
  color: var(--a-tone, var(--text-primary));
}
[data-theme="light"] .alert .alert-close:hover {
  background: color-mix(in oklch, var(--a-tone, var(--text-primary)) 14%, transparent);
  color: var(--a-tone, var(--text-primary));
}

/* When there's no dismiss X, the reserved right column is wasted.
   Authors opt in explicitly so the markup contract stays obvious. */
.alert--no-dismiss { padding-right: var(--space-16); }

/* ── --sm — compact single-line for widget panels and modal bodies ── */
.alert--sm {
  /* May 2026 — icon top-anchored (was center). When the message wraps
     to 2+ lines, the spot-icon stays pinned to the first-line height
     instead of drifting to the visual midpoint as the alert grows. For
     single-line content the visual is functionally identical (24 px
     icon + ~20 px text line-box land within a couple pixels of each
     other at the top), but the multi-line case reads correctly. */
  align-items: flex-start;
  /* Padding + radius standardised to the canonical 16 px card recipe
     (--space-16). The 48 px right reserve stays when a dismiss button
     is present (32 px button + 8 px corner + 8 px breathing = the X
     column); .alert--no-dismiss collapses that to a uniform 16 px so
     the alert reads as a flush card surface. */
  padding: var(--space-16) var(--space-48) var(--space-16) var(--space-16);
  border-radius: var(--card-radius);
  gap: var(--space-8);
}
.alert--sm.alert--no-dismiss { padding-right: var(--space-16); }
.alert--sm .spot-icon {
  width: var(--space-24);
  height: var(--space-24);
  border-radius: var(--space-8);
}
.alert--sm .spot-icon svg {
  width: var(--space-16);
  height: var(--space-16);
}
.alert--sm .alert-title { display: none; }
/* Optical-center align — the .spot-icon's 16 px SVG sits at y=24 from
   the alert's top (12 px padding + 12 px to the 24 px icon-box center);
   the message's first-line glyph center sits at y=22 (12 px padding +
   10 px to the 20 px line-box center). 2 px offset → push the content
   slot down 2 px so SVG center and first-line text center share a row.
   Lives on .alert-content (not on .alert-message) so any future tiered
   content (message + actions cluster) inherits the same nudge as one
   stack. Multi-line text remains icon-top-anchored — extra lines push
   below the first; the icon doesn't drift. */
.alert--sm .alert-content { padding-top: var(--space-2); }
.alert--sm .alert-message {
  font-size: var(--text-sm);
  color: var(--text-primary);
}
.alert--sm .alert-actions {
  margin-top: 0;
  margin-left: auto;
}

/* ── --lg — hero banner for page-level alerts with primary CTA ─── */
.alert--lg {
  /* Padding bumped to --space-24 (hero tier on the +4 per-tier alert
     padding ladder: sm 16 / default 20 / lg 24). Radius stays at the
     canonical card 16 px — corner radius is uniform across the alert
     family per the May 2026 standardisation. */
  gap: var(--space-16);
  padding: var(--space-24) var(--space-48) var(--space-24) var(--space-24);
  border-radius: var(--card-radius);
}
.alert--lg.alert--no-dismiss { padding-right: var(--space-24); }
.alert--lg .spot-icon {
  width: var(--space-40);
  height: var(--space-40);
  border-radius: var(--space-12);
}
.alert--lg .spot-icon svg {
  width: var(--space-20);
  height: var(--space-20);
}
.alert--lg .alert-title {
  font-size: var(--text-body-lg);
  line-height: var(--lh-body-lg);
}
.alert--lg .alert-message {
  font-size: var(--text-body);
  line-height: var(--lh-body);
}

/* ── .error-detail — machine-voice diagnostic echo (alert family) ─────
   The alert family's code-bearing sibling: a tinted callout carrying
   VERBATIM machine output — an RPC revert echo, a webhook response
   body, a failed-request excerpt. Deliberately NOT an .alert: the
   tone-bearing human message lives in the surface that hosts it (a
   result header, an alert above), so this box is supporting forensic
   detail — the documented tonal-DETAIL carve-out from the alert
   spot-icon invariant (CLAUDE.md → "Alert .spot-icon is mandatory" —
   scope). Its meaning is carried by its own text, never by tint alone.

   Visual chrome derives from the alert recipe — the same --a-* slot
   vocabulary, the same borderless pastel surface (bg only; --a-border
   stays parked, mirroring the May 2026 alert border drop), the same
   --card-radius + default-tier --space-20 envelope (uniform — no
   dismiss reserve) and --space-4 content rhythm. Severity IS this
   component's identity, so the slots pin to the danger triplet; a
   future warn/info echo variant rebinds the slots exactly like
   .alert--<tone> does — never hand-pick colours.

     <div class="error-detail">
       <p class="error-detail__code">Error · Insufficient_gas</p>
       <p class="error-detail__msg">execution reverted: …</p>
     </div>

   __code is the machine label (the alert-title slot, spoken in mono
   caps — it's an error CODE, not prose) inked with the tone fg.
   __msg is the verbatim echo — mono caption, primary ink, wraps
   anywhere (hashes, URLs). Mono here is the legitimate code-content
   use; the no-mono rule is about table rows, not verbatim echoes.

   Promoted June 2026 from the Portal withdraw-fees flow's page-local
   .wfee__error (atomic retirement, no alias). Catalog:
   design-system/index.html#error-detail · spec:
   design/components/feedback.md → Error detail. */
.error-detail {
  --a-tone: var(--danger);
  --a-bg: var(--danger-bg);
  --a-border: var(--danger-border);
  --a-fg: var(--danger);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  width: 100%;
  padding: var(--space-20);
  background: var(--a-bg);
  border: 0;                          /* family look — the bg carries identity */
  border-radius: var(--card-radius);
  color: var(--text-primary);
}
.error-detail__code {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  font-weight: var(--fw-semibold);
  text-transform: uppercase;
  letter-spacing: var(--tracking-loose);
  color: var(--a-fg);
}
.error-detail__msg {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  line-height: var(--lh-caption);
  color: var(--text-primary);
  overflow-wrap: anywhere;
}

/* ════════════════════════════════════════════════════════════════════
   .notification — toast / overlay notification
   ════════════════════════════════════════════════════════════════════
   Floating toast for ephemeral messages (success / info / warn / danger)
   that slide in from a viewport corner via .notification-stack. Inspired
   by Tailwind UI's notifications block, restyled with the LI.FI design
   system (paper material + --elev-3 floating shadow + spot icon mark).

   Anatomy:
     .notification
       .notification-mark      — leading status icon (40px circle)
       .notification-content   — text wrapper (flex column, min-width:0)
         .notification-title   — headline (text-body, weight 600)
         .notification-message — body text (text-sm, text-secondary)
         .notification-actions — optional action row (buttons)
       .notification-close     — top-right close X

   Status variants (--n-tone color drives the mark + accent):
     .notification--default    --accent-primary (palette-dependent brand cue)
     .notification--info       --info  (palette-independent semantic info)
     .notification--success    --success
     .notification--warn       --warn
     .notification--danger     --danger

   Layout variants:
     .notification--with-actions   action button row beneath message
     .notification--with-avatar    avatar in place of the icon mark
     .notification--compact        single-line condensed (mark + msg + close)
     .notification--split          two-button footer split by hairline

   Container:
     .notification-stack             positional wrapper (default top-right)
       --top-left / --top-center / --bottom-right / --bottom-left /
       --bottom-center

   Spec:
     - Width: --w-7 (416px); collapses to viewport width on ≤480px
     - Padding: --space-16 default · --space-12 compact
     - Radius: --space-12
     - Surface: paper material (--mat-bg / --mat-border) + --elev-3 shadow
       overrides --mat-shadow because notifications float over content
     - Slide-in: opacity 0 + translateX(24px) → 1 + 0 over 200ms
     - z-index: 10000 (above modal, below alert/system overlays)
     - Reduced motion: opacity-only, no translate

   Spec doc: design/components/feedback.md → Notifications.
   ════════════════════════════════════════════════════════════════════ */

.notification {
  /* Material default = paper, wired via :where() block in the materials
     section. Notifications float, so we override --mat-shadow with elev-3
     directly — paper's elev-1 reads as "card on the page" not "panel
     over content." Keep --mat-bg / --mat-border consuming materials so
     a designer can swap to glass / tinted with data-material. */
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: var(--space-12);
  width: 100%;
  max-width: var(--w-7);
  padding: var(--space-16) var(--space-48) var(--space-16) var(--space-16);
  background: var(--mat-bg);
  border: var(--mat-border);
  border-radius: var(--card-radius);
  box-shadow: var(--elev-3);
  pointer-events: auto;
  /* Default tone — accent-primary. Status modifiers override below. */
  --n-tone: var(--accent-primary);
  /* Slide-in / out — apply via .is-active / .is-dismissing transitions
     on the element. The default rest state is INVISIBLE so authors
     opt into the visible state explicitly (matches modal pattern). */
  opacity: 0;
  transform: translateX(24px);
  transition: opacity 0.2s ease, transform 0.2s ease;
}
.notification.is-active {
  opacity: 1;
  transform: translateX(0);
}
.notification.is-dismissing {
  opacity: 0;
  transform: translateX(24px);
}
@media (prefers-reduced-motion: reduce) {
  .notification {
    transition: opacity 0.12s ease;
    transform: none !important;
  }
}

/* Status tone — drives the .is-primary action color in --split.
   The leading mark consumes its tone via .spot-icon--<tone> directly,
   so this token is now narrower in scope (split-button affordance
   only). Authors tag the .spot-icon child with the matching tone
   modifier and the parent .notification with the matching tone
   modifier — both layers are explicit and decoupled. */
/* .notification--default keeps --accent-primary (no tone modifier =
   brand-cue tinting). .notification--info uses the semantic --info
   token so info-variant toasts stay info-blue regardless of palette. */
.notification--default     { --n-tone: var(--accent-primary); }
.notification--info        { --n-tone: var(--info); }
.notification--success     { --n-tone: var(--success); }
.notification--warn        { --n-tone: var(--warn); }
.notification--danger      { --n-tone: var(--danger); }

/* Avatar slot — when the leading element is an avatar (not a spot-
   icon), drop the spot-icon background wash and let the avatar's own
   image / fill be the surface. The avatar element opts in by
   carrying both .spot-icon (for sizing, alignment) and a class like
   .avatar (for the avatar-specific recipe). */
.notification--with-avatar .spot-icon {
  background: transparent;
  padding: 0;
  overflow: hidden;
}
.notification--with-avatar .spot-icon > img,
.notification--with-avatar .spot-icon > .avatar,
.notification--with-avatar .spot-icon > .avatar {
  width: 100%;
  height: 100%;
  border-radius: 50%;
  object-fit: cover;
}

/* Content stack — title + message + (optional) actions */
.notification-content {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.notification-title {
  font-size: var(--text-body);
  font-weight: var(--fw-semibold);
  color: var(--text-primary);
  line-height: var(--lh-body);
  margin: 0;
}
.notification-message {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  line-height: var(--lh-sm);
  margin: 0;
  /* Allow wrapping but don't break mid-word for narrow column widths. */
  overflow-wrap: break-word;
}
.notification-actions {
  margin-top: var(--space-12);
  display: flex;
  gap: var(--space-12);
  align-items: center;
}

/* Close button — top-right X. Position-only modifier on top of the
   canonical DS button stack `.btn-tertiary.btn-icon.btn-borderless
   .btn-sm.btn-circle`. Visuals (size, hover fill, focus ring, icon
   sizing) are owned by the DS button primitives so a system-wide
   affordance update propagates to every close X without per-component
   CSS. .btn-borderless on .btn-icon is transparent at rest with
   chrome only on hover — keeps the close X visually quiet against the
   notification surface. The button stays anchored on hover anyway —
   buttons are static by default since the May 2026 inversion (lift is
   the opt-in .btn-lift). The 48px right padding on .notification
   reserves the X's space (32px button + 8px corner gap + 8px breathing). */
.notification-close {
  position: absolute;
  top: var(--space-8);
  right: var(--space-8);
}

/* ── --compact — single-line toast (no title, smaller mark) ─────── */
.notification--compact {
  align-items: center;
  padding: var(--space-12) var(--space-48) var(--space-12) var(--space-12);
}
.notification--compact .spot-icon {
  width: var(--space-24);
  height: var(--space-24);
  border-radius: var(--space-8);
}
.notification--compact .spot-icon svg {
  width: var(--space-16);
  height: var(--space-16);
}
.notification--compact .notification-title { display: none; }
.notification--compact .notification-message {
  font-size: var(--text-sm);
  color: var(--text-primary);
}

/* ── --split — two-button footer (primary action + dismiss) ───────
   The footer stretches edge-to-edge with a hairline divider above
   and a vertical divider between the two buttons (Tailwind UI's
   "split-button notification" pattern). */
.notification--split {
  padding: 0;
  overflow: hidden;
}
.notification--split .spot-icon { display: none; }
.notification--split > .notification-content {
  padding: var(--space-16);
}
.notification--split .notification-actions {
  margin: 0;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0;
  border-top: 1px solid color-mix(in oklch, currentColor 8%, transparent);
}
.notification--split .notification-actions > button {
  background: transparent;
  border: 0;
  border-radius: 0;
  padding: var(--space-12) var(--space-16);
  font-size: var(--text-sm);
  font-weight: var(--fw-semibold);
  color: var(--text-primary);
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}
.notification--split .notification-actions > button:hover {
  background: var(--alpha-subtle);
}
.notification--split .notification-actions > button + button {
  border-left: 1px solid color-mix(in oklch, currentColor 8%, transparent);
}
.notification--split .notification-actions > button.is-primary {
  color: var(--n-tone);
}

/* ── .notification-stack — positional wrapper ─────────────────────
   `position: fixed` corner stack. Uses `pointer-events: none` on the
   wrapper so the stack itself doesn't block clicks on page content
   between notifications; child notifications opt back in. Default
   placement is top-right; modifiers move to other corners. */
.notification-stack {
  --card-gap: var(--gap-card-md);
  position: fixed;
  z-index: 10000;
  top: var(--space-24);
  right: var(--space-24);
  display: flex;
  flex-direction: column-reverse;        /* newest at top, stacked downward */
  gap: var(--card-gap);
  width: var(--w-7);
  max-width: calc(100vw - var(--space-32));
  pointer-events: none;
}
.notification-stack > * { pointer-events: auto; }

.notification-stack--top-right    { top: var(--space-24); right: var(--space-24); left: auto; bottom: auto; flex-direction: column-reverse; }
.notification-stack--top-left     { top: var(--space-24); left: var(--space-24); right: auto; bottom: auto; flex-direction: column-reverse; }
.notification-stack--top-center   { top: var(--space-24); left: 50%; right: auto; bottom: auto; transform: translateX(-50%); flex-direction: column-reverse; }
.notification-stack--bottom-right { bottom: var(--space-24); right: var(--space-24); left: auto; top: auto; flex-direction: column; }
.notification-stack--bottom-left  { bottom: var(--space-24); left: var(--space-24); right: auto; top: auto; flex-direction: column; }
.notification-stack--bottom-center{ bottom: var(--space-24); left: 50%; right: auto; top: auto; transform: translateX(-50%); flex-direction: column; }

@media (max-width: 480px) {
  .notification-stack {
    left: var(--space-16);
    right: var(--space-16);
    width: auto;
    max-width: none;
  }
  .notification-stack--top-center,
  .notification-stack--bottom-center {
    transform: none;
  }
}


/* ══════════════════════════════════════════
   THEME CARD — theme selection component
   ══════════════════════════════════════════
   Two variants (.theme-card--row, .theme-card--sample) share a single
   anatomy. Each card reads its palette from four local tokens:
     --tc-primary, --tc-secondary, --tc-tertiary, --tc-surface
   set inline by the consumer.

   Primary action → selection. The whole card is a click target via
   an invisible absolutely-positioned .theme-card-select button.
   Secondary action → edit. Clicking .theme-card-edit dispatches a
   `theme-card-edit` CustomEvent; consumers are expected to drill
   into a dedicated editor (name, description, and seed controls
   all live in that sub-panel — never inline).

   Hover follows the design-system card recipe: accent-primary
   background tint, border highlight, translateY lift, 2px top-edge
   halo via ::before.
   ══════════════════════════════════════════ */

/* ── Group layout */
.theme-card-grid {
  --card-gap: var(--gap-card-lg);
  display: grid;
  gap: var(--card-gap);
  max-width: 100%;
}
.theme-card-grid--row    { grid-template-columns: 1fr; max-width: 480px; }
.theme-card-grid--sample { grid-template-columns: repeat(3, 1fr); }

/* ── Base card */
.theme-card {
  --tc-primary:   var(--accent-primary);
  --tc-secondary: var(--accent-secondary);
  --tc-tertiary:  var(--accent-tertiary);
  --tc-surface:   var(--surface-page);
  /* Optional light-mode override. Docs mockups provide --tc-surface-light
     alongside --tc-surface so the mockup reflects the theme's light-mode
     page background when the site is in light mode (matches the live
     Theme Composer, which pulls surface from preset[editMode]). Live
     panels don't need to set these — the default var(--surface-page) above
     is already theme-aware. */
  position: relative;
  display: flex;
  padding: 0;
  margin: 0;
  /* Canonical UI card recipe (May 2026): --surface-card + --elev-1, a
     1 px transparent border at rest that flips to accent on active.
     The card tier lifts one step above the rail / panel it lives in
     (rail = --surface-raised). The border lives INSIDE the box (not
     a box-shadow ring), so it can't be clipped by a parent's
     overflow: hidden — important for cards inside scrollable rails.
     History: was --surface-raised + hairline border + active halo
     until the May 2026 ui-card unification; the first pass used an
     outset box-shadow ring which got clipped by the rail's
     overflow-x: hidden — switched to a real border for reliability. */
  background: var(--surface-card);
  border: 1px solid transparent;
  border-radius: var(--card-radius);
  color: var(--text-primary);
  overflow: hidden;
  box-shadow: var(--elev-1);
  transition: background 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;
}
/* Hover — bump to elev-2 for the lift cue, plus a light accent tint to
   cue interactivity. */
.theme-card:hover {
  background: color-mix(in oklch, var(--accent-primary) 3%, var(--surface-card));
  box-shadow: var(--elev-2);
}
/* Active (pressed) + .is-active (persistent selected) — softened May 2026.
   Rim is accent mixed 64% into transparent (keeps the hue family, drops the
   punch from a full-saturation border). Fill is a uniform 4% accent in both
   themes — calmer than the prior 5% without going so quiet that light mode
   loses the selected signal. The crisp inside border + tone is still the
   canonical UI-card selection pattern; this is just the calmed recipe.
   No halo glow, no layout shift. */
.theme-card:active,
.theme-card.is-active {
  background: color-mix(in oklch, var(--accent-primary) 4%, var(--surface-card));
  border-color: color-mix(in oklch, var(--accent-primary) 64%, transparent);
  box-shadow: var(--elev-2);
}

/* Light-mode surface override for docs mockups. Only fires when the card's
   inline style explicitly declares a --tc-surface-light value — the
   attribute-substring selector guarantees no effect on cards that don't
   set the light variant (e.g. the live Theme Composer, which re-renders
   on mode change and doesn't need this). */
[data-theme="light"] .theme-card[style*="--tc-surface-light"] {
  /* !important is needed because the inline style also sets --tc-surface
     (the dark value) for the base state, and inline-style specificity beats
     normal stylesheet rules. Attribute-substring selector keeps this scoped
     to cards that opted in — live Theme Composer is unaffected. */
  --tc-surface: var(--tc-surface-light) !important;
}
/* Mockup placeholder lines — dark-on-light when the card is showing its
   light-mode surface. The default (white 18%) reads against a dark
   surface but goes invisible on the lighter one; flip to a dark tint
   only for cards that opted into the light-surface variant. */
[data-theme="light"] .theme-card[style*="--tc-surface-light"] .theme-card-mockup-line {
  background: color-mix(in oklch, black 14%, transparent);
}
/* Alpha edit button — flip to dark tint when the card surface is light
   (default styling above is white-on-dark). Same opt-in selector as
   the mockup-line override so live Theme Composer keeps its dark
   surface + white-tint button. */
[data-theme="light"] .theme-card--sample[style*="--tc-surface-light"] .theme-card-edit {
  background: color-mix(in oklch, black 8%, transparent);
  border-color: color-mix(in oklch, black 10%, transparent);
  color: color-mix(in oklch, black 60%, transparent);
}
[data-theme="light"] .theme-card--sample[style*="--tc-surface-light"] .theme-card-edit:hover {
  background: color-mix(in oklch, black 14%, transparent);
  border-color: color-mix(in oklch, black 18%, transparent);
  color: color-mix(in oklch, black 88%, transparent);
}
/* Unify the sample card's body with the mockup surface in BOTH modes.
   Without this override the card body uses --surface-raised while the
   mockup uses the theme's own --tc-surface, and the lightness gap
   reads as a tone bar between the two regions (most pronounced in
   light mode at ~6 points; smaller but still inconsistent across
   themes in dark mode). Pinning the whole card to the theme's
   --tc-surface makes each card a pure preview of its surface — the
   mockup and footer share one tone, with the existing
   `border-bottom: 1px solid var(--border-subtle)` on the mockup
   carrying the seam between content regions.

   Hover + active states keep the unified surface too: the accent
   border + ambient halo (inherited from the base .theme-card.is-active
   rule) are enough to signal selection, and stacking an accent tint
   here would re-introduce the lightness gap.

   Scoped to docs cards via [style*="--tc-surface-light"] — the live
   Theme Composer doesn't set the light variant, so it's unaffected. */
.theme-card.theme-card--sample[style*="--tc-surface-light"],
.theme-card.theme-card--sample[style*="--tc-surface-light"]:hover,
.theme-card.theme-card--sample[style*="--tc-surface-light"].is-active,
.theme-card.theme-card--sample[style*="--tc-surface-light"]:active {
  background: var(--tc-surface) !important;
}

/* ── Invisible select overlay (primary click target) */
.theme-card-select {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  background: transparent;
  border: 0;
  padding: 0;
  margin: 0;
  cursor: pointer;
  font-family: var(--font-sans);                      /* defensive — <button> UA default is Arial; render brand Figtree if ever given a text label / ::after glyph */
  z-index: 1;
}
.theme-card-select:focus-visible {
  outline: 2px solid var(--accent-primary);
  outline-offset: -3px;
  border-radius: inherit;
}

/* Palette strip — RETIRED May 2026. Consumers migrated to the
   unified .swatch family:

     OLD                        NEW
     .palette-strip       →     .swatch-strip   (container)
     .palette-stripe      →     .swatch swatch--strip   (band)
     .theme-card .palette-strip {z-index:2}  →  .theme-card .swatch-strip {width:72px; z-index:2; position:relative}

   The drift-stopper contract (stripe count = declared brand colors +
   surface, ordering primary → secondary → tertiary → surface) is
   preserved by the existing JS renderers (shared.js / swap.js /
   index.html theme cards) — only the class names changed. See the
   .swatch-strip rule + `.theme-card .swatch-strip` rule in the .swatch
   unification block above. */

/* ── Body (name + meta) */
.theme-card-body {
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  gap: 0;
  min-width: 0;
  pointer-events: none; /* clicks fall through to the select button */
}
.theme-card-name {
  font-size: var(--text-body); font-weight: var(--fw-semibold); color: var(--text-primary);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  line-height: var(--lh-sm);
}
.theme-card-meta {
  font-size: var(--text-caption); color: var(--text-muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  line-height: var(--lh-sm);
}

/* ── Edit button — secondary action.
   Compose `.btn-icon btn-xs btn-circle btn-neutral` in the markup;
   that primitive carries the chrome (size, radius, alpha overlay,
   hover, focus). This rule keeps only the layering hooks needed
   inside the card — z-index above the invisible select overlay so
   the click is captured, and pointer-events:auto in case a parent
   has pointer-events:none. Clicking dispatches a `theme-card-edit`
   CustomEvent; consumers drill into a dedicated editor. */
/* Chrome (size, fill, hover, border) comes from the standard button-family
   classes in the markup: .btn-icon.btn-xs.btn-circle.btn-borderless
   (May 2026 unification — matches the swap widget header's borderless
   icon buttons). This rule only owns the layering hooks needed inside
   the card: z-index above the invisible .theme-card-select overlay so
   clicks land here, pointer-events:auto in case an ancestor disables
   them. Don't author chrome here — reach for a different button-family
   modifier if the recipe needs to change. */
.theme-card-edit {
  position: relative;
  z-index: 3;
  pointer-events: auto;
}

/* ── Edit-pencil visibility — selection-revealed secondary action.
   By default the pencil is hidden on every card so a list of N theme
   cards reads as a list, not a row of N persistent action menus. It
   reveals on the active card (so the current theme always shows the
   edit affordance as a persistent cue), AND on hover / focus-within
   (so a power user can edit a non-current theme without first applying
   it — applying mutates the whole page chrome, which is real friction
   for "I just want to fork Hop"). On touch where :hover doesn't fire,
   behaviour collapses to active-only. Set on the primitive so every
   consumer (rail theme list, FAB Theme Composer, future surfaces)
   inherits the same model. Established May 2026. */
.theme-card .theme-card-edit { display: none; }
.theme-card.is-active .theme-card-edit,
.theme-card:hover .theme-card-edit,
.theme-card:focus-within .theme-card-edit { display: inline-flex; }

/* Check indicator (active state — row variant only) */
.theme-card-check {
  flex-shrink: 0;
  width: var(--space-24); height: var(--space-24); border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: transparent;
  border: 1.5px solid var(--border-subtle);
  color: transparent;
  transition: background 0.2s, border-color 0.2s, color 0.2s;
  position: relative;
  z-index: 2;
  pointer-events: none;
}
.theme-card-check svg { width: var(--space-12); height: var(--space-12); }
.theme-card.is-active .theme-card-check {
  background: var(--accent-primary);
  border-color: var(--accent-primary);
  color: oklch(100% 0 0);
}

/* ───────── Variant A — Compact Row ───────── */
.theme-card--row {
  align-items: center;
  gap: var(--cluster-md);
  padding: var(--space-12) var(--space-12);
}
.theme-card--row .theme-card-body { flex: 1; }

/* ───────── Variant B — Mini UI Sample ───────── */
.theme-card--sample {
  flex-direction: column;
  align-items: stretch;
}

/* Mini UI preview — top region paints the theme tokens as a mock layout:
   surface background, gradient headline ("Aa"), text lines, CTA button + chip. */
.theme-card--sample .theme-card-mockup {
  position: relative;
  background: var(--tc-surface);
  padding: var(--space-12);
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
  min-height: 136px;
  border-bottom: 1px solid var(--border-subtle);
  z-index: 2;
  pointer-events: none;
  overflow: hidden;
}
.theme-card-mockup-head {
  font-size: var(--text-h2); font-weight: var(--fw-extrabold); letter-spacing: var(--tracking-snug);
  background: linear-gradient(135deg, var(--tc-primary) 0%, var(--tc-secondary) 100%);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: transparent;
  line-height: 1;              /* ratio 1.0 — tight gradient "Aa" sample */
}
.theme-card-mockup-lines {
  display: flex; flex-direction: column; gap: var(--space-4);
}
.theme-card-mockup-line {
  height: var(--space-4); width: 90%; border-radius: var(--space-4);
  background: color-mix(in oklch, white 18%, transparent);
}
.theme-card-mockup-ctas {
  display: flex; gap: var(--cluster-xs); margin-top: auto; padding-top: var(--space-4);
}
.theme-card-mockup-btn {
  height: var(--space-20); width: var(--space-56); border-radius: var(--space-8);
}
/* Flat-filled CTAs — primary and secondary slots show the theme's
   primary and secondary colors directly, the way most real product
   buttons render. The gradient is reserved for the headline above so
   the mockup still demonstrates both the gradient and flat treatments. */
.theme-card-mockup-btn--primary {
  background: var(--tc-primary);
}
.theme-card-mockup-btn--secondary {
  background: var(--tc-secondary);
}
.theme-card-mockup-btn--tertiary {
  background: var(--tc-tertiary);
}
/* Subtle vignette tying the mockup to the primary brand color.
   Origin sits at top-right so the brand glow concentrates around the
   absolutely-positioned edit button (also top-right) — that wash both
   makes the button's surface read warmer and gives the corner a focal
   point that pulls the eye toward it. */
.theme-card--sample .theme-card-mockup::after {
  content: '';
  position: absolute; inset: 0;
  background: radial-gradient(ellipse at top right,
    color-mix(in oklch, var(--tc-primary) 22%, transparent) 0%,
    transparent 60%);
  pointer-events: none;
}

/* Footer: name + meta only. Edit moved out of the footer to the
   card's upper-right corner (see .theme-card--sample .theme-card-edit
   below). Footer keeps pointer-events:auto in case any future child
   (e.g. inline metadata chip) needs to be clickable; the body still
   uses pointer-events:none so clicks there fall through to select. */
.theme-card--sample .theme-card-footer {
  display: flex; align-items: center;
  gap: var(--cluster-sm);
  padding: var(--space-12) var(--space-12) var(--space-12);
  position: relative;
  z-index: 2;
  pointer-events: auto;
}
/* Sample variant inherits the base name/meta typography from
   .theme-card-name / .theme-card-meta so row and sample read with
   identical hierarchy across the catalog. Only the flex layout is
   sample-specific (the body fills available width inside the footer). */
.theme-card--sample .theme-card-body {
  flex: 1;
}

/* Sample variant places the same `.btn-icon btn-xs btn-circle
   btn-borderless` edit button at the trailing edge of the footer, next
   to the name + meta block. Footer uses `align-items: center` so
   the button vertically centers against the two-line text block. */
.theme-card--sample .theme-card-edit {
  flex-shrink: 0;
}

/* Responsive */
@media (max-width: 720px) {
  .theme-card-grid--sample { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 480px) {
  .theme-card-grid--sample { grid-template-columns: 1fr; }
}


/* ══════════════════════════════════════════
   THEME COMPOSER — list + edit views
   ══════════════════════════════════════════ */

/* View switching inside the FAB sub-panel */
[data-tc-view="list"] .tc-view--edit { display: none; }
[data-tc-view="edit"] .tc-view--list { display: none; }
.tc-view { display: flex; flex-direction: column; gap: var(--space-8); }

/* Apply-scope seg (June 2026 — the unified apply) — the 3-state
   Widget/Canvas · Chrome · Everywhere scope picker above the theme lists
   (playground rail + FAB composer list view). Full-width so the three
   items split the column evenly; the seg primitive owns the chrome. */
.theme-scope-seg { display: flex; width: 100%; }
.theme-scope-seg .seg-item { flex: 1 1 0; }

/* Vertical stack of theme cards */
.tc-cards-list {
  display: flex; flex-direction: column; gap: var(--cluster-sm);
  margin-bottom: var(--space-4);
}
.tc-cards-empty {
  padding: var(--space-16) var(--space-12);
  font-size: var(--text-caption); color: var(--text-muted);
  text-align: center;
  border: 1px dashed var(--border-subtle);
  border-radius: var(--space-12);
}

/* Edit view no longer ships its own header — the FAB sub-panel's top nav
   ("← THEME COMPOSER") is retitled to the theme name on enter/exit.
   Former rules for .pc-edit-head / .pc-edit-back / .pc-edit-title removed. */

/* Edit view — name + description fields container. Stacked fields
   (.form-label over .input--sm + an at-cap .form-error — June 2026;
   replaced the inline-label .field-inline pair), so the inter-field
   gap steps up to 12 for the taller two-line rhythm. */
.tc-edit-meta {
  display: flex; flex-direction: column; gap: var(--space-12);
  padding-bottom: var(--space-12);
  margin-bottom: var(--space-4);
  border-bottom: 1px dashed var(--border-subtle);
}

/* Delete button — signal destructive action with subtle warning color */
.tc-view--edit #pcDeleteTheme {
  color: oklch(65% 0.2 27);
  border-color: color-mix(in oklch, oklch(65% 0.2 27) 30%, var(--border-subtle));
}
.tc-view--edit #pcDeleteTheme:hover {
  background: color-mix(in oklch, oklch(65% 0.2 27) 10%, transparent);
  color: oklch(72% 0.22 27);
}


/* ══════════════════════════════════════════
   9. LIGHT THEME — STRUCTURAL OVERRIDES
   ══════════════════════════════════════════
   The OKLCH token system in :root / [data-theme="light"]
   handles all text, overlay, and border color flips
   automatically. Only structural differences that can't be
   expressed via tokens are listed here.
   ══════════════════════════════════════════ */

/* Body */
[data-theme="light"] body { background: var(--surface-page); color: var(--text-primary); }

/* Gradient text: in light mode accent-tertiary is now a rich violet,
   so the gradient reads blue → violet rather than blue → pale-pink */
[data-theme="light"] .gradient-text {
  background: linear-gradient(var(--accent-primary), var(--accent-primary)); /* TEMP solid — gradient text paused */
  -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
}

/* Light-mode hero gradient typography — mirrors the hero H1 light-mode rule
   (styles.css: [data-theme="light"] .hero h1 .highlight) so titles using the
   hero gradient shift from Blue→Teal to Blue→Violet in light theme. */
[data-theme="light"] .text-gradient-hero,
[data-theme="light"] .accent-card-title {
  background: linear-gradient(var(--accent-primary), var(--accent-primary)); /* TEMP solid — gradient text paused */
  -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
}

/* Footer: drop to --surface-raised in light mode for a subtler nested surface */
[data-theme="light"] .footer { background: var(--surface-raised); }


/* ══════════════════════════════════════════
   10. SHARED RESPONSIVE BREAKPOINTS
   ══════════════════════════════════════════ */

/* ── 860px: Hamburger replaces desktop nav ── */
@media (max-width: 860px) {
  .navbar-site, .navbar-docs { padding: 0 24px; }
  .nav-links { display: none; }
  .nav-utility { margin-left: auto; }
  button.hamburger { display: inline-flex; flex-shrink: 0; }

  /* Section primitives now auto-adapt via semantic spacing tokens */

  /* Footer: two-column on tablet */
  .footer-inner  { grid-template-columns: 1fr 1fr; gap: 40px; padding: 64px 24px 40px; }
  .footer-bottom { flex-wrap: wrap; gap: 16px; padding: 24px 24px 40px; }
}

/* ── 768px: Dropdown stacks, section desc centers ── */
@media (max-width: 768px) {
  .nav-dropdown-inner { flex-direction: column; padding: 32px 24px; }
  .nav-dropdown-left  { padding: 0 0 var(--space-16); }
  .nav-dropdown-left::after { display: none; }
  .nav-dropdown-grid  { grid-template-columns: 1fr; }

  /* Section primitives */
  /* Tighten clamp floor at mobile so the title doesn't tower over a small card */
  .section-title { font-size: clamp(var(--text-h2), 8vw, var(--text-h1)); }
}

/* ── 600px: Single-column footer, tightest padding ── */
@media (max-width: 600px) {
  /* .section-inner padding auto-adapts via --pad-section-x */
  .footer-inner  { grid-template-columns: 1fr; gap: var(--space-32); }
  .footer-bottom { padding: var(--space-24) var(--space-20) var(--space-32); }
  .footer-inner  { padding: var(--space-48) var(--space-20) var(--space-32); }

  /* Buttons: full-width on phones */
  .btn-primary,
  .btn-secondary,
  .btn-tertiary,
  .btn-white {
    width: 100%;
    justify-content: center;
  }
}

/* ── 400px: Micro adjustments for very small phones ── */
@media (max-width: 400px) {
  .navbar-site, .navbar-docs { padding: 0 var(--space-12); }
  /* Section primitives auto-adapt via semantic tokens */
  .footer-inner  { padding: var(--space-32) var(--space-12) var(--space-24); }
  .footer-bottom { padding: var(--space-16) var(--space-12) var(--space-24); }
}


/* ══════════════════════════════════════════════════════════════════════
   MOBILE POLISH — comprehensive pass for DS catalog + marketing pages
   ──────────────────────────────────────────────────────────────────
   Pragmatic overrides that make every page readable on phones without
   touching the desktop look. Applied in three tiers:
     ≤720px — tablet + large phones: tighten section padding, collapse
              card grids, scroll tables gracefully, wrap code blocks.
     ≤600px — standard phones: more aggressive padding reductions,
              stronger heading clamps, tighter component demos.
     ≤480px — small phones: final squeeze.

   Every rule below is additive — it only takes effect at mobile widths
   and does not affect desktop layouts. Rules are scoped narrowly so no
   single selector grabs broad authority. ══════════════════════════════════════════════════════════════════════ */

/* ── Tier 1: ≤720px — tighten section rhythm, collapse card grids ── */
@media (max-width: 720px) {
  /* Page-wide safety net — any rogue child that escapes its parent's
     clip (e.g. the logo-cloud marquee track with `width: max-content`,
     or a long unwrapped code string) must not produce a horizontal
     scrollbar on <body>. Mobile pages should never scroll sideways. */
  html, body { overflow-x: hidden; }

  /* Clamp known wide children to the viewport even if their internal
     layout is wider. `contain: paint` tells the browser nothing inside
     this element should contribute to ancestor layout (including
     scroll width). `max-width: 100vw` is the belt-and-suspenders. */
  .logo-cloud--marquee {
    max-width: 100vw;
    contain: paint;
  }
  .ds-doc-codeblock,
  .ds-doc-code-block,
  pre {
    max-width: 100%;
    contain: paint;
  }

  /* Long unwrapped identifiers / strings inside code blocks — give
     them permission to wrap so a 1000-char token doesn't push the
     viewport. */
  .ds-doc-codeblock code,
  .ds-doc-codeblock .string,
  .ds-doc-code-block code,
  pre code {
    white-space: pre-wrap;
    word-break: break-word;
  }

  /* DS catalog sections: reduce top/bottom breathing. Outer gutter
     is sized to give content visible breathing room from the screen
     edge while still maximising reading width — 20px reads as
     "intentional margin" rather than "content squeezed against the
     bezel". Component surfaces inside still carry their own padding. */
  .section { padding-block: 48px; }
  .section-inner,
  .section > .container { padding-inline: 20px; }

  /* Doc-style paragraphs: no artificial max-width squeezing text
     into a tiny column when the viewport is already narrow. */
  .ds-doc-text { max-width: none; }

  /* Card grids (Don'ts, spec cards) → single column. Applies to the
     generic .grid-N helpers used across the catalog. */
  .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; gap: var(--space-16); }

  /* Data tables: establish a sensible min-width so columns don't
     squish into unreadable rows. The wrapper already has
     overflow-x: auto, so this makes the scroll meaningful. */
  .ds-doc-table-wrap {
    -webkit-overflow-scrolling: touch;
    scrollbar-width: thin;
  }
  .ds-doc-table-wrap .data-table { min-width: 520px; }

  /* Catalog demo surfaces — reclaim ~16px of horizontal real-estate
     by tightening from desktop's 40-60px down to 16px on mobile.
     Component examples (buttons, badges, chips) inside still read
     as inset specimens because the surrounding card border + bg
     carries the framing, not the padding. */
  .ds-doc-card-demo { padding: 16px !important; }

  /* Code blocks — horizontal scroll for long lines; no silent clip.
     Keep mono + preserve formatting but allow user to pan. */
  .ds-doc-codeblock,
  .ds-doc-code-block,
  pre {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    max-width: 100%;
  }

  /* Comp-label + section-desc already fit fine — no override needed. */
}

/* ── Tier 2: ≤600px — phone-dominant, stronger mobile voice ── */
@media (max-width: 600px) {
  /* Further tighten section rhythm. Container gutter sits at 16px
     so prose reads with deliberate margin from the screen edge,
     yet keeps a generous reading width on small phones (e.g. 360px
     viewport → 328px body-text width). */
  .section { padding-block: 40px; }
  .section-inner,
  .section > .container { padding-inline: 16px; }

  /* Hero titles — aggressive clamp so "Components" / "Brand Colors"
     never overflow. The clamp is defensive: large hero-scale titles
     get bounded by 9vw (≈34px at 380vw), the `.section-title` clamp
     floor by 8vw. */
  .section-title {
    font-size: clamp(1.75rem, 9vw, var(--text-h1));
    letter-spacing: var(--tracking-tight);
    line-height: var(--lh-h2, 1.15);
  }
  .eyebrow-code { font-size: var(--text-caption); }

  /* Doc-style headers (hero-style headings on DS surfaces).
     !important is load-bearing here because `doc-shared.css` loads
     AFTER `styles.css` and declares `.doc-header--compact .header-title
     { font-size: var(--text-display); }` at equal specificity — which
     would otherwise win and produce a 56px title at 320px viewport,
     overflowing the container. The override keeps mobile rules
     concentrated in one block in styles.css rather than splitting
     them across multiple stylesheets. */
  .doc-header .header-title,
  .doc-header--compact .header-title,
  .doc-header--showcase .header-title,
  .doc-header--center .header-title {
    font-size: clamp(2rem, 10vw, var(--text-hero, 4rem)) !important;
    line-height: 1.08 !important;
    letter-spacing: var(--tracking-tighter);
  }
  .doc-header .header-subtitle { font-size: var(--text-body); }
  .doc-header .header-eyebrow { font-size: var(--text-caption); }

  /* Art-page piece headers — tighter horizontal padding so the
     canvas spans nearly the full viewport width. */
  .piece-title {
    font-size: clamp(1.75rem, 8vw, 44px) !important;
    line-height: 1.1;
  }
  .piece-desc { font-size: var(--text-body); }
  .piece-section { padding: 0 16px 64px !important; }
  .piece-header { flex-direction: column !important; align-items: flex-start !important; gap: 16px !important; }
  .piece-actions { align-self: stretch !important; flex-wrap: wrap; }
  .piece-canvas-wrap { height: 360px !important; border-radius: 16px !important; }

  /* Manifesto card on art page — 1-col, lighter padding. */
  .philosophy-section { padding: 0 16px 56px !important; }
  .philosophy-card { grid-template-columns: 1fr !important; gap: 24px !important; padding: 32px 24px !important; }
  .philosophy-title { font-size: clamp(1.75rem, 8vw, 32px) !important; }

  /* Usecase cards on art page */
  .usecase-section { padding: 0 16px 80px !important; }
  .usecase-grid { grid-template-columns: 1fr !important; gap: 16px !important; }

  /* Logo Cloud — generous vertical padding (the cloud IS a section
     moment, not a row stuffed inline) with a clear horizontal margin
     so the grid doesn't run flush with the surrounding card edge. */
  .logo-cloud { padding: var(--space-48) var(--space-20) !important; }
  .logo-cloud-title { font-size: clamp(1.25rem, 5.5vw, var(--text-h3)); }
  .logo-cloud--split .logo-cloud-title { font-size: clamp(1.5rem, 7vw, var(--text-h2)); }
  .logo-cloud-stat-value { font-size: clamp(1.75rem, 8vw, var(--text-h2)); }
  .logo-cloud-grid { gap: var(--space-32) var(--space-24) !important; }

  /* Documentation sidebar on DS pages — already collapses to a
     horizontal rail at 900px. Nothing further needed unless we want
     the accordion variant (Phase 2). */

  /* Footer lists — respect mobile breathing */
  .footer-col { gap: var(--space-8); }

  /* Buttons full-width rule from 600px already applies site-wide; in
     catalog demos override back to auto so inline button rows don't
     get clobbered by full-width stretch. */
  .ds-doc-card-demo .btn-primary,
  .ds-doc-card-demo .btn-secondary,
  .ds-doc-card-demo .btn-tertiary,
  .ds-doc-card-demo .btn-white,
  .comp-row .btn-primary,
  .comp-row .btn-secondary,
  .comp-row .btn-tertiary,
  .comp-row .btn-white {
    width: auto;
    justify-content: center;
  }

  /* Comp rows (button demos, chip demos) — always wrap */
  .comp-row { flex-wrap: wrap; gap: var(--space-8); }

  /* Hero visual slots — shrink their fixed heights so the hero
     eyebrow + title + subtitle + visual all fit in one scroll
     glance. Full 480-560px towers eat the screen. */
  .doc-header { padding-top: 96px !important; padding-bottom: 48px !important; }
  .doc-header .container { padding-inline: var(--space-20) !important; max-width: 100vw !important; }
  .doc-header .header-visual { max-height: 360px !important; }
  .doc-header .header-subtitle { max-width: 100% !important; }

  /* Brand-guide coin stack — the 450×450 isometric coin stack was sized
     for desktop heroes. On mobile the fixed width blows past the
     viewport, so clamp the container AND rescale the stack so it
     reads as a compact decorative element rather than a cropped
     escape. The chained transform preserves the existing isometric
     tilt, just drops a `scale(0.6)` at the end. */
  .header-visual--coin-stack {
    width: 100% !important;
    max-width: 300px !important;
    height: 300px !important;
    margin: 16px auto 0 !important;
    overflow: hidden !important;
  }
  .header-visual--coin-stack .coin-stack {
    transform: rotateX(58deg) rotateZ(-15deg) scale(0.6) !important;
    transform-origin: 50% 50% !important;
  }

  /* Doc-header split layout (brand-guide) → stack vertically on phones
     so the copy column and visual column each get full width. */
  .doc-header--split .container {
    grid-template-columns: 1fr !important;
    display: grid;
    gap: var(--space-20) !important;
  }

  /* Art-page hero canvas */
  .header-visual--art { height: 360px !important; border-radius: var(--space-24) !important; }
  .blades-canvas-wrap { height: 360px !important; border-radius: var(--space-20) !important; }
  .blades-section { padding: 40px 16px !important; }
}

/* ── Tier 2b: Sidebar accordion under ≤900px ───────────────────────
   Consolidated May 2026 — the mobile accordion that used to live
   here as an !important override now lives directly in §3.5 scoped
   to `.side-nav--sticky`. See `@media (max-width: 900px)` inside
   the §3.5 block. */

/* ── Tier 2c: Known catalog offenders ──────────────────────────
   Specific in-catalog live-preview layouts that need mobile-aware
   overrides because they were authored with desktop-only column
   counts or fixed widths. Each override is scoped narrowly so it
   only affects the preview surface, not the real component. */
@media (max-width: 720px) {
  /* Type scale live preview — stack the two-column label + sample */
  .comp-section [style*="display:flex"][style*="gap:"] {
    flex-wrap: wrap;
  }

  /* Color swatch rows — always wrap; never overflow */
  .comp-row, .swatch-row { flex-wrap: wrap; gap: var(--space-8); }
}

/* ── Tier 3: ≤480px — small phones, final squeeze ── */
@media (max-width: 480px) {
  .section { padding-block: var(--space-32); }
  .section-inner,
  .section > .container { padding-inline: var(--space-12); }
  /* On the smallest phones, drop card-demo internal padding to 12px.
     The .ds-doc-card-demo border + background carry the framing — we
     don't need 16-24px of empty space inside it on a phone. */
  .ds-doc-card-demo { padding: var(--space-12) !important; }

  /* Section primitives inside narrow viewports: section-desc becomes
     mobile-first and takes the whole width. */
  .section-desc { font-size: var(--text-body); max-width: none; }

  /* Doc-hero visual — bring it under 320px tall on phones so the hero
     eyebrow + title + subtitle + visual all fit in one scroll glance. */
  .doc-header .header-visual { max-height: 320px !important; }
  .header-visual--art { height: 280px !important; border-radius: 20px !important; }
  .header-visual--coin-stack { height: 280px !important; }
  .blades-canvas-wrap { height: 320px !important; }

  /* Logo cloud grids — 2 cols max on small phones regardless of
     author-declared column count */
  .logo-cloud-grid--3,
  .logo-cloud-grid--4,
  .logo-cloud-grid--5,
  .logo-cloud-grid--6 { --logo-cloud-cols: 2 !important; }

  /* Logo cloud split stacks naturally at 900px; at 480 tighten the
     vertical gap between the copy and the grid. */
  .logo-cloud--split { gap: var(--space-24) !important; }

  /* Catalog demo inline preview wrappers — strong caps so author-
     declared min-widths don't blow out. */
  .ds-doc-card-demo > div[style*="grid-template-columns"],
  .ds-doc-card-demo > div[style*="display:grid"],
  .ds-doc-card-demo > div[style*="display: grid"] {
    grid-template-columns: 1fr !important;
    gap: var(--space-16) !important;
  }

  /* Live preview rows that assume ≥600px horizontal room */
  .ds-doc-card-demo > div[style*="display:flex"],
  .ds-doc-card-demo > div[style*="display: flex"] {
    flex-wrap: wrap !important;
  }

  /* Tables: slim the min-width further; preserve scroll */
  .ds-doc-table-wrap .data-table { min-width: 440px; }
  .ds-doc-table-wrap .data-table th,
  .ds-doc-table-wrap .data-table td { padding: var(--space-8) var(--space-12); font-size: var(--text-caption); }
}


/* ═══════════════════════════════════════════════════════════════
   USE-CASE PAGE PRIMITIVES — .uc-*
   ═══════════════════════════════════════════════════════════════
   Extracted from 6 use-case pages (defi, wallets, payments,
   neobanks, rwa, agentic-commerce) + contact-us, where the
   identical CSS block was duplicated in every `<style>` tag.
   Every class here is the canonical source — pages should
   remove their local copy and inherit from here.

   Also covers the two-column `.contact-layout` pattern used
   on contact-us.html. The right-column helpers (.dev-*) live
   here too so future contact / support pages can reuse them.
*/

/* Hero block — full-bleed with 144/56 padding (Apr 2026: was 192/192;
   bottom dropped to 56 so the next module lands within first-fold
   reach). Body::before halo carries the atmospheric light. No per-hero
   background class — the canvas is the canvas. */
.uc-hero {
  padding: 144px 0 56px;
  position: relative;
  overflow: hidden;
}
.uc-hero .container {
  position: relative; z-index: 1;
  max-width: 1200px; margin: 0 auto;
  padding: 0 48px;
}

/* Hero-context margin override for the .eyebrow-word kicker on
   use-case heroes. The rest of the recipe (size / weight / tracking
   / color / theme) is owned by the canonical .eyebrow-code, .eyebrow-word
   shared base near line 4870 — see also the unified Eyebrow family doc
   at #eyebrow in design-system/index.html.

   Was previously a full re-declaration that drifted size to 0.72rem
   (11.52px) and tracking to 0.16em — retired May 2026 so the use-case
   hero eyebrow matches the rest of the family at 12px / --tracking-wider.
   The hero kept its 20px gap below; everywhere else stays on the
   --space-12 rhythm from the base rule. */
.uc-hero .eyebrow-word { margin-bottom: 20px; }

/* Hero title — 64px display, plain solid color. Matches `.header-title`
   but with tighter tracking for marketing density. No gradient span;
   highlighted phrases use solid accent color (--accent-primary) if a
   page needs visual emphasis on a keyword. */
.uc-title {
  font-size: 64px; font-weight: var(--fw-extrabold);
  letter-spacing: var(--tracking-tighter); line-height: 1.1;
  color: var(--text-primary);
  margin-bottom: 24px;
}
/* `--sm` variant for two-column heroes (contact) where 64px
   would crowd the layout. */
.uc-title--sm { font-size: var(--text-display); }

.uc-desc {
  font-size: 1.15rem; color: var(--text-muted);
  line-height: 1.75; max-width: 600px;
  margin-bottom: 36px;
}

.uc-buttons {
  display: flex; gap: 16px; flex-wrap: wrap;
  margin-bottom: 28px;
}

/* Content section — constrained width with vertical rhythm.
   Use inside `.uc-hero` sibling sections. */
.uc-section {
  max-width: 1200px; margin: 0 auto;
  padding: 80px 48px;
}
.uc-section-title {
  font-size: 36px; font-weight: var(--fw-extrabold);
  letter-spacing: var(--tracking-tight);
  color: var(--text-primary);
  margin-bottom: 48px;
  text-align: center;
}

/* 2-column card grid. Collapses to 1 column below 768px. */
.uc-grid {
  --card-gap: var(--gap-card-xl);
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--card-gap);
}

/* Interactive use-case tile. Uses the canonical UI card recipe —
   --surface-card + --elev-1 at rest, accent tint on hover/active,
   1 px accent border (inside the box) on active. May 2026 ui-card
   unification: was --surface-raised + --border-subtle border +
   2 px translucent accent ring; brought into the system recipe so
   uc-cards sit at the same surface tier as every other card. The
   border lives inside the box-edge (transparent at rest, accent on
   active) so it can't be clipped by a parent's overflow: hidden. */
.uc-card {
  background: var(--surface-card);
  border: 1px solid transparent;
  border-radius: 20px;
  padding: 36px;
  box-shadow: var(--elev-1);
  transition:
    background 0.18s ease,
    border-color 0.18s ease,
    box-shadow 0.18s ease;
}
.uc-card:hover {
  background: color-mix(in oklch, var(--accent-primary) 3%, var(--surface-card));
  box-shadow: var(--elev-2);
}
.uc-card:active,
.uc-card.is-active {
  background: color-mix(in oklch, var(--accent-primary) 5%, var(--surface-card));
  border-color: var(--accent-primary);
  box-shadow: var(--elev-2);
}
.uc-card h3 {
  font-size: 1.1rem; font-weight: var(--fw-bold);
  color: var(--text-primary);
  margin-bottom: var(--space-12);
}
.uc-card p {
  font-size: 0.92rem; color: var(--text-muted);
  line-height: 1.65;
}
[data-theme="light"] .uc-card { background: #fff; }

/* Stats strip — horizontal row of counters, centered. Collapses
   to stacked column on mobile. */
.uc-stats {
  display: flex; gap: 48px; justify-content: center;
  padding: 60px 48px;
}
.uc-stat { text-align: center; }
.uc-stat-num {
  font-size: 36px; font-weight: var(--fw-extrabold);
  color: var(--text-primary);
  letter-spacing: var(--tracking-tight);
}
.uc-stat-label {
  font-size: 0.78rem; color: var(--text-faint);
  text-transform: uppercase; letter-spacing: var(--tracking-wide);
  margin-top: var(--space-8);
}

/* CTA strip — narrow centered block for "ready to get started"
   moments at the end of a page. */
.uc-cta {
  max-width: 700px; margin: 0 auto;
  padding: 80px 48px;
  text-align: center;
}
.uc-cta h2 {
  font-size: 32px; font-weight: var(--fw-extrabold);
  color: var(--text-primary);
  margin-bottom: 16px;
}
.uc-cta p {
  font-size: 1rem; color: var(--text-muted);
  line-height: 1.7; margin-bottom: 32px;
}
.uc-cta-buttons {
  display: flex; gap: 16px;
  justify-content: center;
}

/* Contact page two-column layout. Left column carries the hero
   copy; right column (.uc-side) holds the sales CTA + form.
   `.uc-side-divider` enforces the vertical/horizontal rule. */
.uc-layout {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0;
  align-items: start;
}
.uc-layout-left {
  padding-right: 60px;
  border-right: 1px solid var(--border-subtle);
}
.uc-layout-right {
  padding-left: 60px;
  display: flex; flex-direction: column;
  justify-content: flex-start;
}

/* Mobile collapse — single column, responsive paddings, and title
   downscale so heroes don't overflow on phones. Applied once, in
   the DS; pages should no longer need per-page media queries for
   these primitives. */
@media (max-width: 768px) {
  .uc-grid { grid-template-columns: 1fr; }
  .uc-title { font-size: var(--text-h2-lg); }
  .uc-title--sm { font-size: var(--text-h2-lg); }
  .uc-hero { padding: 100px 0 80px; }
  .uc-hero .container,
  .uc-section,
  .uc-cta { padding-left: 24px; padding-right: 24px; }
  .uc-stats { flex-direction: column; gap: 24px; align-items: center; }
}
@media (max-width: 900px) {
  .uc-layout { grid-template-columns: 1fr; gap: 48px; }
  .uc-layout-left {
    padding-right: 0;
    border-right: none;
    border-bottom: 1px solid var(--border-subtle);
    padding-bottom: 48px;
  }
  .uc-layout-right { padding-left: 0; }
}


/* ═══════════════════════════════════════════════════════════════════
   VIEWPORT (.ds-doc-viewport) — canonical component preview surface
   ═══════════════════════════════════════════════════════════════════

   Replaces the ad-hoc `.ds-doc-card-demo` / `.ds-doc-tile-demo` / per-page
   inline-styled demo wrappers. One primitive, one visual language,
   extensible via sub-components and modifier classes.

   WHEN TO REACH FOR IT
     Any place a component is rendered for inspection — design-system
     catalog previews, dashboard "live examples" panels, brand-guide
     demos, skill-rendered artifacts that embed DS components. If
     you're showing a component to a reader and need a neutral frame
     around it, that's a `.ds-doc-viewport`.

   ANATOMY
     .ds-doc-viewport                      outer surface + padding
       .ds-doc-viewport-body               inner content wrapper (layout
                                           modifiers target this)

   (May 2026: the `-header` / `-footer` / `-label` / `-caption` chrome
    strips were retired as redundant; per-variant labels now live as
    .ds-doc-eyebrow above each demo. See deprecated rules ~13360.)

   MODIFIER AXES (orthogonal — stack freely)
     Surface       --showcase (brand-gradient backdrop) — the base is
                   transparent; to frame a demo in a recessed well use
                   the `.ds-doc-well` primitive (NOT a viewport variant).
     Size (pad)    --sm | (default) | --lg
     Layout        --center | --stack | --stack-lg | --row
     Height        --tall

   EXTENSION
     `.ds-doc-viewport-controls` is an empty flex slot. Drop any DS button
     (`.btn-tertiary.btn-icon`, `.ds-doc-seg-control`, a custom
     `.ds-doc-viewport-control`) inside it — spacing + alignment come for
     free. Add page-specific controls (filter pickers, state switches,
     playground shortcuts) without extending the component.
*/

.ds-doc-viewport {
  --vp-pad: var(--space-32);
  /* Radius is keyed off the design-system radius scale (--r-xl = 16px). Custom
     properties stay defined so variants that DO frame themselves
     (--tabbed, --showcase) can reference them. */
  --vp-radius: var(--r-xl);
  --vp-border: color-mix(in oklch, var(--text-primary) 8%, transparent);
  padding: var(--vp-pad);
  position: relative;
  /* Base viewport is transparent — sits on the page surface. Framed-panel
     chrome (background, border, radius, shadow) lives on the variants
     that actually need a contained surface: --tabbed (the tab strip
     needs a container), --showcase (gradient backdrop demo).
     Non-tabbed previews (--stack, --row, --center, bare) inherit the
     transparent base so demos read against the page surface directly. */
}

/* ── Surface variants ──────────────────────────────────────────── */
/* `--showcase` — ambient brand-gradient backdrop, mimicking the
   surfaces used in decks, proposals, and the marketing hero pages.
   Use on demo cards that need to demonstrate how a *material* reads
   against a real-world brand-tinted background — the canonical case
   is glass material, where the radial brand wash shows through the
   translucency, revealing the material's actual character. The
   gradient stack is two anchored radials (top-left brand-primary,
   bottom-right brand-secondary) plus the raised base surface. */
.ds-doc-viewport--showcase {
  background:
    radial-gradient(ellipse 120% 80% at 0% 0%,
      color-mix(in oklch, var(--accent-primary) 24%, transparent) 0%,
      transparent 60%),
    radial-gradient(ellipse 100% 70% at 100% 100%,
      color-mix(in oklch, var(--accent-tertiary, var(--accent-secondary)) 18%, transparent) 0%,
      transparent 55%),
    var(--surface-raised);
  /* Declares its own framed-panel chrome (border + radius + overflow) — the
     base viewport is transparent, so --showcase carries the full frame. */
  border: 1px solid color-mix(in oklch, var(--accent-primary) 18%, transparent);
  border-radius: var(--vp-radius);
  overflow: hidden;
}

/* ── Size variants ─────────────────────────────────────────────── */
.ds-doc-viewport--sm { --vp-pad: var(--space-20); }
.ds-doc-viewport--lg { --vp-pad: var(--space-48); }

/* ── Layout variants — apply to `.ds-doc-viewport-body` if present,
     else cascade to the viewport itself.

     Why two selectors per variant: bare viewports (no header/footer
     chrome) are centred / rowed / stacked at the outer level. As soon
     as a `.ds-doc-viewport-body` wrapper joins the markup, the chrome
     (header / footer) becomes a sibling — flexing the OUTER viewport
     would line up header + body horizontally instead of stacking them.
     The `:not(:has(> .ds-doc-viewport-body))` guard keeps the outer rule
     from firing when a body wrapper is present, so the chrome layout
     stays vertical and the body gets the layout treatment instead.

     :has() is supported in every modern browser (Chrome ≥105, Safari
     ≥15.4, Firefox ≥121). Older browsers fall back to the outer rule
     applying both — visual glitch, not a crash. */
.ds-doc-viewport--center:not(:has(> .ds-doc-viewport-body)),
.ds-doc-viewport--center > .ds-doc-viewport-body {
  display: flex;
  align-items: center;
  justify-content: center;
}

.ds-doc-viewport--stack:not(:has(> .ds-doc-viewport-body)),
.ds-doc-viewport--stack > .ds-doc-viewport-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-16);
}
.ds-doc-viewport--stack-lg:not(:has(> .ds-doc-viewport-body)),
.ds-doc-viewport--stack-lg > .ds-doc-viewport-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-24);
}

.ds-doc-viewport--row:not(:has(> .ds-doc-viewport-body)),
.ds-doc-viewport--row > .ds-doc-viewport-body {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-16);
  align-items: center;
}

/* ── Height — min-height floor for demos with dynamic content. */
.ds-doc-viewport--tall { min-height: 400px; }

/* ── DEPRECATED — viewport chrome strips ─────────────────────────
   May 2026: the entire `.ds-doc-viewport-header` / `-footer` /
   `-label` / `-caption` family was retired. The strips were
   redundant — every consumer placed an uppercase mono caption above
   the demo describing what variants were inside, but the card's
   own `.ds-doc-card__caption` already covered that role, and within
   each demo the variants carried their own eyebrow ("Paper",
   "Glass", "Primary · brand"). Two layers of labeling for the same
   information made the catalog feel overdocumented and pushed the
   actual demos down the screen.

   New convention: variant labels live inside the demo via
   `.ds-doc-eyebrow` above each variant. The card's caption
   describes the overall card. No strip on top of the viewport.

   Rules kept here as harmless dead code — no current consumers, no
   render impact. Remove in next sweep when validator confirms zero
   usage across all .html files in the repo. */
.ds-doc-viewport-header,
.ds-doc-viewport-footer,
.ds-doc-viewport-label,
.ds-doc-viewport-caption { display: none; }

/* Controls slot — flex row. Accepts any DS button/control; the
   slot just handles alignment + gap. */
.ds-doc-viewport-controls {
  display: inline-flex;
  align-items: center;
  gap: var(--space-4);
}

/* Canonical micro-button for quick action icons (refresh, open,
   inspect, reset). 28×28 fits the header's tight rhythm; hover tint
   confirms "this is clickable" to readers. */
.ds-doc-viewport-control {
  --control-size: var(--space-32);
  width: var(--control-size);
  height: var(--control-size);
  padding: 0;
  border: none;
  border-radius: var(--space-8);
  background: transparent;
  color: var(--text-muted);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
  font: inherit;
}
.ds-doc-viewport-control:hover {
  background: color-mix(in oklch, var(--text-primary) 8%, transparent);
  color: var(--text-primary);
}
.ds-doc-viewport-control:focus-visible {
  outline: 2px solid var(--accent-primary);
  outline-offset: 2px;
}
.ds-doc-viewport-control.is-active {
  background: color-mix(in oklch, var(--accent-primary) 15%, transparent);
  color: var(--accent-primary);
}
.ds-doc-viewport-control svg {
  width: var(--space-12);
  height: var(--space-12);
  display: block;
}

/* Body — the content slot. No box model of its own; the viewport's
   padding provides breathing room. Layout variants above target
   this element so consumers can group wrapped content. */
.ds-doc-viewport-body {
  min-width: 0;
}

/* ─────────────────────────────────────────────────────────────────
   .ds-doc-viewport--tabbed — tabbed multi-pane variant
   ─────────────────────────────────────────────────────────────────
   Extends `.ds-doc-viewport` with a tab strip at the top and one pane
   per tab. Used heavily by `.ds-doc-card` cards in the catalog
   (preview + markup + specs + anatomy + rules + source share one
   surface) but reusable anywhere a tabbed preview surface fits —
   product code playgrounds, marketing demo carousels, anywhere a
   single visual unit needs to show multiple related views.

   Anatomy:
     .ds-doc-viewport.ds-doc-viewport--tabbed
       .ds-doc-viewport-tabs                        edge-to-edge tab strip
         .ds-doc-viewport-tab[data-tab="X"]         one button per pane
       .ds-doc-viewport-pane[data-pane="X"]         pane content; hidden if not active
       .ds-doc-viewport-pane[data-pane="X"][hidden] inactive panes use the HTML hidden attr

   Wiring is via `shared.js → initDsCompDoc()` (the catalog uses this
   same function to drive any tabbed viewport on the page). Click a
   tab → set is-active on the tab, swap visibility of panes by data-tab
   ↔ data-pane match. State stays per-instance (each tabbed viewport
   has its own active tab).

   Nesting note: a `.ds-doc-viewport-pane` MAY contain another `.ds-doc-viewport`
   inside it — only one pane is visible at a time, so there's no
   compositing seam between two stacked viewports.

   The "preview" pane is conventional but not required — name your
   panes whatever the section needs. The catalog uses the canonical
   six (preview / markup / specs / anatomy / rules / source).
*/
.ds-doc-viewport--tabbed {
  --vp-pad: 0;            /* Panes carry their own padding via --vp-pane-pad. */
  --vp-pane-pad: var(--space-32);
  padding: 0;
  /* Framed-panel chrome — tabbed cards are the ONLY viewport variant that
     ships chrome by default; the tab strip needs a containing surface to
     read as a panel (not a floating row of buttons). Non-tabbed viewports
     (--stack, --row, --center, bare) inherit a transparent base.
     Light/dark drop intensities flip via the [data-theme="light"]
     override below. */
  background: var(--surface-raised);
  border: 1px solid var(--vp-border);
  border-radius: var(--vp-radius);
  overflow: hidden;
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.04),
    0 12px 40px -16px color-mix(in oklch, #000 35%, transparent);
}
[data-theme="light"] .ds-doc-viewport--tabbed {
  box-shadow:
    0 12px 40px -16px color-mix(in oklch, #000 12%, transparent);
}

.ds-doc-viewport-tabs {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  background: color-mix(in oklch, var(--text-primary) 3%, transparent);
  border-bottom: 1px solid color-mix(in oklch, var(--text-primary) 6%, transparent);
}

.ds-doc-viewport-tab {
  display: inline-flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-12) var(--space-16);
  background: transparent;
  border: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;       /* Active tab's underline overlaps the strip's bottom border so it sits flush. */
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-faint);
  cursor: pointer;
  transition: color 0.12s ease, background 0.12s ease, border-color 0.12s ease;
}

.ds-doc-viewport-tab:hover {
  color: var(--text-primary);
  background: color-mix(in oklch, var(--text-primary) 4%, transparent);
}

.ds-doc-viewport-tab:focus-visible {
  outline: 2px solid var(--accent-primary);
  outline-offset: -2px;
}

.ds-doc-viewport-tab.is-active,
.ds-doc-viewport-tab[aria-selected="true"] {
  color: var(--accent-primary-mid);
  background: var(--surface-raised);
  border-bottom-color: var(--accent-primary);
}

@media (prefers-reduced-motion: reduce) {
  .ds-doc-viewport-tab {
    transition: none;
  }
}

/* Pane — the content area below the tabs. Padding via --vp-pane-pad
   so consumers can override per-instance. Hidden by default unless
   `.is-active` (the JS toggles this via the HTML `hidden` attribute,
   so the CSS only handles fallback). */
.ds-doc-viewport-pane {
  padding: var(--vp-pane-pad);
}

.ds-doc-viewport-pane[hidden] {
  display: none !important;
}

/* Code-style panes get a subtly darker surface so monospace reads
   sharply against it. Author the pane with `data-pane="markup"` or
   `data-pane="source"` and the variant kicks in. */
.ds-doc-viewport-pane[data-pane="markup"],
.ds-doc-viewport-pane[data-pane="source"] {
  background: color-mix(in oklch, var(--text-primary) 4%, transparent);
}

/* Page-level Detailed mode — RETIRED in the v3 controlbar redesign.
   The toggle that drove this class doesn't exist anymore; the per-card
   tab strip handles pane navigation. The print-specific rule below
   stays — when a card is exported (Cmd+P), every pane should print
   so the artifact is self-contained. */

@media print {
  .ds-doc-viewport-pane[hidden] { display: block !important; }
  .ds-doc-viewport-tabs { display: none; }
}

@media (max-width: 600px) {
  .ds-doc-viewport--tabbed { --vp-pane-pad: var(--space-20); }
  .ds-doc-viewport-tab { padding: var(--space-8) var(--space-12); }
}

/* ════════════════════════════════════════════════════════════════
   .ds-doc-card — canonical component-documentation primitive
   ════════════════════════════════════════════════════════════════
   Tabbed-viewport wrapper for every component section in the catalog.
   The card's identity (label + caption) sits ABOVE the frame; a tabbed
   `.ds-doc-viewport.ds-doc-viewport--tabbed` body holds Preview + Markup +
   Specs + Anatomy + Rules + Source.

   AUDIENCE PRIORITY (drives every visual decision below)
     1. Designers     — primary. Scan a catalog page for the variant
                        name + a glimpse of the live preview. They want
                        to recognise components by NAME, not read prose
                        descriptions.
     2. Engineering   — secondary. Click into Markup / Specs / Source
                        for the engineer-facing detail. They came in
                        knowing the component name.
     3. Marketing     — tertiary. Use the catalog as a brand reference
                        — what's our button style, what's our card
                        treatment, what's the accent palette doing
                        right now. They need legible names, not jargon.

     (AI / scrapers parse the DOM directly; they don't compete for
     visual space and the `hidden` attribute on inactive panes keeps
     every byte of content discoverable.)

   The hierarchy below is tuned to that ordering: NAME is dominant,
   description is supporting, engineer detail is tucked behind the
   tab strip.

   Why progressive disclosure (and not just two pages):
   - Designers don't want to wade through code blocks and spec tables
     between previews — the catalog turns into a wall of monospace.
   - Engineers need markup / tokens / source — but they're a click away,
     not a separate URL away.
   - AI scraping the page source finds every byte of detail; the
     `hidden` attribute keeps inactive panes parseable while visually
     hidden.
   - Screen readers, find-in-page, and copy-paste all keep working —
     nothing is removed, everything is just behind a tab.

   ANATOMY
     .ds-doc-card                       extends .comp-section — the section wrapper
       .ds-doc-card__head               label + caption stack (always visible)
         .ds-doc-card__label            component / variant NAME — h3-scale, primary colour
         .ds-doc-card__caption          supporting line; ≤140 chars; sm size, muted colour
       .ds-doc-viewport.ds-doc-viewport--tabbed tabbed frame — Preview + the engineer-facing tabs

   The frame is documented at #viewport-tabbed in the catalog. The tabs
   `Preview · Markup · Specs · Anatomy · Rules · Source` are canonical;
   non-canonical pane names get a validator warning.

   PAGE-LEVEL TOGGLE — RETIRED
     The catalog used to ship a Concise/Detailed page-level toggle
     that expanded every pane on every card simultaneously. Removed
     in the v3 controlbar redesign because:
       • Designers (audience #1) never wanted Detailed mode.
       • Engineers focus on one card at a time and the per-card tab
         strip is faster than scrolling 80×6 stacked panes.
       • AI scrapers read every pane from the DOM regardless of
         visibility (the `hidden` attribute keeps content parseable).
     The CSS hooks (`.ds-doc-mode--detailed`) are gone too — search
     the file and you won't find them. The print rule still un-hides
     panes when a card is exported, which is enough.
*/

/* ── HEAD — label + caption sit OUTSIDE the viewport frame so the
   card's identity is legible BEFORE the eye lands on the frame.

   Hierarchy is intentional and answers Audience #1 (designers):

     LABEL      h3-scale (24 px) · sans · weight 600 · primary text
                = the dominant element. Designers scanning the catalog
                  read this first. Inline <code> inside the label keeps
                  its mono treatment, which makes the actual class name
                  (.badge, .btn-primary, …) pop within the heading.

     CAPTION    sm (14 px) · sans · weight 400 · secondary text
                = the supporting line. Adds context beyond what the
                  name already implies — variant counts, when-to-use,
                  the canonical sibling class. Designers don't NEED to
                  read it; engineers and marketing skim it.

   Earlier versions had this inverted (label as a tiny mono uppercase
   eyebrow, caption as full body weight) — designers couldn't pre-
   attentively tell one card from another, and the prose pulled focus
   away from the variant name. The flip below is the result. */
.ds-doc-card__head {
  /* Documentation rhythm — the head wraps the label (26 px / weight
     600) and the caption (16 px / weight 400). Two values matter:

       gap: --space-8 (was 6 px) — slightly more breathing between
            the bigger label and bigger caption (both bumped Apr 2026).
            6 px felt cramped at the new sizes.

       margin-bottom: --space-24 (was 16 px) — the gap from caption
            to the first pane / preview content. Pairs with the rest
            of the documentation rhythm (eyebrow→title 12 / title→desc
            20 / desc→cards 64 / head→content 24). See design.md §13
            → Documentation rhythm. */
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
  margin-bottom: var(--space-24);
}

.ds-doc-card__label {
  /* Slightly above h3 scale — bumped from 24 → 26 px (Apr 2026) so
     the variant name has more presence. Designers scan the catalog
     by name alone; the heading is the primary surface and deserves
     the visual weight. Sans (not mono) for the running text; the
     inline <code> chips override the shared chip rule below to drop
     to a quieter mono tag (no outline, no background, lighter
     weight) so they complement the noun phrase instead of competing
     with it.

     Letter-spacing -0.02em — slightly tighter than Figtree's default
     metrics. At 24 px (--text-h3) sans 600, default tracking reads as
     "safe / web default"; tightening to -0.02em reads as "authored
     heading", lining up with the catalog's display headlines
     (--tracking-tight). Subtle but compounds across 90+ cards.

     Line-height: --lh-h3 (32 px) gives a ratio of ~1.33 at 24 px —
     tight enough to read as a heading, loose enough to avoid
     descender clipping on g/y/p. */
  font-family: var(--font-sans);
  font-size: var(--text-h3);
  font-weight: var(--fw-semibold);
  line-height: var(--lh-h3);
  letter-spacing: var(--tracking-firm);
  color: var(--text-primary);
  margin: 0;
}

/* See `.ds-doc-card__label code` quiet override AFTER the shared
   `.code-chip` rule below — it has to come later in source order to
   win the cascade (same selector specificity). */

.ds-doc-card__caption {
  /* Supporting line — prose context for the variant. Bumped from
     14 → 16 px (Apr 2026) to match body prose; reads comfortably as
     a sentence rather than as a small caption. The NAME above is
     still what designers scan first (it's 26 px / weight 600); this
     line is what they read when they want a second of context.

     No max-width — let the parent container dictate line length. The
     viewport surrounding the card already provides the comfortable
     reading width; an additional 720 px cap here forced the caption
     to wrap before the natural container boundary, leaving awkward
     whitespace on wider surfaces. */
  font-size: var(--text-body);
  font-weight: var(--fw-regular);
  color: var(--text-secondary);
  line-height: 1.55;
  margin: 0;
}

/* ── Card-to-card separation — three levels of hierarchy ───────────
   The catalog has three nested layers, each with its own separation
   recipe. Apply consistently across every DS surface that uses
   .ds-doc-card — catalog, dashboard, brand-guide, motion, etc.

   1. SECTION ↔ SECTION (top-level — Buttons, Cards, Forms, …)
      Handled by .section padding (var(--pad-section-y-lg) = 120px
      each side = 240px between sections) plus an optional
      .section-divider between them. Already strong; nothing here.

   2. COMPONENT-GROUP ↔ COMPONENT-GROUP (within a section)
      When a section groups related variants under sub-headings
      (e.g. "Primary actions" / "Secondary actions" inside #buttons),
      the contributor uses .ds-doc-subsection or a regular <h4>.
      Spacing is identical to card ↔ card.

   3. VARIANT-CARD ↔ VARIANT-CARD (the painful case)
      Adjacent .ds-doc-card siblings used to stack with effectively
      zero visual boundary — designers couldn't tell where one card
      ended and the next began. The fix below adds:
        - generous vertical breathing room (80px gap)
        - a quiet hairline divider centred in the gap
        - extra padding-top inside the card so the divider doesn't
          crowd the variant name
      The first card in a section gets only the natural breathing
      room from .section-desc above it; only ADJACENT cards trigger
      the divider via the + sibling combinator.

   Why this lives in CSS, not in the markup: contributors should NOT
   hand-place <hr> or extra spacers between cards. The primitive
   handles separation automatically; if you find yourself authoring
   a divider, the CSS isn't doing its job — fix it here. */

.ds-doc-card {
  /* First card in a section: section header + section-desc already
     give breathing room. A small top margin tightens the relationship
     between the section intro and its first card. */
  margin-top: var(--space-32);
  position: relative;
}

.ds-doc-card + .ds-doc-card {
  /* Adjacent variant cards — the case the user complained about.
     Strong vertical separation + a hairline divider centred in the
     gap so the boundary is unmistakable but never noisy. */
  margin-top: var(--space-80);
  padding-top: var(--space-48);
  border-top: 1px solid color-mix(in oklch, var(--text-primary) 10%, transparent);
}

/* If a section uses an explicit subsection heading between groups of
   variants, the heading itself separates them — the next card under
   the heading should NOT also draw the hairline divider. */
.ds-doc-subsection + .ds-doc-card,
h4 + .ds-doc-card {
  margin-top: var(--space-32);
  padding-top: 0;
  border-top: 0;
}

/* Optional component-group eyebrow — when a section wants explicit
   sub-grouping (e.g. "PRIMARY ACTIONS", "SECONDARY ACTIONS"), use
   this small uppercase mono caption above the first card of each
   group. Quiet enough to read as a category label, not as a heading. */
.ds-doc-subsection {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-faint);
  margin: var(--space-80) 0 var(--space-16);
  padding-top: var(--space-24);
  border-top: 1px solid color-mix(in oklch, var(--text-primary) 14%, transparent);
}

/* The very first subsection in a section doesn't need the top rule —
   the section header is doing that job. */
.section-desc + .ds-doc-subsection,
.section-title + .ds-doc-subsection {
  margin-top: var(--space-40);
  padding-top: 0;
  border-top: 0;
}

/* Print: collapse the divider so cards stack cleanly when exported. */
@media print {
  .ds-doc-card + .ds-doc-card {
    border-top: 0;
    margin-top: var(--space-32);
    padding-top: var(--space-32);
  }
}

/* Mobile: tighten the gap so we don't waste a screen-and-a-half of
   blank space between every card on a phone. */
@media (max-width: 600px) {
  .ds-doc-card + .ds-doc-card {
    margin-top: var(--space-48);
    padding-top: var(--space-32);
  }
}

/* ════════════════════════════════════════════════════════════════
   .ds-doc-* — documentation primitives (code blocks, tables, prose)
   ════════════════════════════════════════════════════════════════
   Reusable primitives for every documentation surface — catalog,
   dashboard, brand-guide, motion, AI, competitors, anywhere a DS
   page needs to render code, tables, prose, or lists.

   Promoted to styles.css from a per-page <style> block so that:
   - Every DS page gets the same treatment without copy-paste.
   - Pane-aware behaviour can be expressed in one place.
   - The validator + skill template can rely on a single source of truth.

   THE BIG RULE — pane awareness
     When these primitives sit INSIDE a .ds-doc-viewport-pane (i.e., inside
     a tabbed `.ds-doc-card` card), the pane is the container. The
     primitives DROP their own border, radius, background, and padding
     so you never see "container in a container" — a code block
     wrapped in a code block, or a table wrapped in a bordered box
     wrapped in a tinted pane.

     When used STANDALONE (outside a pane), they keep their full
     chrome so they still read as a self-contained element. Both
     paths work; the primitive figures it out.

   COLOR-CODE TOKENS
     Inside any .ds-doc-codeblock, wrap syntax tokens in spans with
     these classes for consistent colour-coding:
       <span class="comment"> — comments (italic, muted)
       <span class="string">  — string literals (accent-secondary)
       <span class="tag">     — HTML tag names (accent-primary)
       <span class="attr">    — HTML attributes (accent-tertiary)
       <span class="value">   — attribute values, often = strings
       <span class="keyword"> — JS/CSS keywords (accent-primary, bold)
       <span class="path">    — file paths, URLs (accent-tertiary)
       <span class="punct">   — punctuation, brackets (muted)

     Don't invent new tokens for one-off colours. The eight above
     cover HTML, CSS, JS, JSON, and shell snippets the DS uses.

   OVERFLOW
     Code blocks default to white-space: pre + overflow-x: auto so
     long lines force a quiet horizontal scrollbar. Mobile overrides
     to white-space: pre-wrap so phones don't get a 2-line scrollbar.
*/

/* ── Token ladder ────────────────────────────────────────────
   Vertical stack of [name][value][bar] rows for visualizing a
   token scale. The catalog used to ship two hand-rolled
   inline-styled ladders (spacing scale at 120/60/8px, surface
   widths at 160/80/24px) — each consumer reinventing the grid,
   gap, bar height, color, and radius. This single primitive
   replaces both.

   Audience: documentation only. The .ds-doc- prefix marks it as
   catalog-meta — never use it on a product or marketing surface.

   Per-row API: set --bar-extent on .ds-doc-ladder-row to drive
   the bar's width. Use a literal length (px) when the bar IS the
   token's size (the spacing scale, where 16px-of-bar means 16px-
   of-gap). Use a percentage to scale proportionally inside the
   row's bar column. max-width:100% on the bar prevents overflow
   in either case.

   Column widths default to 160px name + 80px value, sized for the
   longest spacing-family token names and 4-digit pixel values.
   Override per-card by setting --token-ladder-name and
   --token-ladder-value on the .ds-doc-ladder block.

   Documented at design-system/index.html#token-ladder. Keep this
   comment + that catalog entry + design.md in sync if anything
   moves. */
.ds-doc-ladder {
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
  font-family: var(--font-mono);
  font-size: var(--text-caption);
}
.ds-doc-ladder-row {
  display: grid;
  /* minmax(0, 1fr) lets the bar column shrink below the bar's
     intrinsic width — without it, a 1080px bar inflates the
     1fr track to 1080px and `max-width: 100%` becomes a no-op. */
  grid-template-columns:
    var(--token-ladder-name, 160px)
    var(--token-ladder-value, 80px)
    minmax(0, 1fr);
  gap: var(--space-16);
  align-items: center;
}
.ds-doc-ladder-name { color: var(--text-primary); }
.ds-doc-ladder-value { color: var(--text-muted); }
.ds-doc-ladder-bar {
  display: block;
  height: var(--space-8);
  width: var(--bar-extent, 0);
  max-width: 100%;
  background: var(--accent-primary);
  border-radius: var(--space-4);
}

/* ── Device frame — screen-size reference visualization ─────────
   Catalog-meta primitive (.ds-doc- namespace) for the Foundations →
   Device resolutions & breakpoints card. Renders a monitor / laptop /
   tablet / phone silhouette at ONE shared proportional scale so the
   relative footprints read true across the whole set (a phone really
   is a sliver of a QHD monitor — the visualization shows it).

   Per-device API (inline on .ds-doc-device):
     --dw / --dh    RENDERED width/height in px, as unitless numbers
                    (= logical resolution × the stage's shared 0.12
                    scale). Data-derived from real device resolutions,
                    so — like chart coordinates — exempt from the
                    4-grid spacing ladder. Chrome (bezel, notch, base,
                    radius, gaps, labels) stays fully tokenized.
     data-logical-w the device's logical CSS width; read by the
                    viewport-ruler JS to toggle [data-fits].

   Chrome modifiers:
     --monitor  thicker bezel + centered stand (desktop displays)
     --laptop   wide keyboard-deck base (compose --notch for the
                MacBook sensor notch)
     --notch    MacBook-style top notch (laptop sub-style)
     --tablet   camera dot, medium corners, no base
     --phone    very-round corners + Dynamic Island pill
     --home     phone sub-style — top slit + home-button dot (iPhone SE)
     --punch    phone sub-style — centered punch-hole camera (Android)

   Ruler state: [data-fits="true"] accents devices whose logical width
   clears the ruler target; [data-fits="false"] dims the rest. No
   attribute = resting neutral.

   Documented at design-system/index.html#device-resolutions. Keep this
   comment + that catalog entry + design.md (Foundations → Device
   resolutions & breakpoints) in sync. */
.ds-doc-device-stage {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: center;
  gap: var(--space-32) var(--space-24);
  padding: var(--space-12) 0;
}
.ds-doc-device {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-16);
  transition: opacity 0.2s ease, filter 0.2s ease;
}
.ds-doc-device__frame {
  position: relative;
  flex: none;
  width: calc(var(--dw, 160) * 1px);
  height: calc(var(--dh, 100) * 1px);
  background: var(--surface-sunk);
  border: var(--space-2) solid var(--text-faint);
  border-radius: var(--space-8);
  box-shadow: inset 0 1px 0 0 color-mix(in oklch, var(--text-primary) 6%, transparent);
}
.ds-doc-device__label {
  text-align: center;
  max-width: calc(var(--dw, 160) * 1px + var(--space-40));
}
.ds-doc-device__name {
  font-size: var(--text-caption);
  font-weight: var(--fw-semibold);
  line-height: var(--lh-caption);
  color: var(--text-primary);
}
.ds-doc-device__res {
  display: block;
  margin-top: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  color: var(--text-muted);
}

/* Monitor — thicker bezel + centered stand */
.ds-doc-device--monitor .ds-doc-device__frame {
  border-radius: var(--space-4);
  border-width: var(--space-2);
}
.ds-doc-device--monitor .ds-doc-device__frame::after {
  content: "";
  position: absolute;
  left: 50%; top: 100%;
  transform: translateX(-50%);
  width: 16%; height: var(--space-8);
  margin-top: var(--space-2);
  background: var(--surface-raised);
  border: var(--space-2) solid var(--text-faint);
  border-top: 0;
  border-radius: 0 0 var(--space-2) var(--space-2);
}

/* Laptop — wide keyboard-deck base */
.ds-doc-device--laptop .ds-doc-device__frame { border-radius: var(--space-4); }
.ds-doc-device--laptop .ds-doc-device__frame::after {
  content: "";
  position: absolute;
  left: 50%; top: 100%;
  transform: translateX(-50%);
  width: 112%; height: var(--space-4);
  margin-top: var(--space-2);
  background: var(--surface-raised);
  border: var(--space-2) solid var(--text-faint);
  border-radius: 0 0 var(--space-4) var(--space-4);
}

/* MacBook notch — top sensor housing */
.ds-doc-device--notch .ds-doc-device__frame::before {
  content: "";
  position: absolute;
  left: 50%; top: 0;
  transform: translateX(-50%);
  width: var(--space-16); height: var(--space-2);
  background: var(--text-faint);
  border-radius: 0 0 var(--space-2) var(--space-2);
}

/* Tablet — camera dot, no base */
.ds-doc-device--tablet .ds-doc-device__frame { border-radius: var(--space-8); }
.ds-doc-device--tablet .ds-doc-device__frame::before {
  content: "";
  position: absolute;
  left: 50%; top: var(--space-2);
  transform: translateX(-50%);
  width: var(--space-2); height: var(--space-2);
  background: var(--text-faint);
  border-radius: var(--r-full);
}

/* Phone — very-round corners + Dynamic Island pill */
.ds-doc-device--phone .ds-doc-device__frame { border-radius: var(--space-8); }
.ds-doc-device--phone .ds-doc-device__frame::before {
  content: "";
  position: absolute;
  left: 50%; top: var(--space-2);
  transform: translateX(-50%);
  width: var(--space-12); height: var(--space-2);
  background: var(--text-faint);
  border-radius: var(--r-full);
}
/* iPhone SE — home button + top speaker slit (no island) */
.ds-doc-device--home .ds-doc-device__frame { border-radius: var(--space-4); }
.ds-doc-device--home .ds-doc-device__frame::before {
  width: var(--space-8); height: var(--space-2);
  border-radius: var(--r-full);
}
.ds-doc-device--home .ds-doc-device__frame::after {
  content: "";
  position: absolute;
  left: 50%; bottom: var(--space-2);
  transform: translateX(-50%);
  width: var(--space-4); height: var(--space-4);
  border: var(--space-2) solid var(--text-faint);
  border-radius: var(--r-full);
}
/* Android — centered punch-hole camera */
.ds-doc-device--punch .ds-doc-device__frame::before {
  width: var(--space-2); height: var(--space-2);
  border-radius: var(--r-full);
}

/* Interactive device frames + ruler-driven fit state.
   Each .ds-doc-device is a button (role="button" in markup) that snaps
   the ruler to its own logical width — the frames double as presets.
   Static at rest; hover / focus-visible raise an accent border + ring.
   The ruler then accents the devices it covers ([data-fits="true"]) and
   dims the rest. Same accent / focus vocabulary as the rest of the DS. */
.ds-doc-device {
  cursor: pointer;
  border-radius: var(--space-8);
  outline: none;
}
.ds-doc-device__frame {
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.ds-doc-device:hover .ds-doc-device__frame,
.ds-doc-device:focus-visible .ds-doc-device__frame {
  border-color: var(--accent-primary);
}
.ds-doc-device:focus-visible {
  box-shadow: 0 0 0 var(--space-2) color-mix(in oklch, var(--accent-primary) 40%, transparent);
}
.ds-doc-device[data-fits="false"] {
  opacity: 0.3;
  filter: grayscale(0.5);
}
.ds-doc-device[data-fits="true"] .ds-doc-device__frame {
  border-color: var(--accent-primary);
  box-shadow:
    0 0 0 var(--space-2) color-mix(in oklch, var(--accent-primary) 30%, transparent),
    inset 0 1px 0 0 color-mix(in oklch, var(--text-primary) 6%, transparent);
}
.ds-doc-device[data-fits="true"] .ds-doc-device__name { color: var(--accent-primary-mid); }

/* ── Viewport-width ruler — built on the .slider primitive ──────────
   The control is a real `.slider` (.slider-label-row + .slider-range), so
   it inherits the system track / thumb / fill (auto-wired by
   window.lifiSliderFill) and the slider's hover / focus chrome. Below it:
   a live breakpoint `.chip-accent1`, a `.muted` fit-count line, and a
   proportional Tailwind-breakpoint track whose active segment + marker
   track the value. JS: the inline IIFE next to #device-resolutions in
   design-system/index.html (reads [data-device-ruler]). */
.ds-doc-device-ruler {
  display: flex;
  flex-direction: column;
  gap: var(--space-12);
  margin-bottom: var(--space-32);
}
.ds-doc-device-ruler__meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-8) var(--space-12);
}
.ds-doc-bp-track {
  position: relative;
  display: flex;
  height: var(--space-24);
  border-radius: var(--space-4);
  overflow: hidden;
  font-family: var(--font-mono);
  font-size: var(--text-micro);
}
.ds-doc-bp-seg {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-grow: var(--seg, 1);
  flex-basis: 0;
  min-width: 0;
  color: var(--text-muted);
  background: var(--surface-sunk);
  border-right: var(--space-2) solid var(--surface-raised);
  transition: background 0.15s ease, color 0.15s ease;
}
.ds-doc-bp-seg:last-child { border-right: 0; }
.ds-doc-bp-seg[data-active] {
  background: color-mix(in oklch, var(--accent-primary) 14%, transparent);
  color: var(--accent-primary-mid);
}
.ds-doc-bp-marker {
  position: absolute;
  top: 0; bottom: 0;
  width: var(--space-2);
  background: var(--accent-primary);
  transition: left 0.1s ease;
}

/* ── Material demo block ────────────────────────────────────────
   Catalog-only primitive (`.ds-doc-` namespace). A material-aware
   rectangular block that renders the resolved `--mat-*` tokens —
   used in #materials previews to show the same shape rendered in
   each named material side-by-side.

   Usage:
     <div class="ds-doc-material-demo" data-material="paper">…</div>
     <div class="ds-doc-material-demo" data-material="glass">…</div>

   Without `data-material`, the demo wears the :root default (paper).
   The `--mat-*` resolved API is set up in :root + the [data-material]
   selector blocks above (search "MATERIALS — [data-material]").
   ────────────────────────────────────────────────────────────── */
.ds-doc-material-demo {
  background: var(--mat-bg);
  border: var(--mat-border);
  box-shadow: var(--mat-shadow), var(--mat-highlight);
  backdrop-filter: var(--mat-backdrop);
  -webkit-backdrop-filter: var(--mat-backdrop);
  border-radius: var(--space-16);
  padding: var(--space-20);
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
  min-height: var(--space-120);
  color: var(--text-primary);
  /* The demo is a thin frame around content — never let internal
     ascii artwork or tooltips bleed past the rounded corner. */
  overflow: hidden;
}
/* Tile shape — square aspect, smaller content frame. Pair with the
   default card shape above to demonstrate the material at two scales. */
.ds-doc-material-demo--tile {
  min-height: var(--space-120);
  aspect-ratio: 1 / 1;
  padding: var(--space-16);
  gap: var(--space-4);
}
/* Gradient material flips text to white — the brand gradient under
   primary text would otherwise tint-shift below WCAG. */
.ds-doc-material-demo[data-material="gradient"] {
  color: #fff;
}
.ds-doc-material-demo[data-material="gradient"] .mono,
.ds-doc-material-demo[data-material="gradient"] .ds-doc-material-demo-meta {
  color: rgba(255,255,255,0.78);
}

/* ── Iso anatomy diagram ────────────────────────────────────────
   Catalog-only teaching device. Renders an isometric, exploded
   view of a card surface so designers can see the layers that
   compose it — and which DS foundation owns each layer.

   Audience: documentation only. The .ds-doc- prefix marks it as
   catalog-meta — never use it on a product or marketing surface.

   Layout: two rows.
     1. Stage — the iso scene with floating foundation pills. Each
        pill sits at the front-right corner of its plate, labelled
        with the FOUNDATION first (Material / Surface / Elevation
        / Typography), the role second. The pill positions are
        computed from the iso pose (rotateX 58° + rotateZ -32°,
        plates spaced 100px on the Z axis) and live as fixed
        offsets from stack center via calc(50% + ...).
     2. Foundations key — a 4-card row beneath the stage that
        explains each foundation in one sentence + a token list.
        This is the deep-dive companion; the pills above are the
        instant visual association.

   Tokenisation: every layer that has a real DS counterpart pulls
   its fill from a token (--surface-page, --surface-card,
   currentColor mixes for the hairline + rim) so palette / theme
   switches are reflected in the diagram automatically. The drop-
   shadow layer is a synthetic blurred dark plate — the live
   --elev-* recipes are too subtle to read as a discrete plate at
   this scale, and the diagram's whole point is making the layer
   visible. The rule is mentioned in the Specs pane.

   Responsive: below 900px the foundations key reflows to 2x2;
   below 768px the iso rotation is dropped, plates collapse to a
   vertical flat stack, the floating pills are hidden (they'd
   overlap the flat plates), and the foundation cards stack 1-up.

   Documented at design-system/index.html#card-anatomy. */
.ds-doc-iso-card {
  display: grid;
  grid-template-rows: auto auto;
  gap: var(--space-32);
  /* No frame — sit directly on the preview pane background. */
}

.ds-doc-iso-card__stage {
  position: relative;
  height: 480px;
  perspective: 2400px;
  perspective-origin: 50% 50%;
  /* Soft brand gradient — gives the Glass material plate something
     to refract via backdrop-filter. Token-driven so it tracks the
     active palette in real time. */
  background: linear-gradient(
    135deg,
    color-mix(in oklch, var(--lifi-sapphire) 28%, transparent) 0%,
    color-mix(in oklch, var(--lifi-ink) 28%, transparent) 100%
  );
  border-radius: var(--space-16);
  overflow: hidden;
}

.ds-doc-iso-card__stack {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  /* Iconic isometric pose — rotateX tilts plates back, rotateZ
     skews them into the parallelogram form. translateY lifts the
     whole stack so the bottom plate isn't cropped. */
  transform: translateY(-4%) rotateX(58deg) rotateZ(-32deg);
}

.ds-doc-iso-card__layer {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 280px;
  height: 180px;
  margin: -90px 0 0 -140px;
  border-radius: var(--space-16);
}

/* The shadow layer — a blurred dark blob, the bottom of the
   visible stack. Synthetic value (not --elev-*) so it reads as a
   discrete plate at this scale; see comment block above. */
.ds-doc-iso-card__layer[data-iso-layer="shadow"] {
  background: oklch(0% 0 0 / 0.55);
  filter: blur(24px);
  transform: translateZ(-100px);
}

/* The surface fill — GLASS material. Translucent fill +
   backdrop-filter blur. The gradient stage bg is what the glass
   refracts; the shadow plate beneath also gets softened through.
   This illustrates the glass recipe: surface character is
   translucent, backdrop blurs what's behind, edge crispens. */
.ds-doc-iso-card__layer[data-iso-layer="fill"] {
  background: var(--surface-panel-glass);
  backdrop-filter: blur(20px) saturate(1.4);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  transform: translateZ(0);
  outline: 1px solid color-mix(in oklch, var(--text-primary) 14%, transparent);
}
[data-theme="light"] .ds-doc-iso-card__layer[data-iso-layer="edge"] {
  border: 1px solid color-mix(in oklch, var(--text-primary) 26%, transparent);
  box-shadow: inset 0 1px 0 color-mix(in oklch, var(--text-primary) 38%, transparent);
}

/* The edge layer — transparent plate, only the outline + inset
   rim are visible. Demonstrates the 1px hairline + dark-mode top
   highlight that define a card's crisp corner. */
.ds-doc-iso-card__layer[data-iso-layer="edge"] {
  background: transparent;
  border: 1px solid color-mix(in oklch, var(--text-primary) 18%, transparent);
  box-shadow: inset 0 1px 0 color-mix(in oklch, var(--text-primary) 28%, transparent);
  transform: translateZ(100px);
}

/* The content layer — text + value composition. */
.ds-doc-iso-card__layer[data-iso-layer="content"] {
  background: transparent;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 0 var(--space-24);
  gap: var(--space-4);
  transform: translateZ(200px);
}

.ds-doc-iso-card__content-eyebrow {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  color: var(--text-muted);
  letter-spacing: var(--tracking-loose);
}
.ds-doc-iso-card__content-value {
  font-size: var(--text-h2);
  font-weight: var(--fw-bold);
  color: var(--text-primary);
  line-height: 1.1;
  letter-spacing: var(--tracking-firm);
}
.ds-doc-iso-card__content-sub {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  line-height: 1.4;
}

/* ── Numbered callout badges — embedded ON the iso plane ──────
   Flat discs that sit ON each plate, fully on the iso grid: the
   chip body AND its digit type both tilt with the plate. Same
   alignment as the content layer's "$1.24B" type, which also lies
   on the plane.

   Architecture:
     · Badge is a CHILD of __stack so it inherits the parent's
       rotateX(58) rotateZ(-32) — body and text both tilt.
     · Badge's translate3d positions it INSIDE the plate at
       (100, 50, ZN) — 40px in from the front-right corner so the
       chip sits on the plate rather than overhanging it.
     · Same Z as the matching plate → coplanar.
     · No counter-rotation on the digit span. The digits tilt
       along the iso axes like any other on-surface type. Larger
       80px badge + --text-h3 font compensate for the projection
       squish (height ≈ 0.53× after rotateX 58°) so the digits
       remain readable. */
.ds-doc-iso-card__pill {
  position: absolute;
  top: 50%;
  left: 50%;
  width: var(--space-40);
  height: var(--space-40);
  margin: calc(-1 * var(--space-20)) 0 0 calc(-1 * var(--space-20));
  border-radius: 50%;
  background: var(--surface-card);
  border: 1px solid color-mix(in oklch, var(--text-primary) 22%, transparent);
  box-shadow: var(--elev-1);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 5;
  transform-style: preserve-3d;
}
[data-theme="light"] .ds-doc-iso-card__pill {
  background: var(--surface-page);
  border-color: color-mix(in oklch, var(--text-primary) 28%, transparent);
}

/* Digits lie on the iso plane — no counter-rotation. Type tilts
   with the chip body and inherits the parent's iso transform. */
.ds-doc-iso-card__pill > span {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  font-weight: var(--fw-bold);
  color: var(--text-primary);
  letter-spacing: var(--tracking-loose);
  line-height: 1;
}

/* In-plate positions in stack-local coords.
   X=100 (40px in from right edge of 280-wide plate),
   Y=+50 (40px up from front edge of 180-tall plate),
   Z = each plate's translateZ so the badge is coplanar with it. */
.ds-doc-iso-card__pill[data-pill="shadow"]   { transform: translate3d(100px, 50px, -100px); }
.ds-doc-iso-card__pill[data-pill="fill"]     { transform: translate3d(100px, 50px, 0); }
.ds-doc-iso-card__pill[data-pill="edge"]     { transform: translate3d(100px, 50px, 100px); }
.ds-doc-iso-card__pill[data-pill="content"]  { transform: translate3d(100px, 50px, 200px); }

/* ── Foundations key — vertical stack of .action-card primitives ──
   Each foundation is rendered as one .action-card (existing DS
   primitive — anchor-link with icon + title + description + arrow).
   The number badge in the icon slot is the only catalog-meta bit;
   .action-card itself owns all the chrome (paper material, hover,
   accent halo, focus ring). */
.ds-doc-iso-card__key {
  display: flex;
  flex-direction: column;
  gap: var(--space-12);
}

/* Number badge inside .action-card-icon. The icon slot is 44px square
   with an accent gradient bg; we drop a bold mono number in. */
.ds-doc-iso-card__num {
  font-family: var(--font-mono);
  font-size: var(--text-body);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-loose);
  color: var(--accent-primary);
}

/* Below 768px — flatten the iso, hide the floating pills (they'd
   overlap the flat plates), stack the foundation key 1-up. */
@media (max-width: 768px) {
  .ds-doc-iso-card {
    gap: var(--space-24);
    padding: var(--space-24) var(--space-16);
  }
  .ds-doc-iso-card__stage {
    height: auto;
    perspective: none;
  }
  .ds-doc-iso-card__stack {
    position: static;
    transform: none;
    display: flex;
    flex-direction: column-reverse;
    gap: var(--space-8);
  }
  .ds-doc-iso-card__layer {
    position: static;
    margin: 0;
    transform: none !important;
    height: 56px;
    width: 100%;
  }
  .ds-doc-iso-card__layer[data-iso-layer="page"] {
    width: 100%;
    height: 56px;
    margin: 0;
  }
  .ds-doc-iso-card__layer[data-iso-layer="content"] {
    height: auto;
    padding: var(--space-16) var(--space-20);
  }
  .ds-doc-iso-card__layer[data-iso-layer="shadow"] {
    /* Blur is too aggressive at flat 56px — bleeds past the row.
       Tone it down for the flat layout. */
    filter: blur(8px);
    opacity: 0.7;
  }
  .ds-doc-iso-card__pill {
    display: none;
  }
}

/* ── Code block ─────────────────────────────────────────────── */
.ds-doc-codeblock {
  /* Standalone chrome — kept on the element so .ds-doc-codeblock
     read fine outside a pane (legacy authoring guides, README
     snippets, etc.). Pane-aware override below strips this. */
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  line-height: 1.55;
  color: var(--on-alpha-default);
  background: var(--alpha-subtle);
  border-radius: 12px;
  padding: var(--space-16) var(--space-20);
  margin: 0;
  overflow-x: auto;
  white-space: pre;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
  scrollbar-color: color-mix(in oklch, currentColor 25%, transparent) transparent;
  /* Tab characters render at 2 spaces — DS authors use 2-space
     indents in templates, the visual matches if a tab sneaks in. */
  tab-size: 2;
}

/* WebKit scrollbar — thin, quiet, matches the codeblock surface. */
.ds-doc-codeblock::-webkit-scrollbar { height: var(--space-8); }
.ds-doc-codeblock::-webkit-scrollbar-thumb {
  background: color-mix(in oklch, currentColor 25%, transparent);
  border-radius: var(--space-4);
}
.ds-doc-codeblock::-webkit-scrollbar-track { background: transparent; }

/* The element is often <pre class="ds-doc-codeblock"><code>…</code></pre>
   — reset browser <pre> margin so our margin: 0 wins, and let the
   nested <code> inherit everything. */
pre.ds-doc-codeblock { margin: 0; }
.ds-doc-codeblock code {
  font-family: inherit;
  font-size: inherit;
  color: inherit;
  background: transparent;
  padding: 0;
  border-radius: 0;
}

/* PANE-AWARE — drop duplicate chrome. The .ds-doc-viewport-pane already
   provides padding via --vp-pane-pad, and code-style panes already
   carry a tinted background via the pane[data-pane="markup|source"]
   selector earlier in this file. So inside a pane the codeblock is
   just type — no double border, no double background, no margin. */
.ds-doc-viewport-pane .ds-doc-codeblock,
.ds-doc-viewport-pane pre.ds-doc-codeblock {
  background: transparent;
  border: 0;
  border-radius: 0;
  padding: 0;
  max-width: none;
}

/* Color-code tokens — consistent across every DS page. */
.ds-doc-codeblock .comment  { color: var(--on-alpha-muted); font-style: italic; }
.ds-doc-codeblock .string   { color: var(--accent-secondary); }
.ds-doc-codeblock .tag      { color: var(--accent-primary); }
.ds-doc-codeblock .attr     { color: var(--accent-tertiary); }
.ds-doc-codeblock .value    { color: var(--accent-secondary); }
.ds-doc-codeblock .keyword  { color: var(--accent-primary); font-weight: var(--fw-semibold); }
.ds-doc-codeblock .path     { color: var(--accent-tertiary); }
.ds-doc-codeblock .punct    { color: var(--on-alpha-muted); }

/* ── Table wrap ─────────────────────────────────────────────── */
.ds-doc-table-wrap {
  /* Standalone chrome — bordered card around a data-table. */
  border: 1px solid var(--border-subtle);
  border-radius: 12px;
  overflow: hidden;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
.ds-doc-table-wrap + .ds-doc-table-wrap { margin-top: var(--space-16); }

/* PANE-AWARE — pane is the container; the wrap drops its border. */
.ds-doc-viewport-pane .ds-doc-table-wrap {
  border: 0;
  border-radius: 0;
  /* Keep overflow-x for tables wider than the pane — the data still
     scrolls inside the pane, no visual container chrome. */
}

/* ── Prose ──────────────────────────────────────────────────── */
.ds-doc-text {
  color: var(--text-muted);
  font-size: var(--text-sm);
  /* Unitless 1.6 — at 14 px body that's 22.4 px leading, the comfortable
     reading rhythm for documentation prose. The fixed-pixel `--lh-body`
     (24 px) becomes a 1.71 ratio at 14 px, which feels airy + slows
     scanning. Unitless scales correctly when `<strong>`, inline `<code>`
     chips, or font-size-altered children are mixed in. */
  line-height: 1.6;
  margin: 0 0 var(--space-16);
  max-width: 640px;
}
.ds-doc-text:last-child { margin-bottom: 0; }
.ds-doc-text strong { color: var(--text-secondary); font-weight: var(--fw-semibold); }
.ds-doc-text + .ds-doc-text { margin-top: 0; }

/* In a pane, prose uses the pane's full reading width. */
.ds-doc-viewport-pane .ds-doc-text { max-width: none; }

/* ── List ───────────────────────────────────────────────────── */
.ds-doc-list {
  margin: 0;
  padding-left: var(--space-24);
  font-size: var(--text-sm);
  /* Same 1.6 unitless rationale as .ds-doc-text — keeps lists readable
     at sm body without the airy 1.71 ratio of fixed `--lh-body`. */
  line-height: 1.6;
  color: var(--text-muted);
}
.ds-doc-list > li { margin-bottom: var(--space-8); }
.ds-doc-list > li:last-child { margin-bottom: 0; }
.ds-doc-list strong { color: var(--text-secondary); font-weight: var(--fw-semibold); }
.ds-doc-list code  { color: var(--text-primary); }
.ds-doc-list + .ds-doc-text { margin-top: var(--space-16); }

/* ════════════════════════════════════════════════════════════════
   Table family — .data-table + modifiers
   ════════════════════════════════════════════════════════════════
   Promoted from page-local index.html (May 2026) to a single
   cascade source. Every DS surface — catalog, dashboard, brand
   guide, personas, future product surfaces — consumes the same
   base and the same modifiers.

   Base shape:
     <div class="ds-doc-table-wrap">      // wrapper (horizontal scroll)
       <table class="data-table">
         <thead><tr><th>Col</th></tr></thead>
         <tbody><tr><td>Cell</td></tr></tbody>
         <tfoot>…</tfoot>                  // optional totals / pagination
       </table>
     </div>

   Size ladder (four tiers — --sm / --md / default / --xl):
     .data-table--sm  → th 12 / td 14 · pad 8/12   (compact / dense lists)
     .data-table--md  → th 14 / td 16 · pad 12/16  (cozy — denser default)
     .data-table      → th 14 / td 16 · pad 16/20  (default — comfortable)
     .data-table--xl  → th 16 / td 18 · pad 20/24  (spacious — marketing, hero)
   --md reuses default's 14/16 type (no token between 14 and 16) and steps
   density only; the other tiers step header + value one notch on the type scale.

   Modifiers (combine freely):
     .data-table--striped     zebra rows on tbody
     .data-table--bordered    vertical + horizontal cell borders
     .data-table--borderless  no row borders (use with --striped or
                              inside cards that already provide chrome)
     .data-table--no-hover    opt out of row hover (static spec tables)
     .data-table--sticky-head thead stays put while tbody scrolls
                              (wrap parent in a scroll container)
     .data-table--sticky-col  first column stays put on horizontal scroll
                              (pairs with .ds-doc-table-wrap)
     .data-table--align-numeric  default right-align for td (currencies /
                              dashboards); override per-cell with .ta-l

   Row + cell state classes:
     tr.is-selected           tinted row, semantic accent
     tr.is-expanded           row in "open" state; pair with a
                              sibling tr.data-table__detail-row
     tr.data-table__detail-row  full-width expansion content
     th.is-sortable           clickable header; chevron auto-renders
                              based on aria-sort="ascending|descending"
     td.mono                  contextual alts off (NO font swap — sans by
                              default; mono is opt-in via .data-cell-text--mono)
     td.ta-r / td.ta-c        text-align: right / center

   Typography baked in:
     - standard SANS for every string by default — no monotype in tables
       (June 2026 brand decision; a specific column can opt into mono later
       via the family-only .data-cell-text--mono modifier on a rich table)
     - tabular-nums on the whole table  (numeric columns align by digit)
     - three-level emphasis ladder: muted (th / secondary cells) · default
       (td, --text-primary) · bold (<strong>, semibold + primary)
     - <strong> emphasis at 600  (not browser-default 700, which is too
       heavy at table density in Figtree)
   ──────────────────────────────────────────────────────────── */
.data-table {
  /* ── Row-state slot tokens ────────────────────────────────────
     Every row-level state (hover · active · striped · selected ·
     expanded) consumes a named --data-table-row-*-bg slot. Default
     values route through the canonical surface / accent tokens so
     the family auto-flips light/dark with the rest of the system.

     Override on .data-table (or any ancestor) to retint without
     forking the base rule — e.g. a future Lend domain re-routes
     selected to its accent: `.lend-table { --data-table-row-selected-tone:
     var(--lend-accent); }`. The state rules below read from these
     slots; never bake a literal `var(--surface-sunk)` / color-mix
     directly into a row-state declaration.

     The same shape powers .tile / .brand-card / .accent-card — see
     CLAUDE.md → "Variant-aware states — route through a --*-tone
     token, never bake --accent-primary".

     May 2026 — row states aligned with the canonical List-row state
     recipe (CLAUDE.md → "List-row state recipe — variant of the
     card recipe, fill-only"). Hover flipped from var(--surface-sunk)
     to translucent --text-primary 4% (neutral, composes universally).
     Selected reduced from 8% → 6% accent. Active added at 10% accent
     (was missing). Striped + expanded keep their existing values —
     they're table-specific concepts not in the row recipe. */
  --data-table-row-hover-bg:    color-mix(in oklch, var(--text-primary) 4%, transparent);
  --data-table-row-active-bg:   color-mix(in oklch, var(--data-table-selected-tone) 10%, transparent);
  --data-table-row-striped-bg:  color-mix(in oklch, var(--text-primary) 3%, transparent);
  --data-table-row-expanded-bg: color-mix(in oklch, var(--text-primary) 2%, transparent);
  --data-table-selected-tone:   var(--accent-primary);
  --data-table-row-selected-bg: color-mix(in oklch, var(--data-table-selected-tone) 6%, transparent);

  width: 100%;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1, "kern" 1;
}
/* Header voice — the calm, normal-case muted line the Orders / rich tables
   pioneered, now the BASE table default (promoted off .data-table--rich thead th,
   June 2026). Regular weight, normal-case, no tracking, muted ink, so the data is
   the star and the header recedes. NOT the old uppercase/tracked/semibold caption
   — that read as chrome competing with the values. (The --sm tier is the ONE
   exemption — its 10px micro header forces UPPERCASE + tracking for legibility;
   see the size ladder.) Height is pinned on the 4-grid + box-sizing + vertical-
   align so the header row is an exact token height (default 48); horizontal-only
   padding — the height owns the vertical rhythm. */
.data-table th {
  font-size: var(--text-sm);
  font-weight: var(--fw-regular);
  letter-spacing: var(--tracking-normal);
  text-transform: none;
  color: var(--text-muted);
  text-align: left;
  height: var(--space-48);
  box-sizing: border-box;
  vertical-align: middle;
  padding: 0 var(--space-20);
  border-bottom: 1px solid var(--border-subtle);
  font-feature-settings: "kern" 1;
}
/* Cell value — comfortable 16px body type (was 14px --text-sm), standard sans for
   ALL strings by default (no mono — see the .mono rule below). Ink defaults to
   primary; the 3-level emphasis ladder is muted / default / bold (see strong).
   Row height is PINNED on the 4-grid (default 56) via height + box-sizing +
   vertical-align:middle, with horizontal-only padding — so a row lands on an exact
   token height instead of drifting off-grid on the text's line-box (CLAUDE.md →
   Sizing Ladders: "Pin to a tier with height, constrain content to fit"). The
   height acts as a minimum, so multi-line cells still grow and stay centred. */
.data-table td {
  font-size: var(--text-body);
  color: var(--text-primary);
  height: var(--space-56);
  box-sizing: border-box;
  vertical-align: middle;
  padding: 0 var(--space-20);
  border-bottom: 1px solid var(--border-subtle);
}
.data-table tbody tr:last-child td { border-bottom: none; }
.data-table tbody tr:hover td { background: var(--data-table-row-hover-bg); }
.data-table tbody tr:active td { background: var(--data-table-row-active-bg); }
.data-table tfoot td {
  border-top: 1px solid var(--border-subtle);
  border-bottom: none;
  color: var(--text-secondary);
  font-weight: var(--fw-semibold);
}

/* .mono cell — inside a table it no longer means monospace. Tables are SANS by
   default: every string reads in our standard typography (June 2026 brand
   decision — no monotype in tables until a specific column is requested). The
   rule explicitly asserts var(--font-sans) so it overrides any ambient .mono
   utility (e.g. the catalog page's own .mono), keeps the cell on its row's type
   tier (the old 0.92em shrink is gone — it fought the tier; see CLAUDE.md → "the
   mono variant must be a --mono modifier"), and disables contextual alternates
   so any code-like run stays predictable. When a future request DOES want a mono
   column, the opt-in is the family-only .data-cell-text--mono modifier (rich
   tables) — not this class. */
.data-table td.mono,
.data-table .mono {
  font-family: var(--font-sans);
  font-feature-settings: "calt" 0;
}
/* Bold — the emphasis level above default in the muted / default / bold ladder.
   Semibold + primary ink (darker AND heavier), so a value reads as emphasised,
   not greyed. (Was --text-secondary, which made <strong> LIGHTER — backwards.) */
.data-table strong {
  font-weight: var(--fw-semibold);
  color: var(--text-primary);
}

/* Alignment helpers — override per-cell. */
.data-table td.ta-r,
.data-table th.ta-r { text-align: right; }
.data-table td.ta-c,
.data-table th.ta-c { text-align: center; }
.data-table td.ta-l,
.data-table th.ta-l { text-align: left; }

/* ── Size ladder ─────────────────────────────────────────────
   Four tiers. Row heights are PINNED on the 4-grid (height + box-sizing +
   vertical-align:middle, horizontal-only padding) so every tier lands on an
   exact spacing token — no off-grid drift on the text's line-box. The header
   font runs its own clean ladder (micro 10 / caption 12 / sm 14 / body 16),
   the value runs sm 14 / body 16 / body 16 / body-lg 18:

     tier      th    td    th-h   row-h   th-case
     --sm      10    14    32     32      UPPERCASE + tracking  (the exemption)
     --md      12    16    40     40      normal-case
     default   14    16    48     56      normal-case  (base — set above)
     --xl      16    18    56     64      normal-case

   The header VOICE (normal-case, regular, muted) is the base default; --sm is
   the ONE exemption — at 10px (--text-micro) normal-case reads too faint, so it
   forces UPPERCASE + --tracking-loose (the classic dense-table caption header),
   legible AND intentional. The 10/12/14/16 header ladder is what makes --md
   distinct from default (th 12 vs 14), not just a denser twin. Horizontal
   padding steps 12 / 16 / 20 / 24 across the tiers. */
.data-table--sm th {
  font-size: var(--text-micro);
  text-transform: uppercase;
  letter-spacing: var(--tracking-loose);
  height: var(--space-32);
  padding: 0 var(--space-12);
}
.data-table--sm td { font-size: var(--text-sm); height: var(--space-32); padding: 0 var(--space-12); }
.data-table--md th { font-size: var(--text-caption); height: var(--space-40); padding: 0 var(--space-16); }
.data-table--md td { height: var(--space-40); padding: 0 var(--space-16); }
.data-table--xl th { font-size: var(--text-body); height: var(--space-56); padding: 0 var(--space-24); }
.data-table--xl td { font-size: var(--text-body-lg); height: var(--space-64); padding: 0 var(--space-24); }

/* ── Style modifiers ─────────────────────────────────────────
   Striped: even-row tint. Bordered: vertical + horizontal grid.
   Borderless: drop the horizontal rule. --no-hover: static spec
   tables (Specs / Token reference) where hover is a distraction. */
.data-table--striped tbody tr:nth-child(even) td {
  background: var(--data-table-row-striped-bg);
}
.data-table--striped tbody tr:nth-child(even):hover td {
  background: var(--data-table-row-hover-bg);
}
.data-table--bordered th,
.data-table--bordered td {
  border-right: 1px solid var(--border-subtle);
}
.data-table--bordered th:last-child,
.data-table--bordered td:last-child { border-right: none; }
.data-table--borderless th,
.data-table--borderless td { border-bottom: none; }
.data-table--no-hover tbody tr:hover td,
.data-table--no-hover tbody tr:active td { background: transparent; }
.data-table--align-numeric td { text-align: right; }
.data-table--align-numeric th { text-align: right; }
.data-table--align-numeric td:first-child,
.data-table--align-numeric th:first-child { text-align: left; }

/* ── Sticky modifiers ────────────────────────────────────────
   --sticky-head pins thead to the top of the scrolling viewport;
   the parent must be the scroll container (a wrapper with
   max-height + overflow-y: auto). --sticky-col pins the first
   column on horizontal scroll; pairs with .ds-doc-table-wrap.

   Each needs an opaque backdrop because the rest of the table
   scrolls underneath — a transparent sticky cell would show the
   scrolled content through. We use --surface-card for the head
   and --surface-page for the column, matching the paper tier
   the cell typically sits on.

   Note: position:sticky inherits its containing block from the
   nearest scroll ancestor. The wrapper MUST have overflow set
   (auto / scroll / hidden) for sticky to engage. */
.data-table--sticky-head thead th {
  position: sticky;
  top: 0;
  background: var(--surface-card);
  z-index: 1;
}
.data-table--sticky-col tbody td:first-child,
.data-table--sticky-col tfoot td:first-child {
  position: sticky;
  left: 0;
  background: var(--surface-page);
  z-index: 1;
}
.data-table--sticky-col thead th:first-child {
  position: sticky;
  left: 0;
  background: var(--surface-card);
  z-index: 2;
}

/* ── .data-table--rich — geometric-alignment row chassis ────────────────────
   The OPT-IN rich-row layer: turns a .data-table into a surface that aligns
   avatars + text + chips + meters + buttons on ONE centre line per row. The
   base .data-table (above) centres cell content with vertical-align, which
   lands inline content on the font's fractional x-height — fine for plain
   text, off by ~0.5px the moment a row mixes an avatar with a chip. --rich
   replaces that with GEOMETRIC centring:
     • border-collapse: separate — a collapsed 1px header border attributes
       0.5px to the th, knocking the whole tbody onto half-pixels (a half-
       pixel row rasterises the avatar cell off the text cells). Separate
       borders keep every row on the 4-grid; dividers move to box-shadow
       (zero layout height) so the grid-lock holds.
     • explicit row HEIGHT on the 4-grid (--data-table-row-h slot) + box-sizing
       so the row is an exact token height, not padding-derived.
     • each cell's content sits in a flex .data-cell that the <td> centres by
       BOX geometry (integer math), and flex centres the content inside it —
       neither touches vertical-align's fractional x-height.
   Promoted June 2026 from the playground Orders table (.data-table--orders +
   .ui-order-cell / .ui-order-text), which becomes a thin scoped consumer.
   Full diagnostic: CLAUDE.md → "Data-table row alignment". Tier-2 universal —
   any rich data table (product, portal, dashboard, catalog). */
.data-table--rich {
  /* Row height — a 4-grid slot, default 56 (a 32px avatar in a compact row).
     PAIR --rich WITH A COMPACT PADDING TIER: --sm (8px vertical) + the 1px row
     divider + a 32px avatar = 49 ≤ 56, so the declared height holds. With the
     base 16/16 padding the content overflows to ~65, so reach for --sm --rich
     (both real consumers do — the Orders table and the catalog Routes panel).
     A 40px-avatar consumer steps the slot up (--space-72). The size tier owns
     padding; this slot owns the row height — pick them together so the rendered
     height equals the declared one (CLAUDE.md → Sizing Ladders). */
  --data-table-row-h: var(--space-56);
  /* Comfortable body type is the rich-table DEFAULT — 16px values read in the
     same voice as the cards beside them (the look the Orders table pioneered).
     Promoted June 2026 from .data-table--orders into the base: every real --rich
     consumer (Orders, Portal, future tables) wants the comfortable look; none
     wants the lean 14px dense base, so the comfortable size is the default, not
     an opt-in. A genuinely dense table can still step the slot back down. */
  --data-cell-text-size: var(--text-body);
  border-collapse: separate;
  border-spacing: 0;
}
/* Rows on the 4-grid — exact token height, integer line-height, box-sizing so
   padding can't push the row off-grid; vertical-align centres the cell block
   (the flex .data-cell centres content inside it). Row DIVIDERS stay with the
   base border model — base .data-table td border-bottom, or removed via
   --borderless — so a rich table opts in/out of row rules like any other. Under
   separate borders + box-sizing, that border sits inside the row height, so it
   stays on the 4-grid either way. (--borderless orders has no row dividers; a
   default rich table — e.g. a portal members table — keeps them.) */
.data-table--rich td {
  vertical-align: middle;
  height: var(--data-table-row-h);
  box-sizing: border-box;
  line-height: var(--lh-body);
}
/* Header row also grid-locked; divider via box-shadow so the head stays an
   exact token height under separate borders. The calm header VOICE (normal-case
   regular muted) lives on the BASE .data-table th, but the canonical rich pairing
   is --sm --rich (Orders, Portal, Routes) — and the --sm tier carries the 10px
   UPPERCASE micro-header exemption. So this rule RE-ASSERTS the rich header voice
   (14px --text-sm, normal-case, no tracking) to override --sm's exemption: a rich
   table keeps the calm 14px header / 16px value relationship regardless of --sm.
   It also adds the rich chassis's structural grid-lock (exact height, box-sizing,
   single line-box, box-shadow divider). */
.data-table--rich thead th {
  height: var(--space-40);
  box-sizing: border-box;
  line-height: 1;
  border-bottom: 0;
  box-shadow: inset 0 -1px 0 var(--border-subtle);
  font-size: var(--text-sm);
  text-transform: none;
  letter-spacing: var(--tracking-normal);
}

/* ── .data-cell — geometric cell-content wrapper ───────────────────────────
   The flex box every rich-table cell's content sits in. The <td> centres this
   block by geometry; flex centres the content inside it (CLAUDE.md → "Data-
   table row alignment"). Standalone primitive — usable in a list row too.
   --end right-justifies (numeric columns). Promoted from .ui-order-cell. */
.data-cell { display: flex; align-items: center; }
.data-cell--end { justify-content: flex-end; }

/* ── .data-cell-text — ONE text chassis for rich-row values ────────────────
   Every text value in a rich row reads at one chassis: size, line-box (1, so
   the flex wrapper centres the glyph box against an avatar in a sibling cell),
   family, tabular numerics, default primary ink + regular weight. Per-cell
   meaning is exactly two slot-driven axes — WEIGHT + INK — so cell-to-cell
   drift is structurally impossible and a per-instance inline override still
   works. Size is a slot (--data-cell-text-size); the --rich base sets it to
   --text-body (comfortable), so every cell in a rich table tracks 16px; the
   bare-chassis fallback is --text-sm for any standalone (non-rich) use.
   Promoted from .ui-order-text (CLAUDE.md → "Primitive retirement is atomic").
   Tier-2 universal. */
.data-cell-text {
  display: inline-flex;
  align-items: center;
  font-size: var(--data-cell-text-size, var(--text-sm));
  line-height: 1;
  font-weight: var(--data-cell-text-weight, var(--fw-regular));
  color: var(--data-cell-text-ink, var(--text-primary));
  letter-spacing: var(--tracking-normal);
  font-variant-numeric: tabular-nums;
}
.data-cell-text--bold      { --data-cell-text-weight: var(--fw-semibold); }
.data-cell-text--muted     { --data-cell-text-ink: var(--text-muted); }
.data-cell-text--secondary { --data-cell-text-ink: var(--text-secondary); }
.data-cell-text--danger    { --data-cell-text-ink: var(--danger); }
/* Mono font for code-like values (hash · key · address · slug). The family's
   OWN mono modifier — NOT the base `.data-table .mono` rule, which adds a
   0.92em shrink that fights the chassis size (composing `.data-cell-text.mono`
   rendered ~11px against an --sm td). This keeps the chassis font-size and only
   swaps the family. NEITHER the Orders table NOR Portal rows use it — both are
   no-mono (sans tabular-nums everywhere, even hashes/keys; a brand preference,
   June 2026). The variant survives for genuinely standalone / dev-facing tables
   that want monospace identifiers. */
.data-cell-text--mono      { font-family: var(--font-mono); }

/* ── .table-panel — a panel that frames a data table ───────────────────────
   A top-level frame (per CLAUDE.md → "Panel vs card": a -panel HOLDS content,
   a -card sits inside one) that stacks an optional .screen-header above a data
   table. Composes existing primitives, doesn't reinvent them:
     • surface / radius / shadow / padding  ← .panel.ui-card (add both classes)
     • header (title + trailing actions)     ← .screen-header (a child)
     • horizontal scroll for wide tables     ← .table-scroll (+ --pinned)
     • the rich row chassis                   ← .data-table--rich (on the table)
   .table-panel itself owns only the GLUE: the column stack + the scroll-region
   edge-bleed so the table reaches the panel edges (scroll affordance hugs the
   frame) while the first cell still aligns with the header title.

   This is the generalised version of the playground's Orders panel chrome —
   .ui-orders-panel now composes `panel ui-card table-panel ui-orders-panel` and
   layers only its product-specific behavior (Limit-mode visibility gate,
   body-state hide, receive-column flex sizing, the paired expand control) on
   top. Tier-2 universal — product, Portal, dashboards. Consumers: .ui-orders-panel,
   future Portal fee / members / transactions panels.

   Canonical markup:
     <article class="panel ui-card table-panel">
       <header class="screen-header screen-header--main">
         <h2 class="screen-header__title">…</h2>
         <div class="screen-header__trailing">…actions…</div>
       </header>
       <div class="table-scroll table-scroll--pinned">
         <div class="table-scroll__body">
           <table class="data-table data-table--rich …">…</table>
         </div>
       </div>
     </article> */
.table-panel {
  /* Gutter the table keeps from the panel edge so the first cell aligns with
     the .screen-header title — defaults to the --sm cell gutter; a consumer
     with different cell padding overrides this slot. */
  --table-panel-bleed-gutter: var(--space-12);
  display: flex;
  flex-direction: column;
}
/* The scroll region extends edge-to-edge inside the panel padding (minus the
   bleed gutter), and its gradients / pinned cell paint over the panel's raised
   surface (--surface-raised, the .panel tier — NOT --surface-card). */
.table-panel > .table-scroll {
  --table-scroll-surface: var(--surface-raised);
  margin-inline: calc(-1 * var(--panel-padding) + var(--table-panel-bleed-gutter));
}

/* ── .table-scroll — horizontal-scroll affordance for wide tables ──────────
   A non-scrolling viewport (.table-scroll) wrapping an overflow-x __body,
   adding three legibility cues when a table is wider than its container:
     1. edge GRADIENTS (::before / ::after) that fade content in/out at the
        scrollable edges — toggled by data-can-left / data-can-right;
     2. a < > NAV overlay (__nav > __btn ×2) pinned top-right that steps the
        scroller, disables at each end, and hides (with the whole affordance)
        when the table fits — gated on data-scrollable;
     3. an optional PINNED first column (.table-scroll--pinned) that stays put
        (sticky left:0) while the rest scrolls — for an identity column.
   Richer sibling of the CSS-only .data-table--sticky-col / --sticky-head:
   this one is JS-driven (shared.js → initTableScroll sets the data flags +
   the --table-scroll-pin-w measurement and steps on click; scroll / resize /
   mutation observers keep it fresh). Reach for --sticky-col when you only
   need a pinned column; reach for .table-scroll when you want the arrows +
   edge fades + auto-hide too.
   The surface the gradients / pinned cell paint over is --table-scroll-surface
   (default --surface-card); a consumer on a raised panel sets it to
   --surface-raised. The nav's vertical position is --table-scroll-nav-top
   (default --space-4, centring the 32px controls on a 40px header row).
   Tier-2 universal — any wide table (product, dashboard, catalog). */
.table-scroll {
  --table-scroll-surface: var(--surface-card);
  --table-scroll-nav-top: var(--space-4);
  position: relative;
}
.table-scroll__body { overflow-x: auto; }

/* 1 — edge gradients (hosted on the non-scrolling viewport) */
.table-scroll::before,
.table-scroll::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  width: var(--space-40);
  pointer-events: none;
  z-index: 4;
  opacity: 0;
  transition: opacity 0.2s ease;
}
/* Left gradient starts at the pinned column's right edge (--table-scroll-pin-w,
   measured in JS) so it fades the columns emerging from under the pinned one. */
.table-scroll::before {
  left: var(--table-scroll-pin-w, 0px);
  background: linear-gradient(to right, var(--table-scroll-surface), transparent);
}
.table-scroll::after {
  right: 0;
  background: linear-gradient(to left, var(--table-scroll-surface), transparent);
}
.table-scroll[data-can-left="true"]::before { opacity: 1; }
.table-scroll[data-can-right="true"]::after { opacity: 1; }

/* 2 — < > nav overlay; hides when the table fits (no overflow). Sits ON the
   header row (top-right), a left-fading surface backing masking the labels
   under it and blending into the right-edge gradient; z 5 > gradients' 4. */
.table-scroll:not([data-scrollable="true"]) .table-scroll__nav { display: none; }
.table-scroll__nav {
  position: absolute;
  top: var(--table-scroll-nav-top);
  right: 0;
  z-index: 5;
  height: var(--space-32);
  display: flex;
  align-items: center;
  gap: var(--cluster-xs);
  padding: 0 var(--space-8) 0 var(--space-32);
  background: linear-gradient(to left, var(--table-scroll-surface) 70%, transparent);
}

/* 3 — pinned first column (opt-in). Opaque fill so scrolled cells slide UNDER
   it; a subtle right-edge hairline marks the seam. Row hover stays opaque
   (mix over the surface, not transparent) so scrolled cells don't show through. */
.table-scroll--pinned .table-scroll__body .data-table th:first-child,
.table-scroll--pinned .table-scroll__body .data-table td:first-child {
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--table-scroll-surface);
  box-shadow: inset -1px 0 0 var(--border-subtle);
}
.table-scroll--pinned .table-scroll__body .data-table thead th:first-child { z-index: 3; }
.table-scroll--pinned .table-scroll__body .data-table tbody tr:hover td:first-child {
  background: color-mix(in oklch, var(--text-primary) 4%, var(--table-scroll-surface));
}

/* ── Sortable column header ──────────────────────────────────
   Authoring contract:
     <th class="is-sortable" aria-sort="none|ascending|descending">
       Column name
       <svg class="data-table__sort-icon" viewBox="0 0 16 16" aria-hidden="true">
         <path d="M4 6l4-4 4 4M4 10l4 4 4-4"/>
       </svg>
     </th>

   The icon shows two opposing chevrons by default. When aria-sort
   is "ascending", the bottom chevron dims. When "descending", the
   top chevron dims. When "none", both render at half opacity. The
   path is one shape that the CSS masks/highlights — keeps the SVG
   tiny and avoids a JS swap. */
.data-table th.is-sortable {
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
}
.data-table th.is-sortable:hover { color: var(--text-primary); }
.data-table th.is-sortable:focus-visible {
  outline: 2px solid var(--accent-primary);
  outline-offset: -2px;
}
.data-table__sort-icon {
  display: inline-block;
  width: 12px;
  height: 12px;
  margin-left: var(--space-4);
  vertical-align: -2px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0.4;
  transition: opacity 120ms ease, transform 120ms ease;
}
.data-table th[aria-sort="ascending"]   .data-table__sort-icon,
.data-table th[aria-sort="descending"]  .data-table__sort-icon { opacity: 1; color: var(--accent-primary); }
.data-table th[aria-sort="ascending"]   .data-table__sort-icon { transform: rotate(180deg); }

/* ── Row state — selected ────────────────────────────────────
   Routes through --data-table-row-selected-bg (declared on the
   base .data-table rule above) which is derived from --data-table-
   selected-tone. To re-route in a domain-specific consumer:
     `.lend-table { --data-table-selected-tone: var(--lend-accent); }`
   Wins over hover via specificity (selected is a state, hover is
   a transient interaction — selected shouldn't disappear when the
   pointer moves). */
.data-table tbody tr.is-selected td,
.data-table tbody tr.is-selected:hover td {
  background: var(--data-table-row-selected-bg);
}
/* Press feedback on selected rows — parallels .list-item--interactive's
   .is-selected:active rule. Without this, selected (6%) wins over
   :active (10%) on already-selected rows via source order at equal
   specificity. Higher specificity here ensures the press paints. */
.data-table tbody tr.is-selected:active td {
  background: var(--data-table-row-active-bg);
}

/* ── Row state — expandable ──────────────────────────────────
   .is-expanded is a hint for the trigger row (e.g., the chevron
   in the lead cell flips). The expanded content lives in a
   separate sibling row (.data-table__detail-row) so the colspan
   pattern is explicit in the markup. Visual separation: drop the
   trailing border of the trigger row so the detail row reads as
   a continuation. Detail-row bg routes through --data-table-row-
   expanded-bg (declared on the base rule). */
.data-table tbody tr.is-expanded > td { border-bottom-color: transparent; }
.data-table tbody tr.data-table__detail-row > td {
  padding: var(--space-16) var(--space-20);
  background: var(--data-table-row-expanded-bg);
  font-size: var(--text-sm);
}
.data-table tbody tr.data-table__detail-row:hover td { background: var(--data-table-row-expanded-bg); }

/* ── Caption + empty-state cell ──────────────────────────────
   <caption> reads as a leading h-level title above the table;
   we style it to match the section-eyebrow rhythm. The empty
   state lives as a single full-row td with colspan; the modifier
   .data-table__empty styles it as a centered placeholder. */
.data-table caption {
  caption-side: top;
  text-align: left;
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-faint);
  padding: 0 0 var(--space-12);
}
.data-table__empty {
  text-align: center;
  color: var(--text-muted);
  padding: var(--space-40) var(--space-20);
}
.data-table__empty:hover { background: transparent !important; }

/* ════════════════════════════════════════════════════════════════
   Pagination — page controls for tables, lists, and result grids
   ════════════════════════════════════════════════════════════════
   Sibling primitive to the Table family — extracted so future product
   surfaces (search results, transaction history, leaderboards) get
   the same control vocabulary without copy-pasting per consumer.

   Anatomy:
     <nav class="pagination" aria-label="Pagination">
       <button class="pagination__btn" aria-label="Previous" disabled>‹</button>
       <button class="pagination__page is-active" aria-current="page">1</button>
       <button class="pagination__page">2</button>
       <button class="pagination__page">3</button>
       <span class="pagination__ellipsis" aria-hidden="true">…</span>
       <button class="pagination__page">9</button>
       <button class="pagination__btn" aria-label="Next">›</button>
     </nav>

   Two sizes on the canonical ladder:
     .pagination--sm   → 32px controls (pairs with .data-table--sm)
     .pagination       → 40px controls (default; pairs with .data-table)

   Variants:
     .pagination--compact  → drop page-number buttons; show "‹ 1 / 9 ›"
                             only. Right for narrow widget surfaces.
     .pagination--centered → justify-content: center for marketing
                             placements. Default is flex-start.

   Optional sibling caption — "Showing 1-10 of 287" — is just text;
   author it as a <p class="pagination__info"> next to the nav. No
   modifier needed.
   ──────────────────────────────────────────────────────────── */
.pagination {
  display: inline-flex;
  align-items: center;
  gap: var(--space-4);
  font-variant-numeric: tabular-nums;
}
.pagination__btn,
.pagination__page {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 40px;
  height: 40px;
  padding: 0 var(--space-12);
  background: transparent;
  border: 1px solid transparent;
  border-radius: 10px;
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: var(--fw-medium);
  color: var(--text-secondary);
  cursor: pointer;
  transition: background-color 120ms ease, color 120ms ease, border-color 120ms ease;
}
.pagination__btn { padding: 0 var(--space-10); }
.pagination__btn svg {
  width: 16px;
  height: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.pagination__page:hover,
.pagination__btn:not(:disabled):hover {
  background: var(--surface-sunk);
  color: var(--text-primary);
}
.pagination__page:focus-visible,
.pagination__btn:focus-visible {
  outline: 2px solid var(--accent-primary);
  outline-offset: 2px;
}
.pagination__page.is-active,
.pagination__page[aria-current="page"] {
  background: color-mix(in oklch, var(--accent-primary) 12%, transparent);
  color: var(--accent-primary);
  font-weight: var(--fw-semibold);
  border-color: color-mix(in oklch, var(--accent-primary) 24%, transparent);
}
[data-theme="light"] .pagination__page.is-active,
[data-theme="light"] .pagination__page[aria-current="page"] {
  background: color-mix(in oklch, var(--accent-primary) 8%, transparent);
}
.pagination__btn:disabled {
  color: var(--text-faint);
  cursor: not-allowed;
}
.pagination__ellipsis {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 24px;
  color: var(--text-faint);
  font-size: var(--text-sm);
  user-select: none;
}

/* ── Size ladder ───────────────────────────────────────────── */
.pagination--sm .pagination__btn,
.pagination--sm .pagination__page {
  min-width: 32px;
  height: 32px;
  padding: 0 var(--space-8);
  font-size: var(--text-caption);
  border-radius: 8px;
}
.pagination--sm .pagination__btn { padding: 0 var(--space-6); }
.pagination--sm .pagination__btn svg { width: 14px; height: 14px; }

/* ── Compact variant — prev / counter / next ──────────────── */
.pagination--compact .pagination__page { display: none; }
.pagination--compact .pagination__ellipsis { display: none; }
.pagination--compact .pagination__counter {
  display: inline-flex;
  align-items: center;
  padding: 0 var(--space-8);
  color: var(--text-secondary);
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
}

/* ── Centered placement ───────────────────────────────────── */
.pagination--centered {
  display: flex;
  justify-content: center;
  width: 100%;
}

/* ── Info caption sibling ─────────────────────────────────── */
.pagination__info {
  font-size: var(--text-sm);
  color: var(--text-muted);
  margin: 0;
  font-variant-numeric: tabular-nums;
}

/* ── DEPRECATED — .ds-doc-sub aliased to .ds-doc-eyebrow ─────
   May 2026: the catalog had two near-identical uppercase-kicker
   primitives with overlapping roles — `.ds-doc-sub` (sans, 12px,
   "section eyebrow inside a pane") and `.ds-doc-eyebrow` (mono,
   11px, "kicker above one demo"). The visual difference was a 1px
   font-size and a sans-vs-mono flip; the role distinction wasn't
   carried by the names, so authors picked whichever they remembered
   and the catalog drifted.

   Unified on `.ds-doc-eyebrow` (the mono primitive) because mono
   uppercase kickers carry the brand's "code-comment / architecture
   marker" voice — same recipe used by `.eyebrow-code` on marketing
   surfaces. Every consumer in design-system/index.html migrated;
   this alias stays one cycle as a backstop in case any out-of-tree
   consumer still references `.ds-doc-sub`. Remove in next sweep. */
.ds-doc-sub {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-faint);
  margin: 0 0 var(--space-6);
}

/* ── Caption (small annotation under demo elements) ────────── */
/*  Default sans, left-aligned, full container width. Reads as a quieter
    sibling of .ds-doc-text — same default sans, slightly smaller size,
    muted color. Default rhythm is post-demo (8 px top gap).

    Three modifiers cover the three rhythms used across the catalog so
    consumers stop reaching for inline `style="margin-..."` overrides:

      .ds-doc-caption--lead     →  bigger top gap (12 px). Use when a
                                   caption follows its demo and the
                                   default 8 px feels cramped.
      .ds-doc-caption--label    →  caption acts as a LABEL ABOVE the
                                   demo that follows it. Top margin off,
                                   small bottom margin (8 px) instead.
      .ds-doc-caption--centered →  figcaption under a centered demo IN A
                                   GAPPED figure / grid cell — the cell's
                                   `gap` provides the top spacing, so this
                                   modifier zeroes margin. When a centered
                                   caption sits directly under a demo with
                                   NO gap provider (e.g. a direct child of
                                   `.ds-doc-eyebrow-row`), the row re-adds an
                                   8px top gap so it never sits tight. Never
                                   leave a caption flush against its preview.

    May 2026 v16 promotion: collapsed ~85 inline-style overrides
    (`margin-top:var(--space-12)`, `margin-bottom:6-8px;text-align:left`,
    `margin:0;text-align:center`) into these three modifiers. Tail cases
    that don't fit one of the three (margin: 20+, mixed shorthand) stay
    inline by intent — the modifier ladder is the 80% pattern, not 100%. */
.ds-doc-caption {
  color: var(--text-muted);
  font-size: var(--text-caption);
  line-height: 1.5;
  text-align: left;
  margin-top: var(--space-8);
  max-width: none;
}
.ds-doc-caption--lead     { margin-top: var(--space-12); }
.ds-doc-caption--label    { margin-top: 0; margin-bottom: var(--space-8); }
.ds-doc-caption--centered { margin: 0; text-align: center; }

/* ── Swatch label (mono text on visual demos) ──────────────── */
.ds-doc-swatch-label {
  font-size: var(--text-caption);
  font-family: var(--font-mono);
  color: var(--text-muted);
}
.ds-doc-swatch-label--on-alpha { color: var(--on-alpha-muted); }

/* ════════════════════════════════════════════════════════════════
   Inside-pane vocabulary — additional primitives
   ════════════════════════════════════════════════════════════════
   Together with the eight primitives above (.ds-doc-text /
   .ds-doc-list / .ds-doc-codeblock / .ds-doc-table-wrap /
   .code-chip / .ds-doc-sub / .ds-doc-caption /
   .ds-doc-swatch-label — note .code-chip was promoted out of the
   .ds-doc-* namespace in May 2026 v10), these four make up the full set of
   approved building blocks for documentation panes. Authors should
   never inline-style their own labels, grids, dividers, or asides.

   New additions land here so the cascade order matches the section
   above and theme overrides (light/dark) inherit without surprise.
   Pane-aware (in .ds-doc-viewport-pane) where it matters.
*/

/* ── Mono kicker — paired ABOVE a heading, definition row, or demo
   ─────────────────────────────────────────────────────────────────
   One member of the unified Eyebrow family. The family shares ONE
   typography recipe — mono · --text-caption (12px) · 700 · uppercase
   · --tracking-wider — and differs only in color recipe and prefix /
   separator chrome. Family members:

     · .eyebrow-code      // <prefix> · accent-painted · page sections
     · .eyebrow-word      no prefix · accent-painted · page sections
     · .header-eyebrow    accent + optional accent-secondary span · heroes
     · .header-eyebrow--breadcrumb  muted parent + accent leaf · DS heroes
     · .ds-doc-eyebrow    text-faint · quiet kicker inside catalog Panes

   See #eyebrow in design-system/index.html for the family doc. The
   May 2026 typography unification dropped the 11/12 stairstep that
   had grown between the four primitives so the family reads as one
   recipe under different color treatments. */
.ds-doc-eyebrow {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-faint);
  /* Tier 1 of the pane-breathing cascade: a small but real gap between
     the eyebrow kicker and the demo immediately below. Was previously
     var(--space-6), an undefined token that resolved to 0 — eyebrow
     touched the demo. See CLAUDE.md → "Pane breathing room — 4-tier
     cascade" for the full system. */
  margin: 0 0 var(--space-12);
}
.ds-doc-eyebrow--accent { color: var(--accent-primary); }
.ds-doc-eyebrow--ghost  { color: var(--text-muted); opacity: 0.7; }

/* ── Eyebrow-labelled row — pair an eyebrow caption with a demo
   ─────────────────────────────────────────────────────────────────
   The "label + demo" pair pattern that recurs all over the catalog
   inside a single Preview pane. Stacking 3-7 of these is how a card
   shows variants without fragmenting into nested sub-viewports.

   Recipe (drop-in, no wrapper grid required):
     <div class="ds-doc-eyebrow-row">
       <p class="ds-doc-eyebrow">Variant label · purpose</p>
       <demo>...</demo>
     </div>
     <div class="ds-doc-eyebrow-row">…</div>

   Adjacent rows auto-space (32px). Eyebrow → demo gap is fixed (12px).
   Don't author per-instance margins on the eyebrow inside this row —
   the row owns the rhythm so a card's variant grid is uniform. */
.ds-doc-eyebrow-row + .ds-doc-eyebrow-row {
  margin-top: var(--space-32);
}
.ds-doc-eyebrow-row > .ds-doc-eyebrow:first-child {
  margin: 0 0 var(--space-12);
}
/* A trailing centered caption is a DIRECT child here — there's no figure /
   grid gap to space it from the demo above, so `--centered`'s `margin:0`
   would leave it tight against the preview. The row owns this gap too (same
   principle as the eyebrow→demo gap above). Scoped to --centered so the
   gapped figure/grid figcaptions that legitimately use margin:0 are
   untouched. See CLAUDE.md → "Captions under a preview need top whitespace". */
.ds-doc-eyebrow-row > .ds-doc-caption--centered {
  margin-top: var(--space-8);
}

/* ── Flat zone-label default — Tier-3 gap for un-wrapped eyebrows ──────────
   The canonical "labeled cell" pattern is `.ds-doc-eyebrow-row` (handles its
   own 32px between adjacent rows). But many Preview panes stack bare
   `.ds-doc-eyebrow` + demo pairs as flat siblings without the wrapper. There,
   the demo carries no bottom margin and the eyebrow no top margin, so a new
   zone-label butts directly against the previous demo (0 gap). This makes any
   non-first eyebrow that's a DIRECT child of a pane fall back to the Tier-3
   32px gap — the same value `.ds-doc-eyebrow-row` would give. Eyebrows inside
   an `.ds-doc-eyebrow-row` (not direct children) and first-child eyebrows are
   unaffected. The avatar/icon-library rule below overrides to Tier-4 (64px)
   for true category zones. See CLAUDE.md → "Pane breathing room — 4-tier cascade". */
.ds-doc-viewport-pane > * + .ds-doc-eyebrow {
  margin-top: var(--space-32);
}

/* ── .ds-doc-viewport-pane--ruled — opt-in hairline between cells ─────────
   Mirrors the system-level `.ds-doc-card + .ds-doc-card` pattern one tier
   deeper: inserts a 1px hairline divider between any two adjacent direct
   children of a Preview pane, with breathing-room padding above and
   below the line so the divider reads as deliberate, not crowded.

   Apply when variant demos share a visual signature so subtle that the
   default 32px cell-gap can't carry the boundary — eyebrow family, kbd,
   status-dot, color swatches, anything where the demo is itself a small
   quiet token. Authoring is one class on the Preview pane; every cell
   gets the rhythm automatically.

   Same divider color recipe as `.divider` and the inter-card hairline
   (7%/8% dark/light) so the visual language stays unified across tiers.

   Section → card hairline (80px) → ruled cell hairline (32+32px) →
   variant-internal spacing. Same recipe, one tier each. */
.ds-doc-viewport-pane--ruled > * + * {
  margin-top: var(--space-32);
  padding-top: var(--space-32);
  border-top: 1px solid color-mix(in oklch, var(--text-primary) 7%, transparent);
}
[data-theme="light"] .ds-doc-viewport-pane--ruled > * + * {
  border-top-color: color-mix(in oklch, var(--text-primary) 8%, transparent);
}

/* ── .ds-doc-stack — Tier 2 of the pane-breathing cascade ─────────────────
   Vertical stack utility for Preview-pane content where N example rows
   sit under one eyebrow (or no eyebrow at all). Wraps siblings in a flex
   column with a 20px gap so they don't touch.

   Apply directly to a `.ds-doc-viewport-pane[data-pane="preview"]` whose
   direct children are stacked example rows (4 .field-inline siblings,
   3 .checkbox siblings, etc.) OR to a wrapping <div> inside the pane.

   For LABELED cells (eyebrow + demo each), use `.ds-doc-eyebrow-row`
   above instead — that primitive owns the eyebrow → demo gap and the
   inter-row gap as a single rhythm.

     .ds-doc-stack         flex column, gap 20  (default — sibling examples)
     .ds-doc-stack--lg     flex column, gap 32  (substantial examples,
                                                 e.g., cards with desc)
     .ds-doc-stack--xl     flex column, gap 40  (cell groups inside one
                                                 zone — pairs with Tier 3)

   Cascade reference (full table in CLAUDE.md → "Pane breathing room"):
     Tier 1  eyebrow → demo                12px  (.ds-doc-eyebrow margin)
     Tier 2  sibling examples in one demo  20px  (.ds-doc-stack)
     Tier 3  eyebrow-labeled cells         32px  (.ds-doc-eyebrow-row)
     Tier 4  zones within one pane         64px  (margin-top, ad-hoc) */
.ds-doc-stack {
  display: flex;
  flex-direction: column;
  gap: var(--space-20);
}
.ds-doc-stack--lg { gap: var(--space-32); }
.ds-doc-stack--xl { gap: var(--space-40); }

/* ── Side-by-side grid — for compare / pair / trio compositions */
.ds-doc-grid {
  display: grid;
  gap: var(--space-16);
  align-items: start;
  margin-top: var(--space-12);
}
.ds-doc-grid--cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.ds-doc-grid--cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.ds-doc-grid--cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.ds-doc-grid--tight  { gap: var(--space-8); }
.ds-doc-grid--loose  { gap: var(--space-24); }
@media (max-width: 720px) {
  .ds-doc-grid--cols-2,
  .ds-doc-grid--cols-3,
  .ds-doc-grid--cols-4 { grid-template-columns: 1fr; }
}

/* ── Aside / pull-note — line-bordered semantic note ──────────
   Cleaner ground-up redesign (May 2026):
     · No italics — emphasis comes from the colored line + subtle bg,
       not from a font-style. Italic asides read as "tone change"
       rather than "semantic note" — too soft for documentation.
     · Sized to MATCH surrounding prose (.ds-doc-text is 14 px /
       1.6) so an aside reads as a peer paragraph with a marker,
       not a bigger pulled-out callout.
     · Color-coded by semantic intent. Six variants pull from the
       system's approved state palette (--info / --success / --warn
       / --danger) plus a brand-emphasis default and a quiet muted
       fallback. The variant communicates the meaning; readers
       shouldn't need a "Note." / "Don't." prefix to decode tone.
   The accent is exposed as --ds-aside-accent so the variant is one
   line of CSS to extend. */
.ds-doc-aside {
  --ds-aside-accent: var(--accent-primary);
  margin: var(--space-16) 0;
  padding: var(--space-8) var(--space-12) var(--space-8) var(--space-16);
  border-left: 3px solid var(--ds-aside-accent);
  border-radius: 0 var(--space-4) var(--space-4) 0;
  background: color-mix(in oklch, var(--ds-aside-accent) 7%, transparent);
  color: var(--text-secondary);
  font-size: var(--text-sm);
  line-height: 1.6;
  max-width: 640px;
}
.ds-doc-aside strong { color: var(--text-primary); font-weight: var(--fw-semibold); }
.ds-doc-aside code { color: var(--text-primary); }

/* Semantic variants — pulled from the system's state palette so a
   palette switch propagates without per-variant overrides. */
.ds-doc-aside--note    { --ds-aside-accent: var(--info); }
.ds-doc-aside--success { --ds-aside-accent: var(--success); }
.ds-doc-aside--warn    { --ds-aside-accent: var(--warn); }
.ds-doc-aside--danger  { --ds-aside-accent: var(--danger); }
.ds-doc-aside--muted   {
  --ds-aside-accent: color-mix(in oklch, var(--text-primary) 16%, transparent);
  background: transparent;
}

/* ── Hairline separator — quiet visual break inside a pane */
.ds-doc-divider {
  height: 1px;
  border: 0;
  background: color-mix(in oklch, var(--text-primary) 6%, transparent);
  margin: var(--space-20) 0;
}
.ds-doc-divider--tight { margin: var(--space-12) 0; }
.ds-doc-divider--loose { margin: var(--space-32) 0; }

/* ── Demo headline — bigger label below an eyebrow ──────────── */
/*  Pairs with .ds-doc-eyebrow above it to form the canonical
    eyebrow→headline→prose stack inside a pane. body-lg/600 is
    deliberately quieter than .ds-doc-card__label (h3-scale, 26px)
    so demo headlines don't compete with the card's own heading.
    The --sm modifier drops to body for tighter contexts (e.g.
    label-shape demos at #ds-doc-label-shape where Brand button +
    .btn-primary needs to fit in a single line). */
.ds-doc-headline {
  margin: 0;
  font-size: var(--text-body-lg);
  font-weight: var(--fw-semibold);
  color: var(--text-primary);
  line-height: 1.3;
}
.ds-doc-headline--sm { font-size: var(--text-body); }
.ds-doc-headline + .ds-doc-text { margin-top: var(--space-8); }

/* ── Demo well — THE canonical framed surface for component previews */
/*  This is the ONE primitive for framing a demo in a recessed surface.
    Reach for it instead of inline `background:var(--surface-sunk)` wells
    — one radius, one border, one padding scale, no per-card drift. The
    frame-vs-inline DECISION (which previews earn a well vs. sit bare on
    the pane) lives in the lifi-ds-docs skill → "Preview pane layout →
    Frame vs. inline". Short form: frame component / product-surface /
    material demos; leave atoms (buttons, chips, avatars) + text / token
    specimens (eyebrows, captions, swatches, code) bare.

    Pane-aware: standalone reads as a quiet boxed sample; inside a
    .ds-doc-viewport-pane the same recipe carries (the parent pane
    already provides the outer chrome).

    Padding scale (May 2026 — bumped base 16→24 to the component-preview-
    canvas tier, since most wells center a component): base 24px, --tight
    12px for small single specimens, --loose 32px for roomy compositions
    (grids, wide demos). Pick the surface by what the demo illustrates:
    --sunk "below context" (the default), --raised "above context",
    --elevated "floats above" (cards, modals), --alpha state-layer demos.
    The 1px hairline + --r-lg (12px) radius is the recurring sample-frame
    chrome — we'd otherwise re-author it inline 60+ times. */
.ds-doc-well {
  padding: var(--space-24);
  border-radius: var(--r-lg);
  background: var(--surface-sunk);
  border: 1px solid color-mix(in oklch, var(--text-primary) 6%, transparent);
}
.ds-doc-well--sunk     { background: var(--surface-sunk); }
.ds-doc-well--raised   { background: var(--surface-raised); }
.ds-doc-well--elevated { background: var(--surface-card); }
.ds-doc-well--alpha    { background: var(--alpha-subtle); }
.ds-doc-well--tight    { padding: var(--space-12); }
.ds-doc-well--loose    { padding: var(--space-32); }

/* ── Type specimen — typography weight / style sample row ───── */
/*  Used in #typography to show every loaded weight of a family
    as a scannable vertical list: a "Ag" sample at the actual
    weight on the left, a mono caption with the weight name +
    number, and a one-line use-case in muted prose. The list
    spans the full pane width — each row gets all the horizontal
    real estate it needs to read sample / name / use-case in a
    single line.

    Material — Paper. Wired via :where(.ds-doc-type-specimen)
    below so each row reads as a slightly lifted content card
    against the pane (rather than a recessed well). The --elev-1
    drop is paper's default; that's the "slight elevation."

    The --default modifier flips the border to accent-primary on
    the family's "default weight" row (Figtree 700, JetBrains
    Mono 500 in this catalog) so designers can tell at a glance
    which weight the system reaches for first.

    The sample's font-family + font-weight stay inline on
    .ds-doc-type-specimen__sample because they ARE the content
    of the specimen — the row demonstrates a (family, weight)
    pair, and that pair is the data, not the styling. Everything
    else (size, line-height, tracking, color, layout, material)
    is owned by the primitive. */
:where(.ds-doc-type-specimen) {
  --mat-bg:           var(--material-paper-bg);
  --mat-border:       var(--material-paper-border);
  --mat-shadow:       var(--material-paper-shadow);
  --mat-shadow-hover: var(--material-paper-shadow-hover);
  --mat-backdrop:     var(--material-paper-backdrop);
  --mat-highlight:    var(--material-paper-highlight);
}

.ds-doc-type-specimen-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-12);
}

.ds-doc-type-specimen {
  display: flex;
  align-items: center;
  gap: var(--space-24);
  padding: var(--space-16) var(--space-24);
  background: var(--mat-bg);
  border: var(--mat-border);
  border-radius: var(--r-lg);
  box-shadow: var(--mat-shadow), var(--mat-highlight);
}

.ds-doc-type-specimen--default {
  border-color: var(--accent-primary);
}

.ds-doc-type-specimen__sample {
  flex-shrink: 0;
  min-width: 88px;
  font-size: var(--text-h1);
  line-height: 1;
  letter-spacing: var(--tracking-firm);
  color: var(--text-primary);
  /* font-family + font-weight set inline per specimen. */
}

.ds-doc-type-specimen__caption {
  display: flex;
  align-items: baseline;
  gap: var(--space-20);
  flex: 1;
  min-width: 0;
}

.ds-doc-type-specimen__name {
  flex-shrink: 0;
  min-width: 140px;
  font-family: var(--font-mono);
  font-weight: var(--fw-semibold);
  font-size: var(--text-caption);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wider);
  color: var(--accent-primary);
}

.ds-doc-type-specimen__desc {
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  line-height: 1.4;
  color: var(--text-muted);
  flex: 1;
  min-width: 0;
}

/* Mobile fallback — at narrow widths the row collapses so the
   sample stays large and the caption wraps below it instead of
   forcing both into a cramped horizontal line. */
@media (max-width: 720px) {
  .ds-doc-type-specimen {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-12);
    padding: var(--space-16);
  }
  .ds-doc-type-specimen__caption {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-4);
  }
  .ds-doc-type-specimen__name { min-width: 0; }
}

/* ── Pinned mono detail — right-aligned annotation in a row ──── */
/*  Used inside flex rows that pair a label on the left with a small
    mono detail on the right (file paths, dimensions, version
    numbers, token names). margin-left:auto pushes it to the row's
    right edge without needing flex utilities or grid columns.
    Caption-size mono in muted colour reads as "supporting detail"
    without competing with the row's primary text. */
.ds-doc-detail {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  color: var(--text-muted);
  white-space: nowrap;
}
.ds-doc-detail--accent { color: var(--accent-primary); }

/* ── Smaller-text body modifier ─────────────────────────────── */
/*  Modifier on .ds-doc-text. Use for supporting prose under a
    headline or caption that should sit visually quieter than body
    paragraphs — typical at "Why" / "When to use" notes inside
    panes. The base .ds-doc-text already uses --text-secondary;
    this modifier just steps the size from body (16px) to sm (14px). */
.ds-doc-text--sm { font-size: var(--text-sm); }

/* ════════════════════════════════════════════════════════════════
   .code-chip — universal inline-code chip
   ════════════════════════════════════════════════════════════════
   Distinctive visual treatment for code identifiers (class names,
   tokens, modifiers, file paths, etc.) inline in prose. A code
   identifier carries engineering signal — "this is a paste-ready
   identifier" — and it's neither generic code nor running prose.
   The chip gives it a filled mono pill so readers can spot code
   references while scanning a wall of text.

   APPLIED AUTOMATICALLY to EVERY <code> element on the site. The
   bare <code> element selector is the universal trigger — no
   container class needed, no opt-in. Use <code>.foo</code>
   anywhere — marketing prose, dashboard captions, catalog docs,
   product surfaces — and the chip renders.

   The previous 10-selector container list (.ds-doc-text code,
   .ds-doc-list code, etc.) was retired May 2026 in favour of the
   bare element selector. Promoted from tier-1 .ds-doc-class to
   tier-2 .code-chip in the same pass — universal namespace, used
   everywhere. See CLAUDE.md → "Class-naming convention" v10
   migration history and "Inline <code> renders as a chip
   site-wide" hard rule for the full rationale.

   USE THE EXPLICIT CLASS for non-<code> hosts:
     <span class="code-chip">.foo</span>
   The chip works on any inline element; <code> is just the
   recommended semantic host since code identifiers ARE code.

   SUPPRESSED inside .ds-doc-codeblock — code blocks have their own
   syntax highlighting (.comment / .string / .tag / .attr / .keyword
   / .path / .punct token spans). A chip would compete visually.
   Suppression rule lives below the shared rule.

   Visual recipe (Apr 2026 v2 — Filled, no border):
     mono · 0.85em · weight 600 · primary text colour
     bg tint (currentColor at 7% alpha) — solid enough to read as a
       chip without an outline
     no border
     5px radius · tight padding · vertical-align baseline
     hover: accent-tinted bg (12% mix) + accent-mid colour

   Why currentColor for the tint: in dark theme, currentColor is
   light → tint becomes faint white. In light theme, it's dark →
   tint becomes faint dark. The chip stays subtle in both, no theme
   override needed.

   Why no border: at the previous 1 px outline + 4% bg, the outline
   was carrying most of the visual weight; designers reading "Brand
   button .btn-primary" first noticed the boxed `.btn-primary` and
   then the noun phrase. Dropping the outline + bumping the bg to 7%
   inverts that — the chip still reads as a chip (filled mono pill),
   but it's *paired* with the heading, not competing with it.
*/
code,
.code-chip {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 0.85em;
  font-weight: var(--fw-semibold);
  line-height: 1.4;
  color: var(--text-primary);
  background: color-mix(in oklch, currentColor 7%, transparent);
  border: 0;
  padding: 0.05em 0.45em;
  border-radius: var(--space-4);
  letter-spacing: 0;
  /* Cancel any inherited text-transform so class names always read
     as actual code (lowercase), even when the chip lives inside an
     uppercase parent like .ds-doc-eyebrow or a button label. The
     paired letter-spacing: 0 above does the same for tracking — both
     are typography-cancel defaults so the chip is self-contained. */
  text-transform: none;
  white-space: nowrap;
  vertical-align: baseline;
  transition: background-color 0.15s ease, color 0.15s ease;
}

/* Hover — accent-tinted bg + accent-mid colour, hints at future
   click-to-navigate. Doesn't change the cursor (would imply
   linkability we haven't built yet); the colour + bg shift is the
   affordance. Bumped the hover bg from 8 → 12% to compensate for
   the missing border (the chip needed a slightly stronger fill to
   keep the hover feedback visible). */
code:hover,
.code-chip:hover {
  background: color-mix(in oklch, var(--accent-primary) 12%, transparent);
  color: var(--accent-primary-mid);
}

/* ── Inline <code> in the LABEL — quieter override (Apr 2026) ────────
   Inside the variant heading the default chip treatment (0.85em +
   weight 600) is too prominent at the 26 px label scale — the chip
   competes with the noun phrase. The override below shrinks the
   font (0.85em → 0.72em) and drops the weight (600 → 500), keeps the
   inherited filled-bg-no-border chrome from the shared rule, and
   uses a 70%-mix colour so the chip reads as a quiet mono pill that
   the heading carries.

   ⚠️  This block must remain AFTER the shared .code-chip rule
   above — same selector specificity (.ds-doc-card__label code), so
   source order decides. Don't move it earlier or the chip override
   will silently lose the cascade.

   The vertical-align nudge keeps the smaller chip optically centred
   against the larger sans heading on the cap-height baseline. */
.ds-doc-card__label code {
  font-size: 0.72em;
  font-weight: var(--fw-medium);
  color: color-mix(in oklch, var(--text-primary) 70%, transparent);
  vertical-align: 0.08em;
  /* bg / border / padding / radius all inherited from the shared rule */
}
.ds-doc-card__label code:hover {
  /* bg shift inherited from the shared :hover; only the colour
     differs since the label-context chip uses a muted base colour */
  color: var(--accent-primary-mid);
}

/* ── Inline <code> in the EYEBROW row label — size bump ────────────
   Inverse problem to the card-label override above. Inside a
   .ds-doc-eyebrow (small 12px uppercase mono kicker) the shared
   chip's default 0.85em shrinks the class name to ~10px — too
   small to read as a chip. Bump to 1em so it matches the
   surrounding eyebrow size; the filled mono pill + full-strength
   --text-primary colour (from the shared rule) then carries the
   class identity inside the muted label row.

   Why this matters specifically for the Eyebrow doc card: every
   variant row's demo IS an eyebrow, so the row label can't rely
   on typography alone to distinguish itself from the demo. The
   <code> chip is the visual signature that says "I'm metadata,
   not a demo" — the label has a chip, the demos don't.

   Note: text-transform: none + letter-spacing: 0 used to live in
   this block. Both were hoisted to the shared chip rule above so
   every chip cancels parent typography by default, not just the
   eyebrow context. */
.ds-doc-eyebrow code {
  font-size: 1em;
}

/* Suppress the chip treatment inside code blocks — the codeblock has
   its own syntax-highlight token spans (.comment / .string / etc.)
   and a chip would compete visually with them. The universal `code`
   element selector above makes this suppression critical: without it,
   every <code> inside every <pre class="ds-doc-codeblock"> would be
   styled as a chip and clash with the syntax-highlight spans. */
.ds-doc-codeblock code,
.ds-doc-codeblock .code-chip {
  display: inline;
  background: transparent;
  border: 0;
  padding: 0;
  border-radius: 0;
  white-space: inherit;
  font-size: inherit;
  font-weight: inherit;
  vertical-align: baseline;
  transition: none;
}

/* The legacy `.ds-doc-mode-toggle` widget (Concise/Detailed segmented
   pill in the catalog hero) was removed alongside the depth switch in
   the v3 controlbar redesign. Both classes still resolve to nothing —
   any consumer rendering them manually just gets unstyled inline
   content. New code should not author either. */
.ds-doc-mode-toggle,
.ds-doc-mode-toggle-label { display: none; }

/* ════════════════════════════════════════════════════════════════
   .switch — binary on/off toggle (universal form control)
   ════════════════════════════════════════════════════════════════
   Native <input type="checkbox"> wrapped in a label, with a visual
   pill track + thumb. The checkbox is the source of truth — keyboard,
   screen-reader, form submission all work without JS. JS is only
   needed when a change should drive page state (e.g. theme pickers,
   feature flags, settings rows).

   USE ANYWHERE — settings rows, theme pickers, feature flags, opt-in
   prompts, dense control surfaces. Universal sibling to .checkbox
   and .input — fully themeable, palette-aware, no special context
   required.

   Why a switch, not a segmented control:
     A switch reads as "more / less" or "on / off" — one knob with
     two positions. A segmented control reads as "pick from N equal
     options." Use a switch for genuinely binary state; reach for
     .ds-doc-seg or radios when the choice is N-from-set.

   Why labels on the OUTSIDE (Off [switch] On) and not inside the
   track:
     - The on/off semantics are clearer when both states are named.
     - Clicking either label flips the switch (the label wraps the
       input via the surrounding <label> element).
     - The active label gets weight + colour, so state is legible
       without inspecting the thumb position.

   ANATOMY
     .switch                wrapper — <label> wrapping a real <input>
       .switch-label        text label (one per side; `is-active` when matches state)
       .switch-input        the real <input type="checkbox">; visually hidden
       .switch-track        the pill background
         .switch-thumb      the moving knob — positioned absolutely

   SIZES — three variants matching the system convention (sm / default / lg)
     .switch-sm   28×16  thumb 12px   — control bars, dense rows, table cells
     (default)       36×20  thumb 16px   — page-level toggles, settings rows
     .switch-lg   44×24  thumb 20px   — prominent toggles, hero settings

   PALETTE — every visual driven by tokens, theme-composer responds:
     Off track:    --text-primary 14% (theme-aware via color-mix)
     Off border:   --text-primary 8%
     Off thumb:    --switch-thumb-off  (default --surface-raised)
     On track:     --accent-primary       (theme-composer paints this)
     On border:    --accent-primary 60%
     On thumb:     --switch-thumb-on   (default white — high contrast on accent)

     Override per instance: set --switch-thumb-on / -off on the
     wrapper. E.g. on a hero with a tinted bg, set --switch-thumb-off
     to a custom mix to maintain contrast against the bg.

   STATES
     :checked       track tints with --accent-primary, thumb slides right
     :focus-visible accent outline on the track (keyboard nav only)
     :disabled      track muted, thumb dimmed, cursor not-allowed
     :hover         off → track lifts slightly, on → accent intensifies
*/
.switch {
  display: inline-flex;
  align-items: center;
  gap: var(--space-8);
  user-select: none;
  cursor: pointer;
  font-family: var(--font-sans);
  font-size: var(--text-body-sm);
  color: var(--text-secondary);
}

.switch-label {
  font-weight: var(--fw-medium);
  transition: color 0.15s ease, font-weight 0.15s ease;
  cursor: pointer;
}

.switch-label.is-active {
  color: var(--text-primary);
  font-weight: var(--fw-semibold);
}

/* Visually hide the real checkbox but keep it focusable + clickable.
   Clip-path keeps it interactive while removing all visual presence.
   Standard sr-only pattern — 1px dimensions + clip-path are required to
   keep the checkbox tap-targetable; not a layout decision so exempt
   from the 4px-grid rule. */
.switch-input {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
  font-family: inherit;                               /* defensive — <input> UA default is Arial; inherit brand Figtree (input equivalent of the button reset) */
}

.switch-track {
  /* ── Public custom properties ─────────────────────────────
     Override these on the wrapper to retune per instance:
       --switch-w / -h     track dimensions
       --switch-pad        thumb inset from track edge
       --switch-thumb-off  thumb color in OFF state
       --switch-thumb-on   thumb color in ON  state
       --switch-on-color   track fill in the ON state (the brand
                           accent by default; accent modifiers
                           swap this single variable, see below)
     The off/on tokens default to surface-raised / white — the
     standard recipe — but a hero with a custom bg can override
     to keep contrast.
  */
  --switch-w: 36px;
  --switch-h: 20px;
  --switch-pad: 2px;
  --switch-thumb: calc(var(--switch-h) - var(--switch-pad) * 2);
  --switch-thumb-off: var(--surface-raised);
  --switch-thumb-on: white;
  --switch-on-color: var(--accent-primary);

  /* content-box so --switch-w / -h describe the inner box that the
     thumb's top/left measure against. With the global border-box
     reset the 1 px borders would otherwise eat into the inset and
     the thumb would render 1 px high + 1 px right of where it
     should sit — visible misalignment on every size. */
  box-sizing: content-box;

  position: relative;
  display: inline-block;
  width: var(--switch-w);
  height: var(--switch-h);
  border-radius: 999px;
  background: color-mix(in oklch, var(--text-primary) 14%, transparent);
  border: 1px solid color-mix(in oklch, var(--text-primary) 8%, transparent);
  transition: background-color 0.18s ease, border-color 0.18s ease;
  flex-shrink: 0;
}

.switch-thumb {
  position: absolute;
  top: var(--switch-pad);
  left: var(--switch-pad);
  width: var(--switch-thumb);
  height: var(--switch-thumb);
  border-radius: 50%;
  background: var(--switch-thumb-off);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18), 0 1px 1px rgba(0, 0, 0, 0.06);
  transition: transform 0.22s cubic-bezier(0.34, 1.36, 0.64, 1), background-color 0.18s ease;
  pointer-events: none;
}

/* ── ON state — checkbox checked drives every visual.
   Track fill comes from --switch-on-color (theme-composer-aware
   default = --accent-primary; accent modifiers below swap this
   single variable). Thumb uses --switch-thumb-on so per-instance
   overrides keep contrast on custom backgrounds. */
.switch-input:checked + .switch-track {
  background: var(--switch-on-color);
  border-color: color-mix(in oklch, var(--switch-on-color) 60%, transparent);
}

.switch-input:checked + .switch-track .switch-thumb {
  background: var(--switch-thumb-on);
  transform: translateX(calc(var(--switch-w) - var(--switch-h)));
}

/* Focus ring — only when keyboard-focused (focus-visible), not on click. */
.switch-input:focus-visible + .switch-track {
  outline: 2px solid color-mix(in oklch, var(--switch-on-color) 50%, transparent);
  outline-offset: 2px;
}

/* Disabled — quiet everything, lock the cursor. */
.switch-input:disabled ~ * { cursor: not-allowed; }
.switch:has(.switch-input:disabled) {
  opacity: 0.55;
  cursor: not-allowed;
}

/* Hover — subtle track lift on the off state, accent intensifies on. */
.switch:hover .switch-track {
  background: color-mix(in oklch, var(--text-primary) 20%, transparent);
}
.switch:hover .switch-input:checked + .switch-track {
  background: color-mix(in oklch, var(--switch-on-color) 92%, var(--text-primary));
}

/* ── Sizes — sm / default / lg, matches system size convention.
   Each only adjusts the track dimensions; thumb derives via calc().
   Vertical centering with the form-control family is the contract:
     .switch-sm  ↔ .input--sm  / .btn-sm  / .seg-sm   row heights
     (default)   ↔ .input      / .btn     / .seg      row heights
     .switch-lg  ↔ .input--lg  / .btn-lg  / .seg-lg   row heights
   The switch is always shorter than the row it sits in (it's a
   widget, not a text control); align by vertical center. */
.switch-sm .switch-track {
  --switch-w: 28px;
  --switch-h: 16px;
}

.switch-lg .switch-track {
  --switch-w: 44px;
  --switch-h: 24px;
}

/* ── Backwards compatibility — keep .switch-md as an alias for
   .switch-lg so any unrenamed usage doesn't regress. New cards
   should always use --lg; this alias is here only to bridge the
   rename and can be removed once no usages remain. */
.switch-md .switch-track {
  --switch-w: 44px;
  --switch-h: 24px;
}

/* ── Accent modifiers — swap the single --switch-on-color variable.
   Mirrors the button family's semantic taxonomy. Compose with any
   size: e.g. <label class="switch switch-sm switch-success">. The
   default (no modifier) paints --accent-primary, so the palette
   composer keeps retinting unmodified switches automatically. */
.switch-success  .switch-track { --switch-on-color: var(--success); }
.switch-danger   .switch-track { --switch-on-color: var(--danger); }
.switch-accent2  .switch-track { --switch-on-color: var(--accent-secondary); }
.switch-accent3  .switch-track { --switch-on-color: var(--accent-tertiary); }

/* ── Reduced-motion guard — drop the cubic-bezier overshoot on the
   thumb's slide. Color shifts are kept (state changes, not motion);
   the bouncy translate is what motion-sensitive users react to. */
@media (prefers-reduced-motion: reduce) {
  .switch-thumb {
    transition: transform 0.01s linear, background-color 0.18s ease;
  }
}

/* ════════════════════════════════════════════════════════════════
   .cmd-palette + .search-trigger — command palette (⌘K)
   ════════════════════════════════════════════════════════════════
   A reusable centered overlay with search + optional scope filtering.
   Universal DS component — not catalog-specific. The catalog drives
   it via initDsCmdPalette() (a thin shim around the public
   cmdPalette() factory in shared.js); other surfaces can call
   cmdPalette({...}) directly with their own index + scope config.

   Promoted from `.ds-cmd-*` to `.cmd-palette*` April 2026 — same
   migration the form family went through. The `.ds-` prefix is
   reserved for catalog-meta primitives only; this is now a universal
   component.

   Design history (controlbar / palette):
     v1 — Pastel multi-select pill chips with mono eyebrow labels
          ("SCOPE", "DEPTH"), small switch, counter. Three visual
          languages in one row; chips dominated, switch disappeared.
     v2 — Single-select .seg segmented control + switch + counter,
          all in a sticky frosted bar.
     v3 — Inline scope-only horizontal segmented control on the page
          background between the hero and the layout.
     v4 — Moved into the .side-nav (then `.ds-doc-sidebar`) as a
          vertical .seg at the top of the rail. Sidebar widened
          220 px → 260 px to host it.
     v5 — ⌘K command palette. The rail seg was solving one half of
          the discovery problem (scope filter) but designers searching
          for a known component name still had to scan the sidebar.
          The palette folded search and scope into one surface
          accessible from ⌘K (or the rail trigger button).
     v6 (current) — Promoted from catalog-meta to universal DS
          component. Class names dropped the .ds- prefix; JS exposes
          a cmdPalette(options) factory so any surface can drop in
          a palette with its own index, scope vocabulary, and
          on-select behaviour.

   ANATOMY
     .search-trigger                  button that opens the palette
                                      (any surface — sidebar, navbar,
                                       FAB, inline link). Visually a
                                      faux-input + optional .kbd hint.
                                      Documented separately at
                                      `#cmd-palette` (consumer-facing).

     .cmd-palette                     the overlay shell — singleton
                                      per page, rendered once near
                                      </body>
       .cmd-palette__backdrop         full-viewport dim layer
       .cmd-palette__panel            the centered floating panel
         .cmd-palette__search         search input row (input + esc hint)
         .cmd-palette__scopes         optional scope row (label + .seg)
         .cmd-palette__results        scrollable result list
           .cmd-palette-result-group  one logical bucket
             .cmd-palette-result-item one matched entry
         .cmd-palette__empty          empty state (pre-typing + zero matches)

   STATES
     .search-trigger[data-active-scope]
                                    set by JS when scope ≠ 'all'
                                    (drives the accent border + bg)
     .cmd-palette[data-open]        set by JS during the open
                                    transition (drives the slide-in)
     body[data-cmd-open]            presence = palette is open
                                    (locks scroll + dims background)

   May 2026 — `.cmd-palette-trigger` was renamed to `.search-trigger`
   and lifted into a generic tier-2 primitive (input-shaped button
   that opens any modal, not just this palette). The cmd-palette
   modal still owns the `[data-active-scope]` wiring + the
   `cmdPalette({trigger: '.search-trigger'})` factory hookup, but the
   trigger primitive itself stands alone and ships its own card.
*/

/* ── .search-trigger — input-shaped button that opens a search modal ──
   A button styled to look like an input field: leading search icon,
   placeholder-style label, optional trailing keyboard hint chip
   (compose with `.kbd`). Semantically a <button> — clicks open an
   overlay (the .cmd-palette in this catalog; product surfaces may
   wire it to a different modal). Distinct from `.search`, which is
   a wrapper around a real <input type="search"> used on marketing
   hero bars where the user types directly into the field.

   Why an input shape and not a button — at a glance, every reader
   recognises "this is the search affordance" because every modern
   app trains them on the input-with-magnifier idiom (Linear, Vercel,
   Algolia DocSearch, GitHub). A pill button reads as a generic
   action; this primitive reads as "search lives here."

   Chrome uses the `--field-*` token family so it slots into form
   rows without drift. Three sizes (sm / default / lg) mirror the
   `.input` ladder.

   States: rest / hover / focus-visible / `[data-active-scope]`
   (when the modal is filtered the trigger paints a quiet accent
   border + tinted bg, so the reader knows a filter is on without
   opening the modal). */
.search-trigger {
  width: 100%;
  display: flex;
  align-items: center;
  gap: var(--space-12);
  /* Button-aligned chrome (flat fill, no visible outline, no focus
     ring outside keyboard nav). Side padding intentionally matches
     pad-Y at every tier so the trailing .kbd chip reads as having an
     equal halo of space on every side (vertical gap above/below the
     kbd = horizontal gap to the trigger's right edge). Pad-Y +
     radius + font stay locked to the .btn ladder so heights
     (35 / 46 / 50 px) match same-tier .btn siblings — only side
     padding diverges. Radius derives via calc() off --button-radius
     (default = the bare anchor; sm = anchor−4, lg = anchor+4) so it
     tracks the playground per-theme Button-radius slider. June 2026 —
     was literal var(--space-N) (value-paired but token-disconnected). */
  padding: var(--space-12);
  border-radius: var(--button-radius);
  background: var(--field-bg);
  border: 1px solid transparent;
  color: var(--text-primary);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  text-align: left;
  cursor: pointer;
  outline: none;
  box-sizing: border-box;
  transition: background 0.2s;
}
.search-trigger:hover {
  background: var(--field-bg-hover);
}
.search-trigger:focus-visible {
  background: var(--field-bg-focus);
  /* Keyboard-only focus ring — :focus-visible suppresses on mouse click,
     so the outline only appears during keyboard navigation. Required for
     WCAG 2.4.7 since the flat-fill chrome carries no border to thicken. */
  outline: 2px solid var(--accent-primary);
  outline-offset: 2px;
}

.search-trigger__icon {
  flex-shrink: 0;
  width: 16px; height: 16px;
  color: var(--field-placeholder);
  border-radius: 50%;
  /* Animate color + bg + halo together on parent hover. box-shadow
     carries the icon's hover halo (see :hover rule below) so the
     painted hover circle paints outside the icon's box without
     pushing layout — keeps the label position stable. */
  transition: color 0.18s ease, background 0.18s ease, box-shadow 0.18s ease;
}
.search-trigger:hover .search-trigger__icon,
.search-trigger:focus-visible .search-trigger__icon {
  /* Icon hover halo — a circular tinted bg around the magnifier glyph
     painted via background + box-shadow so the visible halo extends
     ~4 px outside the icon's 16 px footprint (effective ~24 px circle)
     without pushing the label rightward. Stronger contrast than the
     trigger's overall hover bg so the icon reads as the "search" hot
     zone within the pill — Read A from the May 2026 design review. */
  color: var(--text-primary);
  background: color-mix(in oklch, var(--text-primary) 10%, transparent);
  box-shadow: 0 0 0 4px color-mix(in oklch, var(--text-primary) 10%, transparent);
}

.search-trigger__label {
  flex: 1 1 auto;
  min-width: 0;
  /* Placeholder-style label — weight 400, muted color, matches the
     `.input::placeholder` treatment so the read-out is identical
     to a real input rest state. */
  color: var(--field-placeholder);
  font-weight: var(--fw-regular);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ── Sizes — pad-Y, radius, and font lock to the .btn ladder; side
   padding matches pad-Y so the trailing .kbd has an equal halo of
   space on every side. Net effect: lg has the same side padding as
   default (12 px), because the lg trigger only grew +4 px while the
   .kbd also grew +4 px — the gap math lands at the same token. ── */
.search-trigger--sm {
  padding: var(--space-8);
  border-radius: calc(var(--button-radius) - var(--space-4));
  font-size: var(--text-sm);
  gap: var(--space-8);
}
.search-trigger--sm .search-trigger__icon { width: 14px; height: 14px; }

.search-trigger--lg {
  padding: var(--space-12);
  border-radius: calc(var(--button-radius) + var(--space-4));
  font-size: var(--text-body);
  gap: var(--space-16);
}
.search-trigger--lg .search-trigger__icon { width: 18px; height: 18px; }

/* ── Active-scope state — when the linked modal is scope-filtered,
   the trigger paints a quiet accent border + tinted bg so the user
   knows a filter is on without opening the modal. The label text
   stays the same — scope name isn't broadcast on the trigger surface
   on purpose; the modal is the surface where scope is read + edited. */
.search-trigger[data-active-scope] {
  background: color-mix(in oklch, var(--accent-primary) 10%, transparent);
}
.search-trigger[data-active-scope] .search-trigger__icon,
.search-trigger[data-active-scope] .search-trigger__label {
  color: var(--accent-primary-mid);
}
.search-trigger[data-active-scope]:hover {
  background: color-mix(in oklch, var(--accent-primary) 16%, transparent);
}

/* ──────────────────────────────────────────────────────────────────
   .kbd — keyboard-hint chip
   ──────────────────────────────────────────────────────────────────
   Inline-flex micro-primitive that renders a keyboard shortcut as a
   compact key cap. Pair with anything that has a keyboard
   accelerator: .search-trigger (⌘K), menu rows ("Enter"), modal
   footers ("Esc to close"), command palette result rows.

   SIZING — mirrors the .btn ladder's arithmetic:
       padY × 2 + line-box = height
     --sm:    2 + 2 + 12 = 16px   (12px caption in 12px line-box)
     default: 4 + 4 + 12 = 20px   (12px caption in 12px line-box)
     --lg:    4 + 4 + 16 = 24px   (14px sm widened to 16px line-box
                                   via calc(16/14) — same trick
                                   .btn-lg uses to seat a 16px label
                                   on the 48px height ladder.)
   Heights step +4 — same +4 cadence the button radius ladder uses.
   Every value is a --space-* or --text-* token; no magic numbers.

   SURFACE — flat fill, no border. Single mix of --text-primary at
   10% alpha so the chip stands out against any container surface
   (search-trigger, menu row, modal footer, inline prose) without
   leaning on a hairline border. Auto-flips light/dark via the
   token, no per-theme rules needed.

   CLUSTERS — chain multiple .kbd chips with a thin space
   (&thinsp;) between them. Never use a + glyph; readers parse the
   spatial grouping as the shortcut, not the punctuation.

   SINGLE-CHAR FLOOR — min-width = total height per size, so a lone
   `K` or `/` chip renders as a perfect square next to a wider `⌘`
   neighbor without squishing. */
.kbd {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* default: padY 4 + line-box 12 = 20px height */
  min-width: var(--space-20);
  padding: var(--space-4) var(--space-8);
  border-radius: var(--space-4);
  background: color-mix(in oklch, var(--text-primary) 10%, transparent);
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  font-weight: var(--fw-medium);
  /* 12px font in 12px line-box — tight key-cap feel. */
  line-height: 1;
  vertical-align: baseline;
  white-space: nowrap;
}

/* Sizes — step +4 in height (16 / 20 / 24). */
.kbd--sm {
  /* padY 2 + line-box 12 = 16px height */
  min-width: var(--space-16);
  padding: var(--space-2) var(--space-4);
  border-radius: var(--space-4);
  font-size: var(--text-caption);
  line-height: 1;
}
.kbd--lg {
  /* padY 4 + line-box 16 = 24px height; 14px font widened to 16px
     line-box via calc(), same trick .btn-lg uses to seat a 16px
     label cleanly on the 48px height ladder. */
  min-width: var(--space-24);
  padding: var(--space-4) var(--space-12);
  border-radius: var(--space-8);
  font-size: var(--text-sm);
  line-height: calc(16 / 14);
}

/* Back-compat shim — the old .ds-doc-controlbar wrapper class still works
   as a no-op so any external consumer that authored the v4 markup
   doesn't break visually. The wrapper renders nothing on its own; the
   trigger button inside it carries all the chrome.

   Retired sub-classes kept as `display: none` aliases so v3-era markup
   (chip + meta + divider) doesn't visibly leak. */
.ds-doc-controlbar { display: contents; }
.ds-doc-controlbar-inner { display: contents; }
.ds-doc-controlbar-section { display: contents; }
.ds-doc-controlbar-note,
.ds-doc-controlbar-chip,
.ds-doc-controlbar-chip-dot,
.ds-doc-controlbar-divider,
.ds-doc-controlbar-meta,
.ds-doc-controlbar-meta-count,
.ds-doc-controlbar-meta-suffix,
.ds-doc-controlbar-label { display: none; }

/* ── .cmd-palette — overlay ─────────────────────────────────── */
/* The palette is always in the DOM (rendered once near the closing
   </body>); its own [hidden] attribute toggles visibility. We avoid
   `display: none` as the close mechanism because [hidden] both hides
   and disables interaction without losing animation potential. */
.cmd-palette[hidden] { display: none; }

.cmd-palette {
  position: fixed; inset: 0;
  z-index: 9000;
  /* Above the sticky navbar (z-index: 1000) and the FAB (z-index: 90)
     but below modal dialogs that may be authored later. */
  display: flex;
  align-items: flex-start;
  justify-content: center;
  /* Anchor the panel one-fifth from the top — easier on the eyes than
     dead-center, leaves room for results below the fold to read as a
     scroll affordance. */
  padding: 16vh 24px 24px;
  pointer-events: none;
}
.cmd-palette[data-open] { pointer-events: auto; }

.cmd-palette__backdrop {
  position: absolute; inset: 0;
  background: color-mix(in oklch, var(--ink-900) 50%, transparent);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  opacity: 0;
  transition: opacity 0.15s ease;
}
.cmd-palette[data-open] .cmd-palette__backdrop { opacity: 1; }

.cmd-palette__panel {
  position: relative;
  width: 100%;
  max-width: var(--w-10);
  max-height: 70vh;
  display: flex;
  flex-direction: column;
  /* Glass material — wired via :where(.cmd-palette__panel) so the
     panel reads as the same surface character as other floating
     glass panels (.ds-doc-menu-panel, .dropdown__panel,
     .playground-rail). The --elev-3 floating-tier shadow is
     overridden here because glass panels embed their own depth
     shadow in --shadow-panel-glass; the cmd-palette nominally
     wanted "modal-tier float" but in practice glass-panel-shadow
     reads with similar weight. */
  background: var(--mat-bg);
  border: var(--mat-border);
  box-shadow: var(--mat-shadow), var(--mat-highlight);
  backdrop-filter: var(--mat-backdrop);
  -webkit-backdrop-filter: var(--mat-backdrop);
  border-radius: var(--space-16);
  overflow: hidden;
  /* Slide-in entrance — disabled on prefers-reduced-motion. */
  transform: translateY(-8px) scale(0.98);
  opacity: 0;
  transition: transform 0.18s ease, opacity 0.15s ease;
}
.cmd-palette[data-open] .cmd-palette__panel {
  transform: translateY(0) scale(1);
  opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
  .cmd-palette__panel { transition: opacity 0.1s ease; transform: none; }
}

/* Search row — input fills the row; ⌘K hint chip on the right. */
.cmd-palette__search {
  display: flex;
  align-items: center;
  gap: var(--space-12);
  padding: var(--space-20) var(--space-16);
}
.cmd-palette__search-icon {
  width: var(--space-20); height: var(--space-20); flex-shrink: 0;
  color: var(--text-muted);
}
.cmd-palette__input {
  flex: 1;
  border: none;
  background: transparent;
  outline: none;
  padding: 0;
  font-family: var(--font-sans);
  font-size: var(--text-body);
  font-weight: var(--fw-medium);
  color: var(--text-primary);
}
.cmd-palette__input::placeholder { color: var(--text-muted); font-weight: var(--fw-regular); }
.cmd-palette__esc-hint {
  flex-shrink: 0;
  padding: var(--space-4) var(--space-8);
  border-radius: var(--space-4);
  background: color-mix(in oklch, var(--text-primary) 6%, transparent);
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  font-weight: var(--fw-semibold);
}

/* Scope row — hosts the canonical .chip primitive in a horizontal flow.
   Single-select filter (cmd+K convention — Linear / Vercel / Stripe);
   the controller toggles .is-active on the matching pill from the
   [data-scope] attribute. The row provides padding + flex; the chip
   carries shape, type, and tone. The is-active override is the only
   scoped rule — it gives the chip its accent-fill selected state,
   which the base .chip primitive doesn't ship (chips are status
   indicators by default; cmd-palette is the first interactive
   single-select consumer). If a second consumer appears, promote to
   `.chip--interactive` at the primitive level. */
.cmd-palette__scopes {
  padding: 0 var(--space-12) var(--space-8);
}
.cmd-palette__scopes .chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
}
.cmd-palette__scopes .chip {
  cursor: pointer;
  user-select: none;
  color: var(--text-muted);
  background: transparent;
  transition: background 0.12s ease, color 0.12s ease;
}
.cmd-palette__scopes .chip:hover {
  color: var(--text-primary);
  background: color-mix(in oklch, var(--text-primary) 6%, transparent);
}
.cmd-palette__scopes .chip:focus-visible {
  outline: 2px solid var(--accent-primary);
  outline-offset: 2px;
}
.cmd-palette__scopes .chip.is-active,
.cmd-palette__scopes .chip[aria-selected="true"] {
  color: var(--accent-primary);
  background: color-mix(in oklch, var(--accent-primary) 12%, transparent);
}

/* Results list — flexes to fill remaining height, scrolls on overflow. */
.cmd-palette__results {
  flex: 1;
  overflow-y: auto;
  padding: var(--space-8) var(--space-8) var(--space-12);
  scrollbar-width: thin;
  scrollbar-color: color-mix(in oklch, var(--text-primary) 8%, transparent) transparent;
}
.cmd-palette__results::-webkit-scrollbar { width: var(--space-8); }
.cmd-palette__results::-webkit-scrollbar-thumb {
  background: color-mix(in oklch, var(--text-primary) 8%, transparent);
  border-radius: var(--space-4);
}

.cmd-palette-result-group {
  /* Group wrapper kept for JS rendering compatibility, but the visible group
     header is retired (May 2026 — Tailwind-style redesign). Category is now
     carried by each result row (.cmd-palette-result-item__category) so every
     row is self-contained and tells the reader where it lives without
     needing the eye to drift back up to a header. */
  margin: 0;
}
.cmd-palette-result-group__label {
  /* Header retired — JS may still emit it for backward compat with existing
     consumers; hidden via display:none so the new per-row category slot is
     the sole hierarchy signal. */
  display: none;
}

.cmd-palette-result-item {
  display: flex;
  align-items: flex-start;
  gap: var(--space-12);
  width: 100%;
  padding: var(--space-8) var(--space-12);
  border: none;
  border-radius: var(--space-8);
  background: transparent;
  text-align: left;
  text-decoration: none;
  color: inherit;
  cursor: pointer;
  transition: background 0.1s ease;
}
.cmd-palette-result-item:hover,
.cmd-palette-result-item.is-highlighted {
  background: color-mix(in oklch, var(--accent-primary) 8%, transparent);
}
.cmd-palette-result-item__icon {
  width: var(--space-16); height: var(--space-16); flex-shrink: 0;
  margin-top: var(--space-4);
  color: var(--text-muted);
}
.cmd-palette-result-item:hover .cmd-palette-result-item__icon,
.cmd-palette-result-item.is-highlighted .cmd-palette-result-item__icon {
  color: var(--accent-primary-mid);
}
.cmd-palette-result-item__body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);                          /* tighter — three lines, not two */
}
/* Per-row category caption — replaces the previous group header. Mono
   uppercase tracking-wide caption sits above the title so the reader sees
   "this result is in Buttons" without needing a separate header row.
   Tailwind-pattern: every row is self-contained. */
.cmd-palette-result-item__category {
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-muted);
  line-height: 1.4;
}
.cmd-palette-result-item:hover .cmd-palette-result-item__category,
.cmd-palette-result-item.is-highlighted .cmd-palette-result-item__category {
  color: var(--accent-primary-mid);
}
.cmd-palette-result-item__label {
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: var(--fw-semibold);
  color: var(--text-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-top: var(--space-2);                   /* breathing room below category */
}
.cmd-palette-result-item__caption {
  font-family: var(--font-sans);
  font-size: var(--text-caption);
  font-weight: var(--fw-regular);
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.cmd-palette-result-item__enter-hint {
  flex-shrink: 0;
  margin-top: 0;
  padding: var(--space-4) var(--space-8);
  border-radius: var(--space-4);
  background: color-mix(in oklch, var(--accent-primary) 14%, transparent);
  color: var(--accent-primary-mid);
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  font-weight: var(--fw-bold);
  opacity: 0;
}
.cmd-palette-result-item.is-highlighted .cmd-palette-result-item__enter-hint { opacity: 1; }

/* Highlight matched substring inside the label — the JS wraps matched
   chars in `<mark>`; we just style the mark. */
.cmd-palette-result-item mark {
  background: color-mix(in oklch, var(--accent-primary) 22%, transparent);
  color: var(--accent-primary-mid);
  padding: 0;
  border-radius: var(--space-4);
}

/* Footer keyboard hints — quiet bar at panel bottom mirroring Tailwind's
   DocSearch. Replaces an attribution slot we don't need (we're not
   actually using Algolia); honest signal to power-users about what the
   keyboard does. Optional in markup — palettes that don't author it
   render without. */
.cmd-palette__footer {
  display: flex;
  align-items: center;
  gap: var(--space-16);
  flex-wrap: wrap;
  padding: var(--space-8) var(--space-16);
  border-top: 1px solid color-mix(in oklch, var(--text-primary) 6%, transparent);
  background: color-mix(in oklch, var(--text-primary) 2%, transparent);
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-muted);
}
.cmd-palette__footer-hint {
  display: inline-flex;
  align-items: center;
  gap: var(--space-6);
}
.cmd-palette__footer-key {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--space-16);
  padding: 0 var(--space-4);
  border-radius: var(--space-4);
  background: color-mix(in oklch, var(--text-primary) 8%, transparent);
  color: var(--text-primary);
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  line-height: 1.4;
}

/* Empty state — shows when the search returns no matches. */
.cmd-palette__empty {
  padding: var(--space-32) var(--space-24);
  text-align: center;
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  color: var(--text-muted);
}
.cmd-palette__empty strong { color: var(--text-primary); font-weight: var(--fw-semibold); }

/* Body lock when palette is open — prevents background scroll behind
   the overlay. Applied via JS toggling [data-cmd-open] on <body>. */
body[data-cmd-open] { overflow: hidden; }

/* ── Mobile: full-screen sheet ─────────────────────────────────── */
@media (max-width: 640px) {
  .cmd-palette { padding: 0; }
  .cmd-palette__panel {
    width: 100%; max-width: 100%; max-height: 100vh;
    border-radius: 0;
    border: none;
  }
}

/* ── .ds-doc-scope-notice — "X cards hidden by current filter" banner ──
   Rendered inline at the top of any section whose cards are partly
   hidden by the active scope filter. The trigger button at the top
   of the rail is the system-wide indicator of an active filter, but
   it's quiet and sits above the fold — designers deep-scrolled into
   a long section don't see it. This per-section notice catches the
   case where the section reads as "broken" because half its cards
   are missing.

   Click → opens the command palette so the user can change scope.
   Hidden when the section has zero hidden cards (default state) or
   when the section itself is entirely hidden (no point announcing
   filter state on something invisible).

   Anatomy
     .ds-doc-scope-notice                outer button — accent-tinted
       .ds-doc-scope-notice__icon        leading filter glyph
       .ds-doc-scope-notice__text        "X cards hidden by current filter"
       .ds-doc-scope-notice__kbd         "⌘K" mono badge
*/
.ds-doc-scope-notice {
  display: flex;
  align-items: center;
  gap: var(--space-12);
  width: 100%;
  margin: var(--space-16) 0 var(--space-24);
  padding: var(--space-12) var(--space-16);
  border: 1px solid color-mix(in oklch, var(--accent-primary) 22%, transparent);
  border-radius: var(--space-12);
  background: color-mix(in oklch, var(--accent-primary) 6%, transparent);
  color: var(--accent-primary-mid);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: var(--fw-medium);
  text-align: left;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease;
}
.ds-doc-scope-notice:hover {
  background: color-mix(in oklch, var(--accent-primary) 10%, transparent);
  border-color: color-mix(in oklch, var(--accent-primary) 32%, transparent);
}
.ds-doc-scope-notice:focus-visible {
  outline: 2px solid var(--accent-primary-mid);
  outline-offset: 2px;
}
.ds-doc-scope-notice[hidden] { display: none; }
.ds-doc-scope-notice__icon {
  width: var(--space-16); height: var(--space-16); flex-shrink: 0;
  color: var(--accent-primary-mid);
}
.ds-doc-scope-notice__text {
  flex: 1;
}
.ds-doc-scope-notice__text strong {
  font-weight: var(--fw-bold);
}
.ds-doc-scope-notice__kbd {
  flex-shrink: 0;
  padding: var(--space-4) var(--space-8);
  border-radius: var(--space-4);
  background: color-mix(in oklch, var(--accent-primary) 14%, transparent);
  color: var(--accent-primary-mid);
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  font-weight: var(--fw-bold);
}

/* ── Scope-filter visibility — single class, applied by JS ──────── */
.is-scope-hidden { display: none !important; }

/* ════════════════════════════════════════════════════════════════
   .ds-doc-icon-grid  /  .ds-doc-avatar-grid — asset-library catalog grids
   ════════════════════════════════════════════════════════════════
   Two parallel grid primitives for the Libraries section of the
   catalog. Both auto-fill at a fixed minimum cell width so the grid
   reflows cleanly across viewports.

   Differences:
     - `.ds-doc-icon-grid` — icons are 24×24 line glyphs, inline SVG so
       `currentColor` follows text + palette.
     - `.ds-doc-avatar-grid` — avatars are 40×40 pre-plated brand shapes
       (the default `.avatar--md`) referenced via `<img>`; palette
       doesn't apply (avatars carry their own brand colours).

   For full-logo brand wordmarks (varied aspect, dark+light pairs)
   use `.ds-doc-partner-logo-grid` — wider cells, no plate.

   Both use a quiet figure / figcaption pattern — the asset is the
   subject; the filename below is a quiet mono caption for grep+paste.
*/

.ds-doc-icon-grid,
.ds-doc-avatar-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(112px, 1fr));
  gap: var(--space-8);
}

.ds-doc-icon-cell,
.ds-doc-avatar-cell {
  position: relative;   /* anchors the per-asset download overlay below */
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-8);
  margin: 0;
  padding: var(--space-16) var(--space-8);
  border-radius: 8px;
  background: color-mix(in oklch, currentColor 3%, transparent);
  border: 1px solid color-mix(in oklch, currentColor 6%, transparent);
  transition: background-color 0.15s ease, border-color 0.15s ease;
}

.ds-doc-icon-cell:hover,
.ds-doc-avatar-cell:hover {
  background: color-mix(in oklch, var(--accent-primary) 6%, transparent);
  border-color: color-mix(in oklch, var(--accent-primary) 22%, transparent);
}

/* Direct-child only — the cell ALSO hosts the injected download button
   (.ds-doc-cell-dl), whose own 12px glyph must keep the button family's
   sizing. A bare descendant `svg` here captured it and inflated the
   glyph to fill the 24px button (June 2026). */
.ds-doc-icon-cell > svg {
  width: 24px;
  height: 24px;
  color: var(--text-primary);
  flex-shrink: 0;
}

.ds-doc-icon-cell figcaption,
.ds-doc-avatar-cell figcaption {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  line-height: 1.3;
  color: var(--text-muted);
  text-align: center;
  word-break: break-word;
  max-width: 100%;
}

.ds-doc-avatar-cell .avatar {
  flex-shrink: 0;
}

/* Per-asset download overlay — a hover/focus-revealed download BUTTON on
   every avatar / portrait / icon / partner-logo cell, decorated by the
   assets.html script (see the "Per-asset download" IIFE there; partner
   cells download whichever file of their dark/light pair the cell SHOWS —
   theme-matching by default, the other one while mode-flipped via the
   .ds-doc-cell-flip toggle below). `.ds-doc-cell-dl` is a stock
   24px circular primary icon button — an ANCHOR composing
   `btn-primary btn-icon btn-xs btn-circle` — top-right. The button
   family owns ALL chrome (fill, on-accent ink, hover, focus, glyph
   sizing); this class is POSITIONING + reveal only. Don't add fill or
   typography here. (Lineage: started as a 32px button + extension badge
   pair — retired, a 112px cell's top band can't host two overlays plus a
   24px glyph; then a labeled `.chip.chip-accent1` with a hand-borrowed
   btn-primary fill — retired June 2026 when the label dropped, because a
   lone actionable glyph IS an icon button; a "flush" icon-only chip
   variant was considered and rejected as a duplicate of this composition.)
   The `transition` shorthand deliberately replaces the button base list —
   opacity (the reveal) + background (btn-primary's hover) are the only
   animating properties here.
   Hidden at rest so the 300+-cell grids stay quiet; :focus-within reveals
   for keyboard users tabbing onto the anchor. */
.ds-doc-cell-dl {
  position: absolute;
  top: var(--space-8);
  right: var(--space-8);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.15s ease, background-color 0.2s ease;
}

.ds-doc-icon-cell:hover .ds-doc-cell-dl,
.ds-doc-avatar-cell:hover .ds-doc-cell-dl,
.ds-doc-partner-logo-cell:hover .ds-doc-cell-dl,
.ds-doc-icon-cell:focus-within .ds-doc-cell-dl,
.ds-doc-avatar-cell:focus-within .ds-doc-cell-dl,
.ds-doc-partner-logo-cell:focus-within .ds-doc-cell-dl {
  opacity: 1;
  pointer-events: auto;
}

/* Per-asset mode flip — partner-logo cells only (the cells holding a
   dark/light pair). A second revealed overlay, top-LEFT mirror of the
   download: a stock NEUTRAL xs circle (btn-neutral btn-icon btn-xs
   btn-circle), injected by the same assets.html IIFE. Neutral, not
   primary — the download is the cell's primary action; the flip is a
   preview affordance, and two primary pills per cell would compete.
   Like .ds-doc-cell-dl, this class is POSITIONING + reveal only. */
.ds-doc-cell-flip {
  position: absolute;
  top: var(--space-8);
  left: var(--space-8);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.15s ease, background-color 0.2s ease;
}

.ds-doc-partner-logo-cell:hover .ds-doc-cell-flip,
.ds-doc-partner-logo-cell:focus-within .ds-doc-cell-flip {
  opacity: 1;
  pointer-events: auto;
}

/* Flipped state (.is-flipped, toggled by the flip button) — renders the
   cell inline in the OPPOSITE mode: an opaque cross-mode plate + the
   other variant of the IMG pair + re-inked caption. The plate colours are
   OKLCH literals BY NECESSITY — they reproduce the *other* theme's
   page-seed + the cell's 3% wash, which the active theme's --surface-*
   tokens cannot express (they always resolve to the CURRENT mode). Keep
   them in lockstep with the :root / [data-theme="light"] --surface-page
   seeds. Flip state is relative to the page theme; the page-flip
   observer clears it (see the assets.html IIFE). */
.ds-doc-partner-logo-cell.is-flipped {
  /* dark page → flipped cell shows the LIGHT plate */
  --flip-plate:  color-mix(in oklch, oklch(8% 0.015 268) 3%, oklch(100% 0 268));
  --flip-border: color-mix(in oklch, oklch(8% 0.015 268) 10%, oklch(100% 0 268));
  --flip-ink:    oklch(0% 0 0 / 0.550);   /* light-mode --text-muted */
  background: var(--flip-plate);
  border-color: var(--flip-border);
}
[data-theme="light"] .ds-doc-partner-logo-cell.is-flipped {
  /* light page → flipped cell shows the DARK plate */
  --flip-plate:  color-mix(in oklch, oklch(100% 0 268) 4%, oklch(8% 0.015 268));
  --flip-border: color-mix(in oklch, oklch(100% 0 268) 12%, oklch(8% 0.015 268));
  --flip-ink:    oklch(100% 0 0 / 0.550); /* dark-mode --text-muted */
}
.ds-doc-partner-logo-cell.is-flipped:hover {
  background: color-mix(in oklch, var(--accent-primary) 6%, var(--flip-plate));
  border-color: color-mix(in oklch, var(--accent-primary) 22%, var(--flip-border));
}
.ds-doc-partner-logo-cell.is-flipped figcaption {
  color: var(--flip-ink);
}

/* The flip toggle itself must follow the FLIPPED plate, not the page —
   .btn-neutral derives its entire recipe (8% fill, ink, 14% hover, 18%
   active) from var(--text-primary), so re-scoping that one token on the
   toggle re-inks the whole button to the opposite mode. Values mirror the
   :root / [data-theme="light"] --text-primary literals (cross-mode by
   necessity, same as the plate above). */
.ds-doc-partner-logo-cell.is-flipped .ds-doc-cell-flip {
  --text-primary: oklch(0% 0 0);    /* light plate → light-mode ink */
}
[data-theme="light"] .ds-doc-partner-logo-cell.is-flipped .ds-doc-cell-flip {
  --text-primary: oklch(100% 0 0);  /* dark plate → dark-mode ink */
}

/* The variant swap — beats the page-theme pair rules on specificity
   ((0,3,0) / (0,4,0) vs the theme rules' (0,2,0) / (0,3,0)). */
.ds-doc-partner-logo-cell.is-flipped .ds-doc-partner-logo__dark  { display: none; }
.ds-doc-partner-logo-cell.is-flipped .ds-doc-partner-logo__light { display: block; }
[data-theme="light"] .ds-doc-partner-logo-cell.is-flipped .ds-doc-partner-logo__dark  { display: block; }
[data-theme="light"] .ds-doc-partner-logo-cell.is-flipped .ds-doc-partner-logo__light { display: none; }

/* Tier 4 of the pane-breathing cascade — zone separation between category
   groups in an asset-library pane. Each category is a distinct zone:
   eyebrow → text → grid (Chains / Tokens / DEXs … in #avatars; Personas /
   Team / Plate options in #portraits). Without this, one zone's grid butts
   directly against the next zone's eyebrow — the grid carries no bottom
   margin and the eyebrow no top margin, so adjacent zones touch (0 gap).
   A 64px top margin on a grid-following eyebrow is the documented Tier-4
   zone break. Scoped under .ds-doc-viewport-pane for (0,3,0) specificity so
   it wins over the (0,2,0) flat-zone Tier-3 default above. See CLAUDE.md →
   "Pane breathing room — 4-tier cascade". */
.ds-doc-viewport-pane .ds-doc-avatar-grid + .ds-doc-eyebrow,
.ds-doc-viewport-pane .ds-doc-icon-grid + .ds-doc-eyebrow {
  margin-top: var(--space-64);
}

/* (v4 controlbar rules retired April 2026 — moved to the .cmd-palette
   block above. The remaining `.ds-doc-controlbar*` aliases collapse to
   `display: contents` / `display: none` so v4 markup keeps rendering
   without visible chrome while we drain consumers.) */

/* (Retired Apr 2026) — `.ds-doc-hero-meta` and its sub-classes lived
   here as a hero "update stamp" pill. Replaced when the catalog
   hero was unified to the canonical `.doc-header` shape with a
   standard `.header-eyebrow` (matching every other DS sub-page).
   Nothing in the codebase ships these classes anymore; the block
   was removed in full. The collector still emits `meta.lastUpdated`
   in `dashboard-data.json` for any future surface that wants to
   bind a date — see `dashboard-live.js` for the data-dash injector. */

/* ════════════════════════════════════════════════════════════════
   .tab-nav — page-level horizontal tab navigation
   ════════════════════════════════════════════════════════════════
   A horizontal rail of section anchors that sticks to the top of
   the page when scrolling. Used by every DS sub-page, available to
   product UI and marketing landings with the same vocabulary.

   The active link tracks the currently-visible section via an
   `IntersectionObserver` set up by the page's own JS (the component
   itself is dumb DOM — it just renders the `is-active` class).

   ANATOMY
     .tab-nav                     outer rail — sticky, frosted, hairline border
       .container                 width-clamped row container (system primitive)
         .tab-nav-list            flex row of anchors — overflow-x: auto on narrow
           a                      anchor — underline-on-hover, accent on active
       .tab-nav-logo              optional left slot — logo + vertical hairline

   SIZES
     .tab-nav-sm   40 px tall · 13 px text   — dense surfaces, sub-section navs
     (default)     56 px tall · 14 px text   — most pages
     .tab-nav-lg   72 px tall · 16 px text   — primary page nav on long surfaces
                                                (every DS sub-page uses this)

   STATES
     a:hover               text → primary, underline → accent at 50%
     a:focus-visible       2 px accent outline (keyboard nav)
     a.is-active           text → primary, underline → accent at 100%

   Why a 3-size system instead of one fixed height:
     The same nav serves three contexts at very different reading
     densities. A long marketing page wants the nav to feel like
     primary chrome (big), a settings panel wants it to feel like
     supporting infrastructure (medium), a dense data view wants
     it to feel like a quiet tab strip (small). Tokenising the
     height + font-size lets one component cover all three without
     forking into separate primitives.
*/
.tab-nav {
  --tab-nav-h:        56px;
  --tab-nav-fs:       var(--text-sm);
  --tab-nav-pad-x:    var(--space-16);
  --tab-nav-underline: 2px;

  position: sticky;
  top: 0;
  z-index: 90;
  /* No background fill — the strip reads as part of the page (Apr 2026:
     dropped the frosted-glass block that used to sit behind the tabs).
     The 1 px hairline track returned Apr 2026 v4 because the active-
     indicator-only treatment failed to read as a tab-bar; without a
     track the 2 px underline floats and you can't tell at a glance
     that the row is a tabstrip. Track is drawn via inset box-shadow
     (not border-bottom) so it sits at the same baseline as the active
     link's 2 px indicator — the indicator visually replaces the 1 px
     track in its segment, which is the conventional tab-bar look. */
  background: transparent;
  border-bottom: 0;
  box-shadow: inset 0 -1px 0 var(--border-subtle);
}

/* Inner row — uses the system `.container` for width-clamping. */
.tab-nav > .container {
  display: flex;
  align-items: center;
  gap: 0;
  height: var(--tab-nav-h);
}

.tab-nav-list {
  display: flex;
  align-items: center;
  gap: 0;
  flex: 1;
  min-width: 0;
  height: 100%;
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}

.tab-nav-list::-webkit-scrollbar { display: none; }

.tab-nav-list a {
  position: relative;
  display: inline-flex;
  align-items: center;
  height: 100%;
  /* Fallback to --space-16 so .tab-nav-list works standalone (outside
     a .tab-nav parent that sets --tab-nav-pad-x). Used by chart-internal
     tab strips that don't need the sticky positioning of .tab-nav. */
  padding: 0 var(--tab-nav-pad-x, var(--space-16));
  font-family: var(--font-sans);
  font-size: var(--tab-nav-fs);
  font-weight: var(--fw-medium);
  line-height: 1;
  color: var(--text-secondary);
  text-decoration: none;
  white-space: nowrap;
  flex-shrink: 0;
  border-bottom: var(--tab-nav-underline) solid transparent;
  /* Top border matches bottom width so vertical alignment doesn't
     shift when the bottom thickens on hover/active. */
  border-top: var(--tab-nav-underline) solid transparent;
  transition: color 0.18s ease, border-color 0.18s ease;
}

.tab-nav-list a:hover {
  color: var(--text-primary);
  border-bottom-color: color-mix(in oklch, var(--accent-primary) 50%, transparent);
}

.tab-nav-list a:focus-visible {
  outline: 2px solid color-mix(in oklch, var(--accent-primary) 50%, transparent);
  outline-offset: -4px;
  border-radius: var(--space-4);
}

.tab-nav-list a.is-active {
  /* EXPERIMENT (Apr 2026 v4): label uses --accent-primary instead of
     --text-primary so the text and the underline read as one signal.
     Reversible — flip back to var(--text-primary) if it reads loud. */
  color: var(--accent-primary);
  border-bottom-color: var(--accent-primary);
  font-weight: var(--fw-semibold);
}
/* No glow on the active underline. Apr 2026 v4: dropped the blurred
   pseudo-element bloom — the 2 px accent underline sitting on the
   1 px hairline track is a strong enough active signal, and the halo
   read as decorative chrome on horizontal navs. Mirrored on
   .nav-link-item.active in the marketing navbar. */

/* Optional left-side logo / branding slot — for surfaces that want
   a vertical hairline + logo before the links. */
.tab-nav-logo {
  display: flex;
  align-items: center;
  color: var(--text-primary);
  margin-right: var(--space-20);
  padding-right: var(--space-20);
  border-right: 1px solid color-mix(in oklch, var(--text-primary) 8%, transparent);
  flex-shrink: 0;
}
.tab-nav-logo svg { display: block; }

/* ── Sizes ─────────────────────────────────────────────────────── */
.tab-nav-sm {
  --tab-nav-h:     40px;
  --tab-nav-fs:    var(--text-caption);
  --tab-nav-pad-x: var(--space-12);
}

.tab-nav-lg {
  --tab-nav-h:     72px;
  --tab-nav-fs:    var(--text-base);
  --tab-nav-pad-x: var(--space-20);
  /* Apr 2026: 3px → 2px so .tab-nav-lg matches the base navbar
     underline thickness — both nav primitives share the same
     active-state visual recipe (text-tight 2px + glow). */
  --tab-nav-underline: 2px;
}

@media (max-width: 720px) {
  /* Tighten padding so more tabs fit on narrow viewports — the
     overflow-x: auto already handles the scroll fallback. */
  .tab-nav-list a       { padding: 0 var(--space-12); }
  .tab-nav-sm  .tab-nav-list a { padding: 0 var(--space-8); font-size: 12px; }
  .tab-nav-lg  .tab-nav-list a { padding: 0 var(--space-16); font-size: var(--text-sm); }
  .tab-nav-lg  { --tab-nav-h: 56px; }   /* lg drops to default size on phones */
}

/* ════════════════════════════════════════════════════════════════════
   COMPOSER FRAME — shared wrapper for the three composers
   ────────────────────────────────────────────────────────────────────
   Palette / Material / Semantic Composers all read as floating glass
   tools. The frame applies the canonical Panel Surfaces recipe (one
   token triplet, see styles.css §PANEL SURFACES) plus a generous
   border-radius + padding so the workspace inside reads as a discrete
   widget — not page chrome.

   Default frame fills its container; the `--compact` modifier caps it
   at 340px (the FAB sub-panel mockup width — Theme Composer's case).
   Inner layout (.screen-header title, content, .ds-doc-panel-btn-row
   footer) composes inside via the documented primitives — the frame
   only provides the container.

   May 2026 v18: the head primitive migrated from .panel-head to
   .screen-header. The previous .composer-frame > .panel-head override
   (forced a flex-centered shape for non-compact frames) is no longer
   needed — .screen-header's :has() adaptive grid handles title-only
   composer demos natively via the
   `:not(:has(__back)):not(:has(__trailing))` → `1fr` rule. Title is
   centered via .screen-header__title's `text-align: center`.
   ════════════════════════════════════════════════════════════════════ */
.composer-frame {
  background: var(--surface-panel-glass);
  box-shadow: var(--shadow-panel-glass);
  backdrop-filter: var(--backdrop-panel-glass);
  -webkit-backdrop-filter: var(--backdrop-panel-glass);
  border-radius: var(--space-20);
  padding: var(--space-24);
  display: flex;
  flex-direction: column;
  gap: var(--space-20);
}
.composer-frame--compact {
  max-width: 340px;
  margin: 0 auto;
  padding: var(--space-20);
}

/* ════════════════════════════════════════════════════════════════════
   MATERIAL COMPOSER  (May 2026)
   ────────────────────────────────────────────────────────────────────
   Live editor for the Materials layer. Inline tuner in the catalog
   (#material-composer) plus a full-screen modal workspace for serious
   editing. JS in shared.js → initMaterialComposer().

   Architecture: sliders mutate `--material-<name>-*` tokens directly
   on :root via JS. The cascade flows naturally — every consumer that
   reads `var(--mat-*)` repaints automatically when the upstream
   --material-* values change.

   Strict in-session-only contract (LI.FI 1.0 default rule). Saved
   variants live in localStorage but are NEVER auto-loaded on page
   init — the user explicitly clicks a saved variant to apply it.
   ════════════════════════════════════════════════════════════════════ */

/* Inline workspace inside #material-composer card. Two-column layout:
   preview tray on the left, controls + sliders on the right. Stacks on
   narrow viewports. */
.material-composer {
  display: grid;
  grid-template-columns: minmax(0, 1.2fr) minmax(280px, 1fr);
  gap: var(--space-24);
  align-items: start;
}
@media (max-width: 880px) {
  .material-composer { grid-template-columns: 1fr; }
}

.material-composer-tray {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-16);
  padding: var(--space-24);
  background: var(--surface-sunk);
  border-radius: 16px;
  min-height: 320px;
}

/* Each preview surface in the tray reads --mat-* like a real consumer.
   Setting data-material on the surface picks the material to wear. */
.material-composer-surface {
  background: var(--mat-bg);
  border: var(--mat-border);
  box-shadow: var(--mat-shadow), var(--mat-highlight);
  backdrop-filter: var(--mat-backdrop);
  -webkit-backdrop-filter: var(--mat-backdrop);
  border-radius: 14px;
  padding: var(--space-20);
  min-height: 120px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--space-8);
  transition: background 200ms, box-shadow 200ms, backdrop-filter 200ms, border 200ms;
}

.material-composer-surface--sm {
  min-height: 88px;
  padding: var(--space-12);
}

.material-composer-surface--span {
  grid-column: 1 / -1;
}

.material-composer-surface__label {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
}

.material-composer-surface__title {
  font-size: var(--text-body-lg);
  font-weight: var(--fw-semibold);
  color: var(--text-primary);
}

.material-composer-surface__body {
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

/* ── Right column: controls ──────────────────────────────────────── */
.material-composer-controls {
  display: flex;
  flex-direction: column;
  gap: var(--space-16);
}

.material-composer-controls__head {
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
}

.material-composer-controls__label {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
}

/* Material picker — segmented control sized for 6 entries. */
.material-composer-picker {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 2px;
  padding: 4px;
  background: var(--surface-sunk);
  border-radius: 12px;
}
.material-composer-picker__item {
  appearance: none;
  border: 0;
  background: transparent;
  padding: var(--space-8) var(--space-4);
  border-radius: 8px;
  font-family: var(--font-sans);
  font-size: var(--text-caption);
  color: var(--text-secondary);
  cursor: pointer;
  transition: background 160ms, color 160ms;
}
.material-composer-picker__item:hover {
  color: var(--text-primary);
  background: color-mix(in oklch, var(--text-primary) 6%, transparent);
}
.material-composer-picker__item.is-active {
  background: var(--surface-card);
  color: var(--text-primary);
  font-weight: var(--fw-semibold);
  box-shadow: var(--elev-1);
}

.material-composer-sliders {
  display: flex;
  flex-direction: column;
  gap: var(--space-12);
}
.material-composer-slider[data-disabled] {
  opacity: 0.4;
  pointer-events: none;
}
.material-composer-slider[data-disabled] .ds-doc-control-val::after {
  content: " · n/a";
  color: var(--text-muted);
  font-style: italic;
}

.material-composer-actions {
  display: flex;
  gap: var(--space-8);
  flex-wrap: wrap;
}

/* ── Modal workspace ─────────────────────────────────────────────── */
.material-composer-modal {
  position: fixed;
  inset: 0;
  z-index: 9000;
  display: none;
}
.material-composer-modal.is-open {
  display: block;
}

.material-composer-modal__backdrop {
  position: absolute;
  inset: 0;
  background: oklch(0% 0 0 / 0.5);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}

.material-composer-modal__panel {
  position: absolute;
  inset: 5%;
  background: var(--surface-card);
  border-radius: 20px;
  box-shadow: var(--elev-3);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
@media (max-width: 720px) {
  .material-composer-modal__panel { inset: 0; border-radius: 0; }
}

.material-composer-modal__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-20) var(--space-24);
  border-bottom: 1px solid color-mix(in oklch, var(--text-primary) 8%, transparent);
}

.material-composer-modal__title {
  font-size: var(--text-h3);
  font-weight: var(--fw-bold);
  color: var(--text-primary);
  margin: 0;
}

.material-composer-modal__close {
  appearance: none;
  border: 0;
  background: transparent;
  font-family: var(--font-sans);                     /* defensive — <button> UA default is Arial; render brand Figtree (renders a × glyph at --text-h3) */
  width: 36px;
  height: 36px;
  border-radius: 10px;
  color: var(--text-secondary);
  cursor: pointer;
  font-size: var(--text-h3);
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.material-composer-modal__close:hover {
  background: color-mix(in oklch, var(--text-primary) 8%, transparent);
  color: var(--text-primary);
}

.material-composer-modal__body {
  flex: 1 1 auto;
  display: grid;
  grid-template-columns: minmax(0, 2fr) 320px;
  gap: var(--space-24);
  padding: var(--space-24);
  overflow-y: auto;
}
@media (max-width: 880px) {
  .material-composer-modal__body { grid-template-columns: 1fr; }
}

.material-composer-modal__workspace {
  display: flex;
  flex-direction: column;
  gap: var(--space-24);
}

/* In modal, give the tray more breathing room */
.material-composer-modal__workspace .material-composer-tray {
  grid-template-columns: repeat(2, 1fr);
  min-height: 380px;
  padding: var(--space-32);
}

.material-composer-modal__sidebar {
  display: flex;
  flex-direction: column;
  gap: var(--space-20);
  padding: var(--space-20);
  background: var(--surface-sunk);
  border-radius: 14px;
  align-self: start;
}

.material-composer-modal__sidebar-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--space-8);
}

.material-composer-modal__sidebar-label {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
}

.material-composer-variants {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  max-height: 320px;
  overflow-y: auto;
}

.material-composer-variant {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-8) var(--space-12);
  background: var(--surface-card);
  border-radius: 10px;
  font-size: var(--text-sm);
  color: var(--text-primary);
  cursor: pointer;
  transition: background 160ms;
  gap: var(--space-8);
}
.material-composer-variant:hover {
  background: color-mix(in oklch, var(--accent-primary) 8%, var(--surface-card));
}

.material-composer-variant__name {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  flex: 1 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.material-composer-variant__delete {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  font-family: var(--font-sans);                     /* defensive — <button> UA default is Arial; render brand Figtree if ever given a text label / ::after glyph */
  padding: 4px;
  border-radius: 6px;
  flex-shrink: 0;
}
.material-composer-variant__delete:hover {
  background: color-mix(in oklch, var(--neg) 12%, transparent);
  color: var(--neg);
}

.material-composer-empty {
  font-size: var(--text-sm);
  color: var(--text-muted);
  font-style: italic;
  padding: var(--space-12);
  text-align: center;
}

.material-composer-export {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  background: var(--surface-card);
  padding: var(--space-12);
  border-radius: 10px;
  max-height: 200px;
  overflow: auto;
  white-space: pre;
  color: var(--text-secondary);
}

/* .semantic-composer — RETIRED June 2026 (the Semantic Composer merge).
   The standalone foundations.html workspace folded into the Theme
   Composer's "Semantic states" section (theme-editor.js features.semantics);
   per-theme coefficients now live on the theme record (`semantics`) and
   paint via theme-composer-core.js → applyPalette. The --sem-* formula
   system at :root (top of this file) is unchanged. See CLAUDE.md →
   "Theme tokens track the scope". */

/* ════════════════════════════════════════════════════════════════════
   TOOLTIP  (May 2026)
   ────────────────────────────────────────────────────────────────────
   Small floating label that appears on hover or keyboard focus near a
   trigger element. Wrapper-based pattern so the show-state cascades
   via :hover and :focus-within — no JS required. Inspired visually by
   shadcn/Radix Tooltip; LI.FI is vanilla so no provider, no portal.

   Anatomy
     <span class="tooltip-wrap">
       <button class="btn-secondary" aria-describedby="tt-id">…</button>
       <span id="tt-id" class="tooltip" role="tooltip"
             data-tooltip-pos="top">Tooltip content</span>
     </span>

   API
     .tooltip-wrap         — required wrapper, scopes :hover / :focus-within
     .tooltip              — the floating panel
     [data-tooltip-pos]    — top (default) | bottom | left | right
     [data-tooltip-align]  — center (default) | start | end. Controls
                             the cross-axis anchor: for pos="left"|"right"
                             it shifts the tooltip's vertical anchor from
                             the trigger's center to its top (start) or
                             bottom (end); for pos="top"|"bottom" it
                             shifts the horizontal anchor. Reach for end
                             when the tooltip is tall/wide and a centered
                             default would overflow a clipping ancestor
                             (e.g., a panel with overflow: hidden) —
                             anchor at the edge that keeps the tooltip
                             inside. Arrow positioning stays at center on
                             these variants; pair with data-tooltip-arrow="false"
                             unless you're authoring per-variant arrow rules.
     .tooltip--multi       — multi-line variant; drops nowrap, caps width
     [data-tooltip-arrow]  — "false" to hide the arrow (default: shown)

   Theming
     Background uses --text-primary (inverse of page bg) for high
     contrast — flips dark/light automatically. Text reads from
     --surface-page so the foreground always opposes the fill. The arrow
     shares the same fill.

   Accessibility
     - Tooltip element carries role="tooltip" + a unique id.
     - Trigger element references it via aria-describedby="<id>".
     - Show-state is keyboard-reachable (tab the trigger; focus-within
       triggers the panel). Hover-only tooltips are a usability bug —
       use the wrapper so focus AND hover both work.
   ════════════════════════════════════════════════════════════════════ */

.tooltip-wrap {
  position: relative;
  display: inline-flex;
  align-items: center;
}

.tooltip {
  position: absolute;
  z-index: 1000;
  background: var(--text-primary);
  color: var(--surface-page);
  font-family: var(--font-sans);
  font-size: var(--text-caption);
  font-weight: var(--fw-medium);
  line-height: var(--lh-sm);
  padding: var(--space-8) var(--space-12);
  border-radius: 8px;
  box-shadow: 0 var(--space-4) var(--space-12) oklch(0% 0 0 / 0.18);
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
  transition: opacity 120ms ease-out, transform 120ms ease-out, visibility 0s linear 120ms;
  /* Per-axis offset used by the slide-in transform; positions override sign. */
  --tooltip-offset: var(--space-4);
  /* Gap between the tooltip and its trigger. Per-instance overridable via
     inline `style="--tooltip-gap: var(--space-N)"` when a denser primitive
     (e.g., the balance __info button) needs more breathing room so the
     arrow tip doesn't crowd the trigger. */
  --tooltip-gap: var(--space-8);
}

.tooltip-wrap:hover .tooltip,
.tooltip-wrap:focus-within .tooltip {
  opacity: 1;
  visibility: visible;
  transition: opacity 120ms ease-out 200ms, transform 120ms ease-out 200ms, visibility 0s linear 200ms;
}

/* Multi-line variant — wrap text, cap width. The default is a single-line
   label; reach for this when the content is a short explanation. The
   `width: max-content` is critical: an absolutely-positioned element's
   shrink-to-fit otherwise clamps to the containing-block width (the
   wrap, sized to the trigger), so without it `max-width` never engages
   and the tooltip collapses to its longest-word width. */
.tooltip--multi {
  white-space: normal;
  width: max-content;
  max-width: var(--w-1);
}

/* Hide the arrow — opt-in */
.tooltip[data-tooltip-arrow="false"]::before { display: none; }

/* ── Position — default is top ─────────────────────────────────── */
.tooltip,
.tooltip[data-tooltip-pos="top"] {
  bottom: calc(100% + var(--tooltip-gap));
  left: 50%;
  transform: translate(-50%, var(--tooltip-offset));
}
.tooltip-wrap:hover .tooltip,
.tooltip-wrap:focus-within .tooltip,
.tooltip-wrap:hover .tooltip[data-tooltip-pos="top"],
.tooltip-wrap:focus-within .tooltip[data-tooltip-pos="top"] {
  transform: translate(-50%, 0);
}

.tooltip[data-tooltip-pos="bottom"] {
  bottom: auto;
  top: calc(100% + var(--tooltip-gap));
  left: 50%;
  transform: translate(-50%, calc(-1 * var(--tooltip-offset)));
}
.tooltip-wrap:hover .tooltip[data-tooltip-pos="bottom"],
.tooltip-wrap:focus-within .tooltip[data-tooltip-pos="bottom"] {
  transform: translate(-50%, 0);
}

.tooltip[data-tooltip-pos="left"] {
  bottom: auto;
  top: 50%;
  left: auto;
  right: calc(100% + var(--tooltip-gap));
  transform: translate(var(--tooltip-offset), -50%);
}
.tooltip-wrap:hover .tooltip[data-tooltip-pos="left"],
.tooltip-wrap:focus-within .tooltip[data-tooltip-pos="left"] {
  transform: translate(0, -50%);
}

.tooltip[data-tooltip-pos="right"] {
  bottom: auto;
  top: 50%;
  left: calc(100% + var(--tooltip-gap));
  transform: translate(calc(-1 * var(--tooltip-offset)), -50%);
}
.tooltip-wrap:hover .tooltip[data-tooltip-pos="right"],
.tooltip-wrap:focus-within .tooltip[data-tooltip-pos="right"] {
  transform: translate(0, -50%);
}

/* ── Arrow — small rotated square pointing at the trigger ──────── */
.tooltip::before {
  content: '';
  position: absolute;
  width: var(--space-8);
  height: var(--space-8);
  background: inherit;
  transform: rotate(45deg);
}
.tooltip[data-tooltip-pos="top"]::before,
.tooltip:not([data-tooltip-pos])::before {
  bottom: -4px;
  left: 50%;
  margin-left: -4px;
}
.tooltip[data-tooltip-pos="bottom"]::before {
  top: -4px;
  left: 50%;
  margin-left: -4px;
}
.tooltip[data-tooltip-pos="left"]::before {
  right: -4px;
  top: 50%;
  margin-top: -4px;
}
.tooltip[data-tooltip-pos="right"]::before {
  left: -4px;
  top: 50%;
  margin-top: -4px;
}

/* ── Cross-axis alignment — center (default) | start | end ─────
   Override the cross-axis anchor so a tall/wide tooltip doesn't
   need to fit centered on its trigger. Y anchor for pos="left"|
   "right"; X anchor for pos="top"|"bottom". Each rule cancels
   the cross-axis half-translation from the default position rule.

   Arrow visibility: these align variants keep the arrow at the
   tooltip's center (where the default position rules place it),
   which no longer points at the trigger. If the variant is
   user-facing, set data-tooltip-arrow="false" to suppress —
   per-align arrow positioning would 4×2 the CSS for marginal
   visual gain and the slide-in animation already carries the
   trigger→tooltip connection. */

/* pos="left" + align — vertical anchor */
.tooltip[data-tooltip-pos="left"][data-tooltip-align="start"] {
  top: 0;
  bottom: auto;
  transform: translate(var(--tooltip-offset), 0);
}
.tooltip[data-tooltip-pos="left"][data-tooltip-align="end"] {
  top: auto;
  bottom: 0;
  transform: translate(var(--tooltip-offset), 0);
}
.tooltip-wrap:hover .tooltip[data-tooltip-pos="left"][data-tooltip-align="start"],
.tooltip-wrap:focus-within .tooltip[data-tooltip-pos="left"][data-tooltip-align="start"],
.tooltip-wrap:hover .tooltip[data-tooltip-pos="left"][data-tooltip-align="end"],
.tooltip-wrap:focus-within .tooltip[data-tooltip-pos="left"][data-tooltip-align="end"] {
  transform: translate(0, 0);
}

/* pos="right" + align — vertical anchor */
.tooltip[data-tooltip-pos="right"][data-tooltip-align="start"] {
  top: 0;
  bottom: auto;
  transform: translate(calc(-1 * var(--tooltip-offset)), 0);
}
.tooltip[data-tooltip-pos="right"][data-tooltip-align="end"] {
  top: auto;
  bottom: 0;
  transform: translate(calc(-1 * var(--tooltip-offset)), 0);
}
.tooltip-wrap:hover .tooltip[data-tooltip-pos="right"][data-tooltip-align="start"],
.tooltip-wrap:focus-within .tooltip[data-tooltip-pos="right"][data-tooltip-align="start"],
.tooltip-wrap:hover .tooltip[data-tooltip-pos="right"][data-tooltip-align="end"],
.tooltip-wrap:focus-within .tooltip[data-tooltip-pos="right"][data-tooltip-align="end"] {
  transform: translate(0, 0);
}

/* pos="top" + align — horizontal anchor */
.tooltip[data-tooltip-pos="top"][data-tooltip-align="start"],
.tooltip:not([data-tooltip-pos])[data-tooltip-align="start"] {
  left: 0;
  right: auto;
  transform: translate(0, var(--tooltip-offset));
}
.tooltip[data-tooltip-pos="top"][data-tooltip-align="end"],
.tooltip:not([data-tooltip-pos])[data-tooltip-align="end"] {
  left: auto;
  right: 0;
  transform: translate(0, var(--tooltip-offset));
}
.tooltip-wrap:hover .tooltip[data-tooltip-pos="top"][data-tooltip-align="start"],
.tooltip-wrap:focus-within .tooltip[data-tooltip-pos="top"][data-tooltip-align="start"],
.tooltip-wrap:hover .tooltip[data-tooltip-pos="top"][data-tooltip-align="end"],
.tooltip-wrap:focus-within .tooltip[data-tooltip-pos="top"][data-tooltip-align="end"],
.tooltip-wrap:hover .tooltip:not([data-tooltip-pos])[data-tooltip-align="start"],
.tooltip-wrap:focus-within .tooltip:not([data-tooltip-pos])[data-tooltip-align="start"],
.tooltip-wrap:hover .tooltip:not([data-tooltip-pos])[data-tooltip-align="end"],
.tooltip-wrap:focus-within .tooltip:not([data-tooltip-pos])[data-tooltip-align="end"] {
  transform: translate(0, 0);
}

/* pos="bottom" + align — horizontal anchor */
.tooltip[data-tooltip-pos="bottom"][data-tooltip-align="start"] {
  left: 0;
  right: auto;
  transform: translate(0, calc(-1 * var(--tooltip-offset)));
}
.tooltip[data-tooltip-pos="bottom"][data-tooltip-align="end"] {
  left: auto;
  right: 0;
  transform: translate(0, calc(-1 * var(--tooltip-offset)));
}
.tooltip-wrap:hover .tooltip[data-tooltip-pos="bottom"][data-tooltip-align="start"],
.tooltip-wrap:focus-within .tooltip[data-tooltip-pos="bottom"][data-tooltip-align="start"],
.tooltip-wrap:hover .tooltip[data-tooltip-pos="bottom"][data-tooltip-align="end"],
.tooltip-wrap:focus-within .tooltip[data-tooltip-pos="bottom"][data-tooltip-align="end"] {
  transform: translate(0, 0);
}


/* ════════════════════════════════════════════════════════════════
   Product-surface (.ui-*) primitives — promoted from swap.css
   ════════════════════════════════════════════════════════════════
   Tier-3 product-surface primitives that aren't swap-specific —
   they belong to the wider "widget canvas" vocabulary that future
   widgets (lend, payments, agentic) will all consume. Promoted
   from swap.css May 2026 once the first cross-widget primitive
   appeared. Same naming tier (.ui-*) is kept — these stay
   product-surface only, never used on marketing or DS-meta surfaces.

   What lives here vs swap.css:
     here       → cross-widget vocabulary (screen chrome,
                  layout shells that any widget needs)
     swap.css   → swap-specific primitives (.ui-card,
                  .ui-token-select, .ui-amount-field, .ui-swap-arrow)
                  — promote a swap.css primitive when a second
                  widget needs it.
*/

/* ── .screen-header — universal screen-surface chrome ─────────────
   The chrome line at the top of any screen-shaped surface: widget
   screens, modal dialogs, drawers, bottom sheets, full-page flows.
   Optional back/close button, title (with optional subtitle),
   optional trailing actions cluster. Every slot is OPTIONAL —
   author includes only what the screen needs; CSS adapts the grid
   via :has() so markup intent matches visual layout.

   Promoted from `.ui-screen-header` to bare `.screen-header` in
   May 2026 v13 alongside the .modal shell rebuild — modals appear
   on marketing AND product, so the header inside them can't stay
   product-tier. The two-tier navigation contract below carries
   over verbatim from the product-context original.

   Anatomy:
     <header class="screen-header">
       <button class="screen-header__back" aria-label="Back">
         <svg>…back arrow…</svg>
       </button>
       <h1 class="screen-header__title">Exchange</h1>
       <div class="screen-header__trailing">
         <button class="btn-icon btn-sm">…</button>
       </div>
     </header>

   Variants on the header (title treatment only — slot presence
   stays markup-driven):
     (default)                    Sub-screen — title centered, h3, has back
     .screen-header--main         Main screen — title left, h2, NO back

   Modifier on the __back slot:
     .screen-header__back--close  Close action — same shape, X icon,
                                  danger-tinted hover. For widgets /
                                  modals that dismiss outright.

   Layout matrix (driven by which slots are in markup):
     [back] [title] [trailing]     → 3-col  24px / 1fr / auto
     [title] [trailing]             → 2-col  1fr / auto    (main screen)
     [back] [title]                 → 3-col  24px / 1fr / 24px
                                       (phantom right balances back —
                                       keeps centered title geometrically
                                       centered between the two reserves)
     [title]                        → 1-col  1fr           (plain)

   Navigation contract (two-tier, deliberate simplification — applies
   wherever a screen-header lives, including modals):
     - The host surface has ONE Main screen + zero-or-more Sub-screens.
       For a single-screen modal, only Main is present — use
       .screen-header--main with the close affordance.
     - Sub-screens are reached from the Main; there are NO nested
       sub-sub-screens. If a future flow needs depth, it gets a modal
       wrapper or a separate widget — not a third tier.
     - .screen-header__back ALWAYS returns to the Main screen,
       regardless of which Sub the user is on. JS handlers should
       wire this as popToRoot(), not popOne(). With two-tier these
       are equivalent today — the rule clarifies that even if the
       UI grows, the back arrow doesn't accumulate stack semantics.
     - .screen-header__back--close is a different action — it
       dismisses the surface entirely (closes the modal, hides the
       drawer). Don't conflate.
*/
.screen-header {
  display: grid;
  grid-template-columns: var(--space-40) 1fr auto;
  align-items: center;
  gap: var(--space-12);
  min-height: var(--space-40);
}

/* Markup-driven grid via :has(). All four combos covered.

   Title-centering contract (added v6 — May 2026):
     The title sits in the 1fr middle column. For it to read as
     visually centered, the LEFT and RIGHT columns must be the same
     width. The base rule above declares `var(--space-40) 1fr auto`
     — back column 40 px on the left, trailing column `auto` on the
     right (which collapses to 0 px when trailing is empty or absent).
     That asymmetry makes the title sit 20 px right of true center.

     Fix: the `:has(.screen-header__back)` rule below sets the right
     column to `minmax(var(--space-40), auto)`, so the right column
     ALWAYS reserves at least 40 px (matching the back column)
     regardless of whether the trailing slot is present, empty, or
     absent. When trailing has content the column grows past 40 px
     naturally — the title shifts left as the right grows, which is
     correct because heavy trailing visually balances the light back
     button asymmetrically.

     This supersedes the v5 implementation, which used a `::after`
     phantom cell on `:has(back):not(:has(trailing))`. That selector
     was defeated by an empty trailing div (`<div class="…__trailing"
     ></div>` left in the DOM for JS hydration or symmetric authoring)
     because `:has(trailing)` matched even when the div had no content.
     The minmax pattern doesn't care about emptiness — empty trailing
     column still respects the 40 px min.

   May 2026 v5: back-slot column widened from 32 → 40 px so the
   .btn-icon.btn-borderless.btn-circle (now at default 40×40
   size — drop .btn-sm) fits cleanly. Earlier history: column was
   24 → 32 in v1 when the back-slot composition moved from custom
   chrome to .btn-icon.btn-sm.btn-neutral; v5 bumps to 40
   alongside the title-size + scopebar size refresh. */
.screen-header:has(.screen-header__back) {
  grid-template-columns: var(--space-40) 1fr minmax(var(--space-40), auto);
}
.screen-header:not(:has(.screen-header__back)):has(.screen-header__trailing) {
  grid-template-columns: 1fr auto;
}
.screen-header:not(:has(.screen-header__back)):not(:has(.screen-header__trailing)) {
  grid-template-columns: 1fr;
}

/* ── __avatar — large leading identity avatar (profile / org-header lead) ──
   An alternative lead to __back: a large brand / identity avatar to the LEFT of
   the title, for profile / organization / settings headers. The slot is any
   .avatar (a logo <img> or a monogram); this class only sizes it large
   (xl / 64 by default — swap --avatar-size for lg 48 or 2xl 96) and marks the
   slot so the grid reserves an `auto` lead column. Unlike __back (which centres
   the title, modal-style), the avatar lead LEFT-aligns the title.
   `:not(:has(__back))` keeps this at (0,3,0) — above the trailing-column rules —
   so an avatar + trailing header still reserves the lead column. Catalog:
   #screen-header-avatar. */
.screen-header:has(.screen-header__avatar):not(:has(.screen-header__back)) {
  grid-template-columns: auto 1fr auto;
}
.screen-header__avatar { --avatar-size: var(--space-64); }
.screen-header:has(.screen-header__avatar) .screen-header__title { text-align: left; }

/* ── __back — back / close lead-action slot ─────────────────────────
   May 2026 v5: composes the canonical icon-button family at the default
   (40×40) tier — drop .btn-sm:
     <button class="screen-header__back btn-icon btn-borderless btn-circle">
   `.screen-header__back` is kept ONLY as a structural marker so the
   parent grid's `:has()` rules can detect the slot and adapt the
   columns. All visual styling — 40×40 size, transparent idle / hover
   fill, focus ring — comes from `.btn-icon.btn-borderless.btn-circle`.
   The SVG inside is a Lucide library icon (arrow-left for back,
   x for close). Don't add visual properties to this class.

   The default switched from .btn-neutral to .btn-borderless.btn-circle in
   May 2026 to match the borderless-seg idiom — when a screen-header
   carries a `.seg--borderless` mode picker in its title slot, the
   back button's old alpha chip chrome competed with the borderless
   tabs. The borderless icon button reads as part of the same minimal
   tier — bg only on hover, full circle. */

/* Close-action variant — destructive hover affordance. Author swaps
   the SVG to an X (Lucide x.svg) and adds the variant class. Hover
   tints with --danger so the destructive intent is felt before click. */
.screen-header__back--close:hover {
  background: color-mix(in oklch, var(--danger) 10%, transparent);
  color: var(--danger);
}

/* ── __title / __subtitle — title group ─────────────────────────── */
.screen-header__title {
  /* Tag convention: consumers author this as <h2> at EVERY tier (the host page
     owns the <h1>; the screen-header title is a section heading). Size is
     class-driven below, so the heading level stays uniform regardless of
     --compact. See surfaces.md → "Composition contract". */
  text-align: center;
  /* May 2026 v6: stepped down one tier to --text-body-xl (20 px).
     Full trail: 32 → 24 (v1) → 20 (v2) → 18 (v3) → 20 (v4) → 24 (v5) →
     20 (v6).

     RATIONALE — visual pairing, not a chrome-size-driven match.
     The chrome (back + trailing icon buttons + .seg--borderless mode
     picker in the title slot) sits at the default 40 px tier. At
     v5 the title was h3 (24 px), which read as competing with the
     40 px borderless icon buttons — the title felt heavier than the
     surrounding actions. body-xl (20 px) is the visual sweet spot:
     it stays clearly headline-tier (vs body 16 px) while reading
     lighter than the chrome's 40 px stroke-width-2 SVG icons. The
     pairing is a *proportional* judgment, NOT a line-height match
     to chrome height — line-height stays at 32 px via --lh-body-xl
     (ratio 1.6 — the "oversized lede" pair), and the chrome height
     (40 px) drives the screen-header row height via the buttons,
     not the title's line-box. */
  font-size: var(--text-body-xl);
  line-height: var(--lh-body-xl);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-snug);
  color: var(--text-primary);
  margin: 0;
}

/* ── __titles + __subtitle — stacked title cell ───────────────────
   The header is a grid with __title in the 1fr middle column. To stack a
   supporting line under the title, wrap both in .screen-header__titles so
   they occupy that one cell:
     <div class="screen-header__titles">
       <h2 class="screen-header__title">…</h2>
       <p  class="screen-header__subtitle">…</p>
     </div>
   __subtitle is caption-tier muted text — the supporting line that the
   Portal's retired .portal-panel__sub maps onto. Honors the default
   centered alignment AND the --main left-aligned variant (the wrapper is a
   flex column; alignment follows the title's text-align via inheritance). */
.screen-header__titles {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
.screen-header__subtitle {
  margin: var(--space-2) 0 0;
  font-size: var(--text-caption);
  line-height: var(--lh-caption);
  color: var(--text-muted);
}

/* ── __trailing — actions cluster ─────────────────────────────────
   Flush layout (gap: 0). Trail of reductions: 8 → 4 (v1, to fit
   three+ icon buttons next to a .seg--borderless on the 416 px
   widget header) → 0 (v2, after the seg's intrinsic width pushed
   the third button past the panel's right edge on the Main + seg +
   3-icon combination — seg 237 + column-gap 12 + trailing 128 =
   377 on a 374 px header). The 40 px buttons keep their full tap
   target; the visible glyph inside each is ~20 px, so two adjacent
   icons still have ~20 px between glyph centers — flush boxes, not
   crammed glyphs. Hover paints on one button at a time (borderless
   tier — bg only on hover), so the touched boxes never both light
   up. If a fourth trailing button ever lands here, the budget runs
   out again — at that point the right fix is structural (compact
   icon tier in this context, or an overflow menu), not a smaller
   gap. */
.screen-header__trailing {
  justify-self: end;
  display: inline-flex;
  align-items: center;
  gap: 0;
}

/* ── --main variant — main / entry screen ──────────────────────────
   The widget's entry point (no back button). The default centered
   variant is the sub-screen tier; this modifier flips alignment to
   left so the title leads the screen.

   May 2026 v6: title at --text-body-xl (20 px) in both centered
   default AND this main variant — stepped down one tier from v5 h3
   (24 px) as a visual-pairing optimization with the 40 px borderless
   icon buttons in the same chrome row. See the .screen-header__title
   rule above for the full rationale. The shape of the variant is the
   left-alignment, not a typography difference. Trail: 32 → 24 (v1)
   → 20 (v2) → 18 (v3) → 20 (v4) → 24 (v5) → 20 (v6).

   Renamed v12 from .screen-header--form (a visual descriptor) to
   reflect the navigation role: Main vs. Sub. See the anatomy comment
   block above for the full two-tier navigation contract. */
.screen-header--main .screen-header__title {
  text-align: left;
}

/* ── --compact variant — narrow glass popovers ──────────────────────
   The 32 px chrome tier for FAB sub-panels, theme composer menus,
   and any narrow-column popover head where the default 40 px row
   would crowd a 280–340 px container. Same gestalt as the default
   tier — back button slot · title slot · trailing slot, auto-
   centering via :has() — only the size axes shrink.

   May 2026 v18 — promoted from the standalone `.panel-head`
   primitive into the `.screen-header` family as a size modifier.
   The Sizing Ladders rule (CLAUDE.md → Component Sizing Ladders)
   says every multi-size component reaches for one canonical name
   + size modifier; two name-distinct primitives doing the same
   role contradicts that. Retired `.panel-head` in the same
   commit; consumers (FAB sub-panel JS in shared.js, palette /
   material / semantic composer JS, Edit-theme rail sub-screen,
   hero Globe panel, brand-guide Logo Stack, lab.html demos)
   all migrated together.

   The old 34 px chrome snapped to the 4 px grid as part of the
   migration — 32 px sides + 32 px min-height. .btn-sm composed
   in the back slot fits the 32 px column exactly. */
.screen-header--compact {
  grid-template-columns: var(--space-32) 1fr var(--space-32);
  gap: var(--space-8);
  min-height: var(--space-32);
}
.screen-header--compact:has(.screen-header__back) {
  grid-template-columns: var(--space-32) 1fr minmax(var(--space-32), auto);
}
.screen-header--compact .screen-header__title {
  font-size: var(--text-body);
  line-height: var(--lh-body);
  letter-spacing: var(--tracking-normal);
}

/* ── --sticky-glass variant — frosted-sticky surface ────────────────
   For scrollable panel bodies (FAB sub-panels) where the head pins
   above scrolling content. The parent panel already carries the
   bg-glass fill AND a backdrop-filter, so the header stays fully
   transparent (no background, no own filter) and reads as one
   continuous surface with the body. When content scrolls, the
   1 px border-bottom + the dark panel surface behind keep the
   title legible — re-blurring inside the header would only stack
   another filter on top of the panel's, creating a visible step.

   May 2026 v18 — migrated from `.panel-head--sticky-glass`. Same
   recipe, new canonical name. Pair with `--compact` for the FAB
   sub-panel chrome; the two modifiers are orthogonal axes (size
   vs. surface treatment). */
.screen-header--sticky-glass {
  position: sticky;
  top: -20px;
  z-index: 20;
  background: transparent;
  margin: calc(-1 * var(--space-20)) calc(-1 * var(--space-20)) var(--space-12);
  padding: var(--space-20) var(--space-20) var(--space-12);
  border-radius: var(--space-16) var(--space-16) 0 0;
  border-bottom: 1px solid var(--border-subtle);
}

/* ── --quiet variant — small uppercase mono-feel title ──────────────
   Matches the legacy theme-composer aesthetic — the title reads
   as a tag / scope caption rather than a page title. Use when the
   head sits above a narrow controls cluster and the title is the
   tool's identifier (e.g., a "Material Composer" popover). The Theme
   Composer itself is the DEFAULT tier (a 480px panel), not --quiet.

   May 2026 v18 — migrated from `.panel-head--quiet`. */
.screen-header--quiet .screen-header__title {
  font-size: var(--text-sm);
  letter-spacing: var(--tracking-normal);
  text-transform: uppercase;
  color: var(--text-secondary);
}

/* ── Title-slot seg auto-hug ─────────────────────────────────────
   Auto-applies the .seg--hug behavior whenever a seg sits inside a
   screen-header — the canonical pattern is a Main-variant mode
   picker (Swap / Bridge / Limit) where filling the 1fr title column
   would create awkwardly wide tabs. The same recipe lives on
   `.seg--hug` for non-screen-header consumers; both rules are kept
   in sync. See `.seg--hug` in the seg section above for the
   modifier's public API + rationale. */
.screen-header .seg {
  justify-self: start;
}
.screen-header .seg-item {
  flex: 0 1 auto;
}


/* ════════════════════════════════════════════════════════════════
   .chip-shortcuts — preset-value picker for amount fields
   ════════════════════════════════════════════════════════════════
   Wraps a row of .chip children that act as one-tap shortcuts for
   setting a numeric amount field — 25 / 50 / 75 / MAX in the swap
   widget's amount card, slippage presets in settings, fee tiers
   anywhere a finite set of preset values speeds up entry.

   Universal tier-2 primitive (no prefix) — usable on any surface
   that has a paired numeric input. Promoted out of swap.css May 2026
   once the design intent surfaced for cross-surface reuse.

   Composition over invention. The wrapper owns:
     · cluster layout — flex row, nowrap, auto-adapting gap
     · optional reveal-on-hover animation modifier
     · group semantics (role + aria-label)
   Each chip child stays a canonical .chip with whatever tone +
   size class fits the context. No parallel "amount-chip" primitive,
   no styling fork.

   Anatomy:
     <div class="chip-shortcuts" role="group" aria-label="…">
       <button type="button" class="chip chip-neutral">25%</button>
       <button type="button" class="chip chip-neutral">50%</button>
       <button type="button" class="chip chip-neutral">75%</button>
       <button type="button" class="chip chip-neutral">MAX</button>
     </div>

   Sizes come from .chip's existing ladder — put the size class on
   each child, not on the wrapper:
     .chip               → default ~24 px (today's swap-widget look)
     .chip .chip--md     → ~32 px (medium amount cards)
     .chip .chip--lg     → ~40 px (hero amount inputs)
     .chip .chip--xl     → ~48 px (banner / print)

   The wrapper's gap auto-adapts to the largest chip tier via :has(),
   so adjacent chips always carry the right breathing room.

   Variants:
     (default)                          chips always visible — the
                                         safe baseline (keyboard /
                                         touch / printed contexts
                                         that don't have hover)
     .chip-shortcuts--reveal-on-hover staggered fade-in from the
                                         right when the cluster (or
                                         any direct ancestor) is
                                         hovered or focus-within.
                                         Up to 7 children staggered
                                         at 30 ms intervals; past 7,
                                         the last delay applies.

   Availability gate — `hidden` attribute on the wrapper:
     The cluster is a SHORTCUT for picking a percentage of an existing
     balance. If there's no balance (wallet disconnected, balance = 0,
     selected token unavailable on the wallet's chains), the chips
     have nothing to act on — show them and the user gets a row of
     dead buttons. The consumer toggles the native `hidden` attribute
     on the wrapper to remove the cluster from layout + the a11y tree
     when its preconditions aren't met. The override rule below
     defeats the `display: inline-flex` declared above (specificity
     class + attr beats class alone) so `hidden` actually paints.
     See swap.js → renderAmountsFromSim for the canonical gate logic
     (wallet-connected && balance > 0).
*/
.chip-shortcuts {
  display: inline-flex;
  align-items: center;
  gap: var(--space-4);
  flex-wrap: nowrap;
}
.chip-shortcuts[hidden] { display: none; }

/* Auto-adapt gap to the largest chip size inside.
   :has() detects the highest-tier chip among direct children and
   bumps the cluster gap accordingly so the visual rhythm stays
   right at every chip tier. */
.chip-shortcuts:has(> .chip--md) { gap: var(--space-8); }
.chip-shortcuts:has(> .chip--lg) { gap: var(--space-8); }
.chip-shortcuts:has(> .chip--xl) { gap: var(--space-12); }

/* ── --reveal-on-hover — opt-in staggered fade-in ──────────────────
   Chips begin at opacity 0 + slight rightward translate; on hover /
   focus-within of the cluster OR any direct ancestor, they animate
   into place. The rightmost child reveals first, with each
   preceding child delayed +30 ms.

   Trigger contracts (both work — pick whichever matches the host):
     (a) self-trigger — hovering the cluster itself reveals chips.
         Useful when the cluster sits in a non-hoverable parent.
     (b) parent-trigger via :has() — hovering ANY direct parent
         (.ui-amount-card__header, an input wrapper, etc.) reveals
         the chips inside. The canonical pattern for the swap
         widget's amount card.

   Both triggers fire :focus-within too — keyboard users tab into
   a chip, the cluster reveals, the user can Enter. pointer-events:
   none on hidden chips blocks mouse clicks on invisible targets
   but doesn't break keyboard focus.

   Reduced-motion users get a flat fade with no translate. */
.chip-shortcuts--reveal-on-hover > .chip {
  opacity: 0;
  transform: translateX(var(--space-8));
  pointer-events: none;
  transition: opacity 160ms ease, transform 160ms ease;
}
.chip-shortcuts--reveal-on-hover:hover > .chip,
.chip-shortcuts--reveal-on-hover:focus-within > .chip,
*:has(> .chip-shortcuts--reveal-on-hover):hover > .chip-shortcuts--reveal-on-hover > .chip,
*:has(> .chip-shortcuts--reveal-on-hover):focus-within > .chip-shortcuts--reveal-on-hover > .chip {
  opacity: 1;
  transform: translateX(0);
  pointer-events: auto;
}

/* Stagger — rightmost first. :nth-last-child counts from the end,
   so the last (rightmost) child gets delay 0 and earlier children
   step backwards by +30 ms each. The wrapping selector on each
   line keeps the delay tied to the reveal modifier. */
.chip-shortcuts--reveal-on-hover > .chip:nth-last-child(1) { transition-delay: 0ms; }
.chip-shortcuts--reveal-on-hover > .chip:nth-last-child(2) { transition-delay: 30ms; }
.chip-shortcuts--reveal-on-hover > .chip:nth-last-child(3) { transition-delay: 60ms; }
.chip-shortcuts--reveal-on-hover > .chip:nth-last-child(4) { transition-delay: 90ms; }
.chip-shortcuts--reveal-on-hover > .chip:nth-last-child(5) { transition-delay: 120ms; }
.chip-shortcuts--reveal-on-hover > .chip:nth-last-child(6) { transition-delay: 150ms; }
.chip-shortcuts--reveal-on-hover > .chip:nth-last-child(n+7) { transition-delay: 180ms; }

@media (prefers-reduced-motion: reduce) {
  .chip-shortcuts--reveal-on-hover > .chip {
    transform: none !important;
    transition: opacity 80ms ease !important;
    transition-delay: 0ms !important;
  }
}
