
/* colors_and_type.css */
/* ============================================================
   247420 design system — colors + type
   shadcn-neutral (2026-07-30 restyle): confident display type, ink-on-paper
   with a grayscale accent, print texture inert. RESTYLED from the prior
   "Acid Editorial" warm-paper/electric-lime identity -- see the RESTYLED
   notes below for the token-by-token detail. Scope-prefixed at build time.
   Source-of-truth tokens. Component sheet lives in app-shell.css.
   ============================================================ */

/* System-font stack only — no web-font @import/@font-face. Every --ff-*
   token below is the same fallback chain that used to sit after the custom
   family names; removing the custom names just exposes it directly. */

.ds-247420 {
  /* Tree view indentation tokens */
  --tree-indent: 12px;
  --tree-base-indent: 6px;

  /* Paper / Ink — RESTYLED (2026-08b) back to true neutral grayscale,
     replacing the brief "warm editorial, amber lead" identity (2026-08a,
     warm cream/brown/amber) which itself replaced the 2026-07-30
     shadcn-neutral grayscale identity. User rejected warm-brown outright
     ("brown is lame") and asked for a pure neutral base so the brand duo
     below is the only color in the system. Every downstream color-mix()/
     var() derivation in this file keeps its formula; only these ROOT values
     change, cascading through all 32 consumer files automatically.
     Contrast re-verified by hand (WCAG relative-luminance formula): --ink
     17.40:1, --ink-2 12.63:1, --ink-3 7.00:1 on --paper (all far clear of AA
     4.5:1); --ink-3 on --paper-2/--paper-3 also re-measured at 6.42:1 /
     5.56:1, both still clear. */
  --paper:       #FFFFFF;
  --paper-2:     #F5F5F5;
  --paper-3:     #E5E5E5;
  --ink:         #1A1A1A;
  --ink-2:       #333333;
  --ink-3:       #595959; /* re-verified 2026-08b neutral restyle: 7.00:1 on --paper, 6.42:1 on --paper-2, 5.56:1 on --paper-3 -- all clear of AA 4.5:1 with margin */
  --ink-3-dark:  #4D4D4D; /* dark theme bg-3 (tier-3 surface), neutral equivalent */
  --paper-3-dark: #BFBFBF; /* dark theme fg-3 (tier-3 text); re-verified against --ink-3-dark/--ink-2/--ink below */

  /* Lore palette. --acid held the shadcn-neutral near-black accent
     (2026-07-30), then a warm amber lead (2026-08a) -- RESTYLED again
     (2026-08b) to the project's own brand codename read as a color: #247420
     (green). --purple below is retuned to #420247, the codename's digit
     reversal, as the deliberate secondary lead -- the two together ARE the
     new identity, not an arbitrary accent choice. mascot/signals stay as
     the category/status palette, untouched by this restyle. */
  --acid:        #247420; /* accent FILL -- 5.84:1 against --paper via --accent-fg (white) on top */
  --acid-deep:   #133F10; /* accent TEXT tone on --paper -- 12.03:1 */
  --green:       #247420;
  --green-2:     #3A9A34;
  --green-deep:  #133F10;
  --green-tint:  #D7E4C2;
  --purple:      #420247; /* the brand codename's digit reversal, the identity's secondary lead -- 15.77:1 on --paper */
  --purple-2:    #7F18A4;
  --purple-deep: #220026;
  --purple-tint: #E2CFE4;
  --mascot:      #FF2E84;
  --mascot-2:    #FF3CA7;
  --mascot-tint: #F7CEDD;
  --mascot-deep: #B81F63;
  /* Fixed ink-surface mascot text tone (6.67:1 on --ink). Unlike
     --mascot-deep, this NEVER follows the theme: it is for always-dark
     surfaces like the .cli card, which stay ink under a light theme and so
     need a tone chosen for ink rather than for the active theme. Same role
     --paper-3-dark plays for the tier-3 text ramp. */
  --mascot-on-ink: #FF5C9E;
  /* Same always-dark-surface pattern, for .ds-cli-ok's success/OK text.
     RESTYLED 2026: .ds-cli-ok previously used --acid directly (the bright
     lime), which cleared contrast on --ink by construction; now that --acid
     is a dark neutral, it fails 1.15:1 on the still-always-dark .cli card
     (npm run a11y caught this on the terminal kit). Reuses the dark-theme
     --green retune value (7.48:1 on --ink) rather than inventing a new hue,
     since --green already carries the "success" semantic elsewhere. */
  --success-on-ink: #5CBF52;

  /* Signals */
  --sun:         #F5C344;
  /* Error/status tones are theme-tuned: the root values are the LIGHT (paper)
     pair (flame 4.74:1, amber 4.5+:1 on paper); the dark blocks below restore
     the originals (#FF5A1F / #D9A93A) which were tuned on ink and fail on
     paper (2.9:1 / 2.0:1). */
  --flame:       #C53E00;
  /* #8A6512 measured 4.5+:1 on --paper but only 4.26:1 on --bg-2, which the
     dashboard card status text actually renders on - darkened for margin on
     both backgrounds (5.21:1 on --bg-2). */
  --amber:       #7C570F;
  --sky:         #3A6EFF;
  /* --warn = interactive danger controls (buttons, banners, menus, error
     inputs); --danger below = passive error/status text and metrics
     (kpi/sparkline deltas, auth-error text). Two tokens, one convention.
     #E0241A measured 4.19:1 on --paper — fails WCAG AA 4.5:1 for the many
     TEXT usages (.field-error, .ds-btn-warn, .ds-stat-val.err-rate, toast/
     session error labels), not just the interactive-control (>=3:1) usages.
     Darkened to clear AA on --paper (5.29:1), --paper-2 (4.78:1), and the
     thebird warm-paper preset (5.25:1) while staying visually a clear
     warm red. */
  --warn:        #C41C0C;
  /* Paired foreground for --warn used as a FILL (a danger button background),
     not as text-on-paper (--warn's own tuning above). --paper clears 5.97:1
     on this --warn -- --warn was tuned dark specifically for the text-on-paper
     direction, so re-using it for the opposite direction (paper text on this
     as a fill) needs its own token, not a re-derivation of --warn itself. */
  --warn-fg:     var(--paper);
  /* Syntax-token aliases: light values pass 4.5:1 on paper; the dark blocks
     restore the bright dark-tuned originals. Components reference ONLY these. */
  --code-string:  var(--green);
  --code-keyword: #2453C9;
  --code-fn:      #B84300;
  --code-str-alt: var(--mascot-deep);
  --code-num:     #946300;
  /* 0.55 lightness measured 4.70:1 on --paper but only 4.25:1 on --bg-2 (the
     auth-error banner's actual background) - darkened for AA margin on both.
     --danger = passive error/status text and metrics; --warn above =
     interactive danger controls. */
  --danger:      oklch(0.50 0.18 25);
  --success:     var(--green-2);
  --live:        var(--green);

  /* Semantic surfaces */
  --bg:          var(--paper);
  --bg-2:        var(--paper-2);
  --bg-3:        var(--paper-3);
  --fg:          var(--ink);
  --fg-2:        var(--ink-2);
  --fg-3:        var(--ink-3);

  /* --accent is the FILL; --accent-ink is the readable TEXT tone for
     links/eyebrows/accents on paper; --accent-fg is what sits ON an --accent
     fill. RESTYLED: --accent (formerly the bright --acid lime, dark text on
     top) is now a dark neutral (webjsx-toolkit's --primary: 0 0% 15%), so
     --accent-fg flips to a LIGHT tone (webjsx-toolkit's --primary-foreground:
     0 0% 100%) -- verified via npm run a11y after the initial restyle caught
     this exact dark-on-dark 1.15:1 failure across every kit's .btn-primary/
     .active nav item, both of which read --accent-fg as their text color. */
  --accent:      var(--acid);
  --accent-fg:   var(--paper);
  --accent-ink:  var(--acid-deep);
  --accent-bright: var(--acid); /* base value so var(--accent-bright) never falls back empty; ink + [data-accent] retune it */
  /* Blends --accent-ink (the readable/darker ink variant), not the raw
     --accent fill, so light-theme active states clear contrast against --bg;
     dark/ink and auto-dark restate the prior --accent-based value explicitly
     below so this change does not leak into them via inheritance. */
  --accent-tint: color-mix(in oklab, var(--accent-ink) 22%, var(--bg));

  /* On-color foregrounds — text/icons that sit ON a saturated fill (accent,
     danger, category chips). Theme-driven so white-on-color never hard-codes
     #fff. --on-accent tracks --accent-fg; --on-color is the generic light
     foreground for any colored fill. */
  --on-accent:   var(--accent-fg);
  --on-color:    #fff;

  /* Scrims — modal/overlay backdrops. Derived from --fg so a dark theme dims
     with its own ink rather than a baked rgba(0,0,0). */
  --scrim:        color-mix(in oklab, var(--fg) 40%, transparent);
  --scrim-strong: color-mix(in oklab, var(--fg) 72%, transparent);
  --scrim-media:  color-mix(in oklab, var(--fg) 85%, transparent);

  /* Category palette — surfaces (forum tags, project cards) that color-code by
     category. Canonical so a theme/accent swap recolors categories instead of
     drifting from a private --cat-* block. Derived from the base palette. */
  --cat-green:   var(--green-2);
  --cat-purple:  var(--purple-2);
  --cat-mascot:  var(--mascot);
  --cat-sun:     var(--sun);
  --cat-flame:   var(--flame);
  --cat-sky:     var(--sky);

  /* Readable TEXT tones for the category hues — the same FILL/TEXT split the
     lead accent already draws with --accent (fill) vs --accent-ink (text).
     The bare --cat-* values are chroma-matched to sit behind ink on a swatch,
     avatar, or tag; as `color:` on newsprint paper they land at ~3.1:1, under
     the 4.5:1 floor. Any `color:` use of a category hue takes the -ink tone;
     only background/border-color take the bare one. Each -deep source is
     already remapped per-theme below, so these follow the theme too. */
  --cat-green-ink:  var(--green-deep);
  --cat-purple-ink: var(--purple-deep);
  --cat-mascot-ink: var(--mascot-deep);

  --panel-bg:     var(--bg);
  --panel-bg-2:   var(--bg-2);
  --panel-accent: var(--accent);
  --panel-0:      var(--bg);
  --panel-1:      var(--bg-2);
  --panel-2:      var(--bg-3);
  --panel-hover:  color-mix(in oklab, var(--fg) 4%, var(--panel-1));
  --panel-3:      color-mix(in oklab, var(--fg) 12%, transparent);
  --panel-text:   var(--fg);
  --panel-text-2: var(--fg-2);
  --panel-text-3: var(--fg-3);
  --panel-accent-2: var(--accent-bright, var(--accent));
  --panel-shadow: 0 1px 0 color-mix(in oklab, var(--fg) 6%, transparent), 0 4px 14px color-mix(in oklab, var(--fg) 8%, transparent);
  /* Single-layer, half-strength companion to --shadow-1, used ONLY by .panel.
     .panel already carries a tonal --panel-1 fill and a --rule border; a
     full --shadow-1 on top made three simultaneous edge/surface signals
     read as boxed/cardy rather than PilotDeck's flat, near-borderless look.
     --shadow-1/2/3 stay untouched — they are the real elevation scale for
     things that must read as floating (dialog/dropdown/tooltip/toast). */
  --panel-shadow-1: 0 1px 2px color-mix(in oklab, var(--fg) 4%, transparent);

  /* Type — system-font stack, no custom web fonts loaded (README's explicit
     no-web-font principle: zero network font requests, zero FOUT risk).
     RESTYLED 2026-08b: --ff-display no longer forks to a serif stack (that
     was the prior warm-editorial identity's one deliberate exception,
     explicitly rejected by the user -- "we dont like any fonts, and we dont
     like any serifs"). One system sans-serif stack for every text role;
     headings differentiate from body purely by the fs/fw scale tokens and
     weight, never by font-family. */
  --ff-display:  system-ui, sans-serif;
  --ff-narrow:   system-ui, sans-serif;
  --ff-body:     system-ui, sans-serif;
  --ff-mono:     ui-monospace, Menlo, Consolas, monospace;

  /* Weight scale — regular is the default voice for body/UI text everywhere;
     medium/semibold/bold are reserved for genuine emphasis (active state,
     unread counts, own-message author, section headers). Never reach for a
     hardcoded 600/700 in a component rule — use these so the whole surface
     can be re-tuned in one place instead of drifting per-component. */
  --fw-regular:  400;
  --fw-medium:   500;
  --fw-semibold: 600;
  --fw-bold:     700;

  /* Sub-12px tiers exist ONLY for dense OS/data chrome — taskbar strips,
     session chips, tool-call headers, file-row metadata, ID/stat columns.
     Never use them for body copy or ordinary UI text: at these sizes the
     system stack loses its readable x-height, so they are reserved for
     glanceable, secondary, non-prose material. --fs-pico is the floor. */
  --fs-pico:     10px;
  --fs-nano:     11px;
  --fs-micro:    12px;
  --fs-tiny:     13px;
  --fs-xs:       14px;
  --fs-sm:       15px;
  --fs-body:     16px;
  --fs-lg:       18px;
  --fs-xl:       21px;
  --fs-h4:       clamp(24px, 2cqi, 28px);
  --fs-h3:       clamp(26px, 2.6cqi, 34px);
  --fs-h2:       clamp(28px, 3.6cqi, 44px);
  --fs-h1:       clamp(34px, 4.4cqi, 52px);
  --fs-hero:     clamp(42px, 7cqi, 96px);
  --fs-mega:     clamp(56px, 11cqi, 168px);
  /* Bridges the fixed --fs-* ladder (tops out at --fs-xl, 21px) and the fluid
     heading clamps below it — use for fixed (non-clamping) UI text that needs
     to sit between --fs-xl and a heading tier, e.g. empty-state icon glyphs. */
  --fs-2xl:      28px;

  /* Icon size scale — canonical sizes for glyphs/icons across the kit going
     forward. Existing ad-hoc pixel values (6/7/8/10/12/14/16/18px etc.)
     don't need to be migrated in this pass; new/updated icon usage should. */
  --icon-xs: 12px;
  --icon-sm: 16px;
  --icon-md: 20px;
  --icon-lg: 24px;

  /* App/chrome typescale: the default heading scale above is tuned for
     marketing/hero layouts (h1 up to 64px). Application chrome — page titles,
     panel headers — needs a compact ceiling so a PageHeader title doesn't
     dwarf the content. Opt in with `data-typescale="app"` on a container. */
  --fs-h1-app:   clamp(22px, 2cqi, 26px);
  --fs-h2-app:   clamp(18px, 1.6cqi, 21px);
  --fs-h3-app:   clamp(16px, 1.5cqi, 18px);
  --fs-h4-app:   clamp(15px, 1.6cqi, 17px);

  --lh-tight:    1.05;
  --lh-snug:     1.2;
  --lh-base:     1.55;
  --lh-long:     1.7;

  --tr-caps:     0.10em;
  --tr-label:    0.10em;
  --tr-tight:    -0.02em;
  --tr-tighter:  -0.03em;

  /* Spacing (8pt) */
  /* Micro rungs below --space-1 — dense editor/inspector chrome (table cell
     padding, pill gaps, tight toolbar gutters) needs finer steps than the 8pt
     scale offers; same reasoning as --r-hair/--r-0 sitting below --r-1. */
  --space-hair:  2px;
  --space-half:  3px;
  --space-0:     0;
  --space-1:     4px;
  --space-1-5:   5px;
  --space-1-75:  6px;
  --space-2:     8px;
  --space-2-5:   10px;
  --space-2-75:  12px;   /* fills the 10->16 hole (row-form gap, dense cell pad) */
  --space-3:     16px;
  --space-3-5:   20px;   /* fills the 16->24 hole (settings body, mono block pad) */
  --space-4:     24px;
  --space-5:     32px;
  --space-6:     48px;
  --space-7:     64px;
  --space-8:     96px;
  --space-9:     128px;
  --space-10:    192px;

  --density:     1;
  --pad-y:       calc(var(--space-6) * var(--density));
  --pad-x:       calc(var(--space-6) * var(--density));
  --gutter:      calc(var(--space-4) * var(--density));

  /* Borders — chunky or none */
  --bw-hair:     1px;
  --bw-rule:     2px;
  --bw-chunk:    4px;
  --bw-slab:     8px;

  /* Retuned toward PilotDeck's near-borderless whisper: 14% read as a visibly
     drawn line, not a hairline. 8% keeps the edge perceptible (still enough to
     separate a --panel-1-filled surface from its --bg backdrop) without
     competing with the fill + shadow already doing the container-definition
     work in .panel. --rule-strong is untouched — it is the deliberate
     stronger variant for places that need a visible seam (table rules, active
     tab underline), not the ambient panel/card edge. */
  --rule:        color-mix(in oklab, currentColor 8%, transparent);
  --rule-strong: color-mix(in oklab, currentColor 28%, transparent);

  /* Micro rungs below --r-1 — chips, badges, scrollbar thumbs, tick marks
     genuinely want a tighter corner than the base radius; without these two
     tokens the component sheets fell back to raw px literals bypassing the
     scale. RESTYLED (webjsx-toolkit look): base --radius is 0.5rem (8px),
     with radius-sm/md/lg at radius-4px/radius-2px/radius (4px/6px/8px) --
     --r-1 below is retuned to that 8px base; --r-2/--r-3 keep proportional
     headroom above it for large-surface corners (cards, dialogs) rather than
     collapsing to a single flat rung, since webjsx-toolkit itself has no
     opinion above its one --radius value. */
  --r-hair:      2px;
  --r-0:         4px;
  --r-1:         8px;
  --r-2:         10px;
  --r-3:         14px;
  --r-pill:      999px;

  --dur-snap:    80ms;
  --dur-base:    160ms;
  --dur-slow:    280ms;
  --dur-reveal:  560ms;
  --ease:        cubic-bezier(0.2, 0, 0, 1);
  /* Physical tier — a slight overshoot that reads as a printed stamp landing,
     used by presses and signature reveals (not by routine UI fades). Kept
     available post-restyle since some existing consumers still reference it;
     new work should prefer the webjsx-toolkit-equivalent curves below. */
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
  --ease-exit:   cubic-bezier(0.4, 0, 1, 1);
  /* RESTYLED (webjsx-toolkit look): its three named easing curves, ported
     verbatim from webjsx-toolkit/src/styles.css's @theme inline block. */
  --ease-drawer:     cubic-bezier(0.32, 0.72, 0, 1);
  --ease-collapse:   cubic-bezier(0.2, 0, 0, 1);
  --ease-out-strong: cubic-bezier(0.23, 1, 0.32, 1);

  /* Print texture — the prior "Acid Editorial" signature surface treatment.
     RESTYLED: opacity zeroed so .ds-grain/.ds-halftone (currently applied by
     no consumer, verified by grep across src/components and ui_kits) render
     inert under the flat-surface webjsx-toolkit look, without deleting the
     machinery in case a future editorial-mode consumer opts back in. --grain
     itself (the data-URI) and --grain-blend are left defined for that reason. */
  --grain-opacity:    0;
  --grain-blend:      multiply;
  --grain:       url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  /* Offset-ink registration — the small print-mark offset used on editorial
     edges (panel spine, expo numbering). A literal length, not a color. */
  --ink-offset:  3px;

  /* ============================================================
     Stacking scale — the ONE ordered set of layers anything may
     paint into. Rungs are 100 apart so a future layer can be
     inserted between two neighbours without renumbering the rest,
     and so a component may locally offset (+1/+2) within its own
     rung without colliding with the next one.

     Order is the contract; the numbers are just how it is spelled.
     Read top to bottom as "what paints over what":

       --z-below     -1   decorative texture UNDER its own content
                          (.ds-grain/.ds-halftone print layer)
       --z-base       0   in-flow content; the default plane
       --z-raised     100 a thing lifted above its siblings inside
                          one component (code-lang tab, timeline dot,
                          selection checkbox, resize handle)
       --z-sticky     200 sticky-within-a-scroller chrome: section
                          labels, sticky table headers, save bars.
                          Above raised content it scrolls past.
       --z-header     300 the app's own sticky chrome band
                          (.app-topbar/.app-chrome)
       --z-drawer     400 off-canvas panels (sidebar/pane/session
                          drawers) and their scrim, which must cover
                          the header they slide over
       --z-window     500 free-floating window-manager surfaces —
                          the OS kit's .wm-root canvas of draggable
                          windows. Above the shell, below all
                          transient popups.
       --z-dock       600 persistent OS shell furniture that must
                          stay above windows: launcher dock, menubar,
                          taskbar, os-root
       --z-dropdown   700 menus/popovers anchored to a trigger; they
                          must escape the dock that spawned them
       --z-modal      800 blocking dialogs, lightboxes, command
                          palettes and their backdrops
       --z-toast      900 transient notifications — above a modal,
                          because they report on what the modal did
       --z-tooltip   1000 pointer-following hint; never blocked
       --z-top       1100 last resort: boot/loading veils and
                          full-screen takeovers that outrank
                          everything, including a tooltip
     ============================================================ */
  --z-below:      -1;
  --z-base:        0;
  --z-raised:    100;
  --z-raised-1:  calc(var(--z-raised) + 1);
  --z-raised-2:  calc(var(--z-raised) + 2);
  --z-raised-3:  calc(var(--z-raised) + 3);
  --z-sticky:    200;
  --z-header:    300;
  --z-drawer:    400;
  --z-window:    500;
  --z-dock:      600;
  --z-dropdown:  700;
  --z-modal:     800;
  --z-toast:     900;
  --z-tooltip:  1000;
  --z-top:      1100;

  --measure:     68ch;
  --measure-wide:   940px;
  --measure-narrow: 760px;
  /* Page-stage ceiling — independent of --measure-wide (which stays the
     narrower editorial hero/prose measure). Fluid so ultra-wide viewports
     actually gain real estate instead of flat-lining at a fixed px cap.
     Percentage, not vw: a stage nested in a sidebar shell or an embedded
     window has no useful viewport of its own, and a vw term there resolves
     against the browser instead of the container it actually sits in. */
  --stage-wide:  min(96%, 1600px);
  /* Ultra-wide tier — past this the eye loses the line more than the layout
     gains, so multi-column content takes the extra width instead of one
     column growing without bound. */
  --stage-ultra: min(96%, 2100px);

  --sidebar-width:           256px;
  --sidebar-width-collapsed: 64px;

  /* Canonical breakpoint values — @media can't reference custom properties,
     so these are documented here (not tokenized) for consistency across
     responsive.css/responsive2-workspace.css/hero-content.css:
       - 480px  (max-width) mobile portrait
       - 768px  (max-width) mobile/tablet boundary, compact nav/sidebar collapse
       - 768-1024px (min/max-width) tablet landscape band
       - 900px  (max-width) / 901px (min-width) tablet/desktop layout reflow
       - 1025px (min-width) desktop
     Always use these exact values; a near-miss (e.g. 760px, 481px) should be
     aligned to its canonical neighbour when touched. */

  --lh-tall:     1.4;

  /* Control size scale — heights for status/topbar/crumb + controls. */
  --size-sm:     32px;
  --size-base:   42px;
  --size-lg:     56px;

  /* Dense control-height ladder — the actual heights front-door controls
     (buttons, fields, rows) snap to, one rung tighter than the --size-*
     chrome scale so app UI reads compact instead of marketing-scale. Density
     scales them so [data-density=compact/spacious] moves every control at once.
     Mirrors webgeist's 30/38/46 button ladder, retuned for this system. */
  --ctl-sm:      calc(28px * var(--density));
  --ctl-md:      calc(34px * var(--density));
  --ctl-lg:      calc(42px * var(--density));
  /* The canonical single-line input/select/searchfield height (== --ctl-md)
     and the default list-row min-height, both density-aware. */
  --field-height: var(--ctl-md);
  --row-height:   calc(40px * var(--density));

  /* Unified focus-ring tokens (single source for the design system). Outset
     rings (the default) tokenize colour+width+offset; bordered text fields use
     the inset variant. Deliberate negative offsets on edge-flush/clipping
     containers stay literal. --focus-color uses --accent-ink (the readable
     text/UI-boundary tone), never bare --accent (the lime FILL token, which
     measures ~1.07:1 against --paper and is nearly invisible as an outline —
     see AGENTS.md token-split rule). */
  --focus-color:       var(--accent-ink);
  --focus-w:           2px;
  --focus-offset:      2px;
  --focus-ring-inset:  inset 0 0 0 var(--focus-w) var(--focus-color);

  /* letterbox background for video/media containers */
  --media-letterbox:   #000;
}

/* ============================================================
   Focus-visible styles for keyboard navigation
   ============================================================ */
.ds-247420 :focus-visible {
  outline: var(--focus-w) solid var(--focus-color);
  outline-offset: var(--focus-offset);
}

/* Button variants with focus-visible */
.ds-247420 .btn:focus-visible,
.ds-247420 .btn-primary:focus-visible,
.ds-247420 .btn-ghost:focus-visible {
  outline: var(--focus-w) solid var(--focus-color);
  outline-offset: var(--focus-offset);
}

/* Link focus-visible — all route through the single --focus-* token set so a
   focus retune (colour/width/offset) happens in one place, never per selector. */
.ds-247420 a:focus-visible,
.ds-247420[role="link"]:focus-visible {
  outline: var(--focus-w) solid var(--focus-color);
  outline-offset: var(--focus-offset);
}

/* Form inputs with focus-visible */
.ds-247420 input:focus-visible,
.ds-247420 textarea:focus-visible,
.ds-247420 select:focus-visible {
  outline: var(--focus-w) solid var(--focus-color);
  outline-offset: var(--focus-offset);
}

/* Interactive element focus-visible */
.ds-247420[tabindex]:focus-visible {
  outline: var(--focus-w) solid var(--focus-color);
  outline-offset: var(--focus-offset);
}

.ds-247420[data-theme="ink"],
.ds-247420[data-theme="dark"] {
  color-scheme: dark;
  --bg:        var(--ink);
  --bg-2:      var(--ink-2);
  --bg-3:      var(--ink-3-dark);
  --fg:        var(--paper);
  --fg-2:      var(--paper-2);
  --fg-3:      var(--paper-3-dark);
  --panel-bg:  var(--bg);
  --panel-bg-2: var(--bg-2);
  --panel-0:   var(--bg);
  --panel-1:   var(--bg-2);
  --panel-2:   var(--bg-3);
  --panel-3:   color-mix(in oklab, var(--fg) 20%, transparent);
  --panel-text:   var(--fg);
  --panel-text-2: var(--fg-2);
  --panel-text-3: var(--fg-3);
  /* RESTYLED 2026-08b (green/purple lead): dark-mode accent is a LIGHT green
     chip (#5CBF52, already verified elsewhere in this file at 7.48:1+ on ink
     surfaces -- reused rather than inventing a new hex) with a DARK --ink
     foreground -- the inverse of light mode's dark-fill/light-text pairing,
     same convention every prior restyle used (light chip on dark page, not a
     same-dark-as-bg chip). --accent does not alias --acid here (--acid is
     tuned for light-mode fill-with-light-text; dark mode needs its own
     light-fill-with-dark-text direction). */
  --accent:    #5CBF52;
  --accent-fg: var(--ink);
  --accent-ink: var(--paper);
  --accent-bright: #5CBF52;
  --accent-tint: color-mix(in oklab, var(--accent) 30%, var(--bg));
  --danger:    oklch(0.68 0.19 25);
  --flame:     #FF5A1F;
  --amber:     #D9A93A;
  /* warn/sky dark-tuned pairs (mirror flame/amber): the :root paper values
     (#E0241A / #3A6EFF) are too dark on ink; these clear AA 4.5:1 on --ink. */
  --warn:      #FF5A52;
  /* Paired foreground for --warn-as-fill (see :root's --warn-fg comment) --
     dark theme's --warn is the lighter tuned red, so its fill needs the dark
     --ink text, the opposite pairing from :root. */
  --warn-fg:   var(--ink);
  --sky:       #6E9BFF;
  /* Category/heritage dark-tuned pairs (same convention as flame/amber/warn/
     sky above): the :root values are the PAPER tones and go dark-on-dark here.
     Measured on --ink (#0E0E12) / --bg-2 (#1F1F26): mascot-deep #B81F63 was
     3.13:1 / 2.66:1, purple-2 #7F18A4 2.36:1 / 2.00:1, green #247420 3.30:1 /
     2.80:1 — all failing AA for the text roles they carry (aicat/community
     author names, kpi deltas, stamps, rails). The replacements clear 4.5:1 on
     BOTH ink surfaces: #FF5C9E 6.67/5.67, #C277FF 6.77/5.75, #5CBF52 8.28/7.04.
     These are text-tone retunes only — the paper values are untouched. */
  --mascot-deep: #FF5C9E;
  --purple-2:  #C277FF;
  --green:     #5CBF52;
  /* --cat-*-ink text tones for dark. The :root sources (--green-deep #133F10,
     --purple-deep #220026) are near-black PAPER tones — on ink they land at
     1.60:1 / 1.01:1, i.e. invisible. Repoint them at the dark-tuned values
     already proven above: #5CBF52 8.28:1 on ink / 7.04:1 on --bg-2, #C277FF
     6.77:1 / 5.75:1. --cat-mascot-ink needs no entry here: it tracks
     --mascot-deep, which this same block already remaps to #FF5C9E. */
  --cat-green-ink:  #5CBF52;
  --cat-purple-ink: #C277FF;
  --code-string:  var(--green-2);
  --code-keyword: var(--sky);
  --code-fn:      var(--flame);
  --code-str-alt: var(--mascot);
  --code-num:     var(--sun);
}

/* [data-accent="X"] overrides (below) hardcode LIGHT-tuned fills (--acid,
   --purple, --green) with no dark-theme companion, so data-theme=dark +
   data-accent=acid/purple/green produced a near-invisible fill against the
   dark page bg (measured: acid 1.15:1, purple 1.04:1, green 2.98:1 on --ink
   -- all under the 3:1 non-text UI-fill floor). Same convention as the base
   --accent retune above: restate the fill as its dark-tuned equivalent so
   the swatch stays visible, using tones already verified elsewhere in this
   file (green retune 7.48:1 on --ink, purple-2 retune 6.11:1 on --ink). */
.ds-247420[data-theme="ink"][data-accent="acid"],
.ds-247420[data-theme="dark"][data-accent="acid"] {
  --accent: #5CBF52; --accent-bright: #5CBF52; --accent-fg: var(--ink); --accent-ink: var(--paper); --panel-accent: #5CBF52;
}
.ds-247420[data-theme="ink"][data-accent="purple"],
.ds-247420[data-theme="dark"][data-accent="purple"] {
  --accent: #C277FF; --accent-bright: #C277FF; --accent-fg: var(--ink); --accent-ink: #C277FF; --panel-accent: #C277FF;
}
.ds-247420[data-theme="ink"][data-accent="green"],
.ds-247420[data-theme="dark"][data-accent="green"] {
  --accent: #5CBF52; --accent-bright: #5CBF52; --accent-fg: var(--ink); --accent-ink: #5CBF52; --panel-accent: #5CBF52;
}

/* Restores the pre-existing (weaker) dark-theme selection tint after the
   light-theme-only bump in app-shell.css's base ::selection rule. */
.ds-247420[data-theme="ink"] ::selection,
.ds-247420[data-theme="dark"] ::selection {
  background: color-mix(in oklab, var(--accent) 24%, var(--bg-2));
  color: var(--fg);
}

/* NOTE: the [data-theme="auto"] block below mirrors the
   [data-theme="ink"/"dark"] block above token-for-token. 'auto + OS-dark' and
   'explicit dark' are the same visual context and resolve every token —
   including --danger — identically. The two are kept as separate selectors
   only because one is media-gated; a future de-drift can consolidate them into
   a shared selector list once a build step can hoist the media query. */
@media (prefers-color-scheme: dark) {
  .ds-247420[data-theme="auto"] {
    --bg:        var(--ink);
    --bg-2:      var(--ink-2);
    --bg-3:      var(--ink-3-dark);
    --fg:        var(--paper);
    --fg-2:      var(--paper-2);
    --fg-3:      var(--paper-3-dark);
    --panel-bg:  var(--bg);
    --panel-bg-2: var(--bg-2);
    --panel-0:   var(--bg);
    --panel-1:   var(--bg-2);
    --panel-2:   var(--bg-3);
    --panel-3:   color-mix(in oklab, var(--fg) 20%, transparent);
    --panel-text:   var(--fg);
    --panel-text-2: var(--fg-2);
    --panel-text-3: var(--fg-3);
    --accent:    #5CBF52; /* mirrors [data-theme=ink/dark] retune above */
    --accent-fg: var(--ink);
    --accent-ink: var(--paper);
    --accent-bright: #5CBF52;
    --accent-tint: color-mix(in oklab, var(--accent) 30%, var(--bg));
    --danger:    oklch(0.68 0.19 25);
    --flame:     #FF5A1F;
    --amber:     #D9A93A;
    --warn:      #FF5A52;
    --warn-fg:   var(--ink);
    --sky:       #6E9BFF;
    /* Mirrors the [data-theme="ink"] category retune above token-for-token —
       see that block for the measured before/after ratios. */
    --mascot-deep: #FF5C9E;
    --purple-2:  #C277FF;
    --green:     #5CBF52;
    --cat-green-ink:  #5CBF52;
    --cat-purple-ink: #C277FF;
    --code-string:  var(--green-2);
    --code-keyword: var(--sky);
    --code-fn:      var(--flame);
    --code-str-alt: var(--mascot);
    --code-num:     var(--sun);
  }
  .ds-247420[data-theme="auto"] ::selection {
    background: color-mix(in oklab, var(--accent) 24%, var(--bg-2));
    color: var(--fg);
  }
}

/* Nested-scope safety: if .ds-247420 appears under another .ds-247420 (e.g.
   mount() added the class to an inner element while the ancestor html
   already had it), the inner element would otherwise re-trigger the root
   light defaults and detach from the ancestor's data-theme override. Force
   the inner element to inherit the ancestor's resolved tokens. */
.ds-247420 .ds-247420 {
  --bg:        inherit;
  --bg-2:      inherit;
  --bg-3:      inherit;
  --fg:        inherit;
  --fg-2:      inherit;
  --fg-3:      inherit;
  --panel-bg:  inherit;
  --panel-bg-2: inherit;
  --accent:    inherit;
  --accent-fg: inherit;
  --accent-ink: inherit;
  --panel-1:   inherit;
  --panel-2:   inherit;
  --panel-3:   inherit;
  --panel-text: inherit;
  --panel-text-2: inherit;
  --panel-text-3: inherit;
  --panel-accent-2: inherit;
  --panel-shadow: inherit;
  --panel-shadow-1: inherit;
  --on-accent: inherit;
  --on-color: inherit;
  --scrim: inherit;
  --scrim-strong: inherit;
  --scrim-media: inherit;
  --cat-green: inherit;
  --cat-purple: inherit;
  --cat-mascot: inherit;
  --cat-green-ink: inherit;
  --cat-purple-ink: inherit;
  --cat-mascot-ink: inherit;
  --mascot-on-ink: inherit;
  --success-on-ink: inherit;
  --cat-sun: inherit;
  --cat-flame: inherit;
  --cat-sky: inherit;
  --code-string: inherit;
  --code-keyword: inherit;
  --code-fn: inherit;
  --code-str-alt: inherit;
  --code-num: inherit;
}

/* ============================================================
   Named-theme preset contract
   ------------------------------------------------------------
   A theme is one [data-theme="X"] block that overrides ONLY the semantic
   surface tokens — never component rules, never raw literals in component
   sheets. The base :root above IS the "paper" theme; [data-theme="paper"]
   restates it so the attribute is explicit and round-trippable. To add a
   theme: copy a block, rename the attr, retune the ~8 semantic tokens.
   ============================================================ */
.ds-247420[data-theme="paper"] {
  color-scheme: light;
  --bg: var(--paper); --bg-2: var(--paper-2); --bg-3: var(--paper-3);
  --fg: var(--ink);   --fg-2: var(--ink-2);   --fg-3: var(--ink-3);
  --accent: var(--acid); --accent-fg: var(--paper); --accent-ink: var(--acid-deep);
  --accent-tint: color-mix(in oklab, var(--accent-ink) 22%, var(--bg));
  /* A paper island under a dark ancestor must not inherit the dark signal pair. */
  --flame: #C53E00; --amber: #7C570F; --warn: #C41C0C; --sky: #3A6EFF;
}

/* thebird — warm-paper brand preset. A first-class swappable theme: it lives
   here in the canonical layer (not as a global :root override) so any consumer
   can opt in with data-theme="thebird". The OS-shell chrome (bar heights,
   window controls) stays in src/kits/os/theme.css; only color lives here. */
.ds-247420[data-theme="thebird"] {
  color-scheme: light;
  --paper:   #F5F0E4;
  --paper-2: #EFE9DB;
  --paper-3: #E3DAC7;
  --bg: var(--paper); --bg-2: var(--paper-2); --bg-3: var(--paper-3);
  --fg: var(--ink);   --fg-2: var(--ink-2);   --fg-3: var(--ink-3);
  --accent: var(--green); --accent-fg: var(--paper); --accent-ink: var(--green);
}

/* github-dark — pi-web's GitHub-dark theme. Cool dark palette with blue accent,
   ported from pi-web (https://github.com/jmfederico/pi-web). */
.ds-247420[data-theme="github-dark"] {
  color-scheme: dark;
  --bg:        #0d1117;
  --bg-2:      #161b22;
  --bg-3:      #21262d;
  --fg:        #e6edf3;
  --fg-2:      #c9d1d9;
  --fg-3:      #8b949e;
  --panel-bg:  var(--bg);
  --panel-bg-2: var(--bg-2);
  --panel-0:   var(--bg);
  --panel-1:   var(--bg-2);
  --panel-2:   var(--bg-3);
  --panel-3:   color-mix(in oklab, var(--fg) 20%, transparent);
  --panel-text:   var(--fg);
  --panel-text-2: var(--fg-2);
  --panel-text-3: var(--fg-3);
  --accent:    #58a6ff;
  --accent-fg: #ffffff;
  --accent-ink: #58a6ff;
  --accent-bright: #79c0ff;
  --accent-tint: color-mix(in oklab, var(--accent) 30%, var(--bg));
  --danger:    #ff7b72;
  --flame:     #ff7b72;
  --amber:     #d29922;
  --warn:      #ff7b72;
  --warn-fg:   #0d1117;
  --sky:       #58a6ff;
  --mascot-deep: #ff7b72;
  --purple-2:  #d2a8ff;
  --green:     #3fb950;
  --green-2:   #3fb950;
  --cat-green-ink:  #3fb950;
  --cat-purple-ink: #d2a8ff;
  --code-string:  var(--green-2);
  --code-keyword: var(--sky);
  --code-fn:      var(--flame);
  --code-str-alt: var(--mascot-deep);
  --code-num:     var(--amber);
}

.ds-247420[data-theme="github-dark"] ::selection {
  background: color-mix(in oklab, var(--accent) 24%, var(--bg-2));
  color: var(--fg);
}

.ds-247420[data-density="compact"]     { --density: 0.75; }
.ds-247420[data-density="comfortable"] { --density: 1; }
.ds-247420[data-density="spacious"]    { --density: 1.35; }

.ds-247420[data-accent="acid"]   { --accent: var(--acid);   --accent-bright: var(--acid);     --accent-fg: var(--paper); --accent-ink: var(--acid-deep); --panel-accent: var(--acid);   }
.ds-247420[data-accent="green"]  { --accent: var(--green);  --accent-bright: var(--green-2);  --accent-fg: var(--paper); --accent-ink: var(--green);     --panel-accent: var(--green);  }
.ds-247420[data-accent="purple"] { --accent: var(--purple); --accent-bright: #C277FF;         --accent-fg: var(--paper); --accent-ink: var(--purple-2);  --panel-accent: var(--purple); }
.ds-247420[data-accent="mascot"] { --accent: var(--mascot); --accent-bright: var(--mascot-2); --accent-fg: var(--ink);   --accent-ink: var(--mascot-deep);--panel-accent: var(--mascot); }

/* Type-scale modifier — opt-in body/lg/xl bump for surfaces that want denser
   or larger reading sizes without a custom override. */
.ds-247420[data-typescale="sm"] {
  --fs-body: 15px; --fs-lg: 17px; --fs-xl: 20px;
  --fs-h4: clamp(22px, 1.9cqi, 24px);
  --fs-h3: clamp(23px, 2.4cqi, 32px);
  --fs-h2: clamp(26px, 3.4cqi, 41px);
  --fs-h1: clamp(32px, 4.2cqi, 48px);
}
.ds-247420[data-typescale="lg"] {
  --fs-body: 17px; --fs-lg: 20px; --fs-xl: 24px;
  --fs-h4: clamp(27px, 2.2cqi, 31px);
  --fs-h3: clamp(28px, 2.9cqi, 38px);
  --fs-h2: clamp(31px, 4cqi, 49px);
  --fs-h1: clamp(38px, 4.8cqi, 58px);
}

/* Debug grid overlay — 8px mascot grid. Toggle via class on any container. */
.ds-247420 .with-grid-overlay {
  background-image:
    linear-gradient(to right, rgba(232,75,138,0.15) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(232,75,138,0.15) 1px, transparent 1px);
  background-size: 8px 8px;
}

/* Reduced-motion guard — neutralizes every transition/animation/transform
   inside the design-system scope when the user requests reduced motion.
   Components remain free to opt back in with
   `@media (prefers-reduced-motion: no-preference) { ... }`. */
@media (prefers-reduced-motion: reduce) {
  .ds-247420 *,
  .ds-247420 *::before,
  .ds-247420 *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

/* ============================================================
   Semantic alias tier — primitive -> semantic -> component
   ------------------------------------------------------------
   Three layers, in order:
     1. PRIMITIVE  (above)  raw palette values: --acid, --green, --sky, --warn,
        --panel-1/2/3, --paper/--ink, etc. These are the only tokens that ever
        carry a literal color/length. A re-brand edits primitives.
     2. SEMANTIC   (here)   named-by-INTENT aliases that resolve to a primitive
        (e.g. --accent: var(--panel-accent)). A component or a future re-brand
        can retune ONE semantic variable (say, --rail-danger) without hunting
        through every primitive it happens to reuse elsewhere.
     3. COMPONENT  (app-shell.css, community.css, etc.) component sheets keep
        consuming primitives directly today (--accent, --panel-*, --green,
        --warn, ...) exactly as before — this tier does not repoint any
        existing component rule. It exists so a FUTURE swap has a semantic
        layer ready to migrate onto, one component sheet at a time, without
        a flag-day rewrite. Purely additive: no primitive token's name or
        value changes here, and no component references these new names yet.

   Naming matches vocabulary already established elsewhere in this system:
   --surface-1/2 mirror the --panel-1/--panel-2 tonal-surface steps already
   used across app-shell.css/community.css; --rail-info/-success/-warning/-error
   mirror the canonical .tone-info/.tone-success/.tone-warning/.tone-error
   banner/badge/chip severity names (app-shell.css L592, community.css
   .cm-banner.tone-*) rather than inventing a new severity vocabulary.
   ============================================================ */
.ds-247420 {
  /* --accent already exists as a primitive-facing alias (var(--acid)) above;
     restated here under the semantic tier for discoverability alongside its
     siblings. Same value, same token — not a second definition. */
  --surface-1: var(--panel-1);
  --surface-2: var(--panel-2);

  /* Rail/status severity aliases — map onto the same color choices the
     existing .cm-banner.tone-* rules in community.css already use, so a
     future rail/indicator component can consume one semantic name instead
     of picking the right primitive per severity by hand. */
  --rail-info:    var(--sky);
  --rail-success: var(--green-2);
  --rail-warning: var(--amber);
  --rail-error:   var(--warn);
}

/* Elevation tokens consumed by overlays (Tooltip, Popover, Dropdown, Dialog). */
.ds-247420 {
  --shadow-1: 0 1px 2px color-mix(in oklab, var(--fg) 8%, transparent);
  --shadow-2: 0 4px 12px color-mix(in oklab, var(--fg) 10%, transparent),
              0 2px 4px  color-mix(in oklab, var(--fg) 6%,  transparent);
  --shadow-3: 0 12px 32px color-mix(in oklab, var(--fg) 14%, transparent),
              0 4px 8px   color-mix(in oklab, var(--fg) 8%,  transparent);
  /* Overlay elevation — the float for dialogs/lightboxes that sit above a
     scrim; heavier than --shadow-3 and theme-driven (was raw rgba(0,0,0,.4)). */
  --shadow-overlay: 0 12px 40px color-mix(in oklab, var(--fg) 28%, transparent),
                    0 4px 12px  color-mix(in oklab, var(--fg) 16%, transparent);
  /* --z-popover is an alias, not a rung: popovers and dropdowns are the
     same anchored-to-a-trigger layer and must never be able to drift
     apart. The rungs themselves live with the stacking scale above. */
  --z-popover:   var(--z-dropdown);
}

/* app-shell.css */
.ds-247420 * { box-sizing: border-box; }
/* width:100% + overflow-x:clip on both html and body: without an explicit
   width, either can end up narrower than the real viewport once ANY
   descendant's layout box overflows horizontally by even a subpixel (a flex
   row that doesn't quite wrap, a fixed-width child inside a shrinking
   container) -- the sticky .app-topbar/.app-chrome then reads as "not
   filling the page" because it's pinned to .app's box while the page-level
   scrollbar (driven by body's real scrollWidth) sits further right than
   .app's right edge, and login-style narrow pages show the same scrollbar
   short of the visual page edge. clip (not hidden) so no new scroll
   container is introduced -- .app-main/.app-side-shell already own their
   own scroll regions and must stay the only scrollers. */
.ds-247420, .ds-247420 body { margin: 0; padding: 0; width: 100%; overflow-x: clip; }
.ds-247420 button, .ds-247420 input, .ds-247420 select, .ds-247420 textarea { font: inherit; }

/* Global reduced-motion safety net: catches any component transition/animation
   that lacks its own per-file prefers-reduced-motion override (most components
   already opt out locally; this is the backstop for the rest, e.g.
   plugins-config.css, catalog-theme.css, sidebar-misc.css, row-print.css,
   topbar.css, primitives.css, git-status.css, panel-row.css). Scroll-snap
   carousels handle scroll-behavior themselves and are unaffected. */
@media (prefers-reduced-motion: reduce) {
  .ds-247420 *, .ds-247420 *::before, .ds-247420 *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

.ds-247420 .skip-link {
  --skip-link-preflight-translate-clear: -100%;
  position: absolute;
  top: 0;
  left: 0;
  transform: translateY(var(--skip-link-preflight-translate-clear));
  background: var(--accent);
  color: var(--accent-fg);
  padding: var(--space-2) var(--space-3);
  text-decoration: none;
  z-index: var(--z-top);
  border-radius: var(--r-pill);
  font-weight: 600;
  font-size: var(--fs-sm);
  --skip-link-min-touch-target: 44px;
  min-height: var(--skip-link-min-touch-target);
  display: inline-flex;
  align-items: center;
}
.ds-247420 .skip-link:focus {
  transform: translateY(0);
  top: 10px;
  outline: var(--focus-w) solid var(--focus-color);
  outline-offset: var(--focus-offset);
}
.ds-247420 .sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.ds-247420 .ds-aspect {
  aspect-ratio: var(--aspect, 16 / 9);
  width: 100%;
  overflow: hidden;
}
.ds-247420 .ds-aspect > * { width: 100%; height: 100%; object-fit: cover; display: block; }
.ds-247420 .ds-aspect-square { --aspect: 1 / 1; }
.ds-247420 .ds-aspect-video { --aspect: 16 / 9; }
.ds-247420 .ds-aspect-portrait { --aspect: 3 / 4; }

.ds-247420 body {
  background: var(--bg);
  color: var(--fg);
  font-family: var(--ff-body);
  font-size: var(--fs-body);
  line-height: var(--lh-base);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
.ds-247420 {
  --selection-tint-paper-share: 46%;
}
.ds-247420 ::selection { background: color-mix(in oklab, var(--accent) var(--selection-tint-paper-share), var(--bg-2)); color: var(--fg); }

.ds-247420 .app, .ds-247420 .ds-stage {
  container-type: inline-size;
  overflow-x: clip;
  min-width: 0;
}
.ds-247420 .app * { min-width: 0; }

.ds-247420 .app, .ds-247420 .ds-stage, .ds-247420 .app *, .ds-247420 .ds-stage * {
  scrollbar-width: thin;
  scrollbar-color: var(--bg-3) transparent;
}
/* Firefox gets themed scrollbars via scrollbar-width/-color above; Chrome,
   Safari, and Edge need the ::-webkit-scrollbar family too, or every
   scrollable node in the app shell falls back to the raw OS scrollbar and
   clashes with the dark UI. Same selector scope, same --bg-3 token. */
.ds-247420 .app::-webkit-scrollbar, .ds-247420 .ds-stage::-webkit-scrollbar, .ds-247420 .app *::-webkit-scrollbar, .ds-247420 .ds-stage *::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}
.ds-247420 .app::-webkit-scrollbar-track, .ds-247420 .ds-stage::-webkit-scrollbar-track, .ds-247420 .app *::-webkit-scrollbar-track, .ds-247420 .ds-stage *::-webkit-scrollbar-track {
  background: transparent;
}
.ds-247420 .app::-webkit-scrollbar-thumb, .ds-247420 .ds-stage::-webkit-scrollbar-thumb, .ds-247420 .app *::-webkit-scrollbar-thumb, .ds-247420 .ds-stage *::-webkit-scrollbar-thumb {
  background: var(--bg-3);
  border-radius: var(--r-pill, 999px);
}

.ds-247420 h1, .ds-247420 .t-h1 { font-family: var(--ff-display); font-size: var(--fs-h1); line-height: var(--lh-snug); letter-spacing: var(--tr-tight); font-weight: var(--fw-semibold); margin: 0; }
.ds-247420 h2, .ds-247420 .t-h2 { font-family: var(--ff-display); font-size: var(--fs-h2); line-height: var(--lh-snug); letter-spacing: var(--tr-tight); font-weight: 600; margin: 0; }
.ds-247420 h3, .ds-247420 .t-h3 { font-family: var(--ff-display); font-size: var(--fs-h3); line-height: var(--lh-snug); letter-spacing: -0.01em;          font-weight: 600; margin: 0; }
.ds-247420 h4, .ds-247420 .t-h4 { font-family: var(--ff-display); font-size: var(--fs-h4); line-height: var(--lh-snug);                                  font-weight: 600; margin: 0; }

.ds-247420[data-typescale="app"] h1, .ds-247420[data-typescale="app"] .t-h1 { font-size: var(--fs-h1-app); line-height: 1.15; font-weight: var(--fw-medium); }
.ds-247420[data-typescale="app"] h2, .ds-247420[data-typescale="app"] .t-h2 { font-size: var(--fs-h2-app); font-weight: var(--fw-medium); }
.ds-247420[data-typescale="app"] h3, .ds-247420[data-typescale="app"] .t-h3 { font-size: var(--fs-h3-app); font-weight: var(--fw-medium); }
.ds-247420[data-typescale="app"] h4, .ds-247420[data-typescale="app"] .t-h4 { font-size: var(--fs-h4-app); }

.ds-247420 .t-hero    { font-family: var(--ff-display); font-size: var(--fs-hero); line-height: var(--lh-tight); letter-spacing: var(--tr-tighter); font-weight: 700; margin: 0; }
.ds-247420 .t-mega    { font-family: var(--ff-display); font-size: var(--fs-mega); line-height: 0.92;            letter-spacing: var(--tr-tighter); font-weight: 700; margin: 0; }

.ds-247420 .t-display { font-family: var(--ff-display); font-weight: 600; letter-spacing: var(--tr-tight); }
.ds-247420 .t-mono    { font-family: var(--ff-mono); font-weight: 400; }
.ds-247420 .t-body    { font-family: var(--ff-body); }

.ds-247420 .t-label   { font-family: var(--ff-body); font-size: var(--fs-tiny); letter-spacing: 0; font-weight: 600; }
.ds-247420 .t-micro   { font-family: var(--ff-body); font-size: var(--fs-micro); letter-spacing: 0; color: var(--fg-3); font-weight: 600; }
.ds-247420 .t-meta    { font-family: var(--ff-body); font-size: var(--fs-sm); color: var(--fg-3); }

.ds-247420 .lede, .ds-247420 .t-lede {
  font-family: var(--ff-body); font-size: var(--fs-xl);
  line-height: var(--lh-tall); max-width: 38ch; font-weight: 400; color: var(--fg-2);
  margin: 0;
}

.ds-247420 h1 + .lede, .ds-247420 h1 + .t-lede, .ds-247420 h2 + .lede, .ds-247420 h2 + .t-lede, .ds-247420 h3 + .lede, .ds-247420 h3 + .t-lede, .ds-247420 .t-h1 + .lede, .ds-247420 .t-h1 + .t-lede {
  margin-top: var(--space-2);
}

/* .app-main.narrow widens well past prose measure on wide no-sidebar
   viewports on purpose (topbar.css) so full-width siblings like .post-meta/
   h1/.post-index use the room. .lede/.ds-prose keep their reading-measure
   caps (38ch/--measure) but stayed left edge-anchored inside that wider
   column, turning the freed width into one-sided dead whitespace on the
   right of the article body instead of being absorbed as margin. */
.ds-247420 .app-main.narrow > .lede, .ds-247420 .app-main.narrow > .t-lede, .ds-247420 .app-main.narrow > .ds-prose { margin-left: auto; margin-right: auto; }

.ds-247420 .lede + .post-body, .ds-247420 .t-lede + .post-body, .ds-247420 .lede + .ds-prose, .ds-247420 .t-lede + .ds-prose {
  margin-top: var(--space-4);
}

.ds-247420 .lede + .ds-section, .ds-247420 .t-lede + .ds-section {
  margin-top: var(--space-8);
}

.ds-247420 .eyebrow {
  font-family: var(--ff-mono);
  font-size: var(--fs-tiny);
  font-weight: 600;
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--accent-ink);
  display: inline-flex; align-items: center; gap: 12px;
}
.ds-247420 .eyebrow::before {
  content: '';
  width: 28px; height: 3px; background: currentColor;
}

.ds-247420 .ds-prose {
  font-family: var(--ff-body);
  font-size: var(--fs-lg);
  line-height: var(--lh-long);
  max-width: var(--measure);
  text-wrap: pretty;
}
.ds-247420 .ds-prose p  { margin: 0 0 var(--space-4); }
.ds-247420 .ds-prose h1 { font-size: clamp(34px, 5.4cqi, 64px); margin-bottom: var(--space-3); }
.ds-247420 .ds-prose h2 { margin: var(--space-6) 0 var(--space-3); font-size: var(--fs-h3); }
.ds-247420 .ds-prose h3 { margin: var(--space-5) 0 var(--space-2); font-size: var(--fs-h4); font-weight: 600; }
.ds-247420 .ds-prose h4 { margin: var(--space-5) 0 var(--space-2); font-size: var(--fs-xl); font-weight: 600; }
.ds-247420 .ds-prose hr { border: none; border-top: var(--bw-hair) solid var(--rule); margin: var(--space-6) 0; }
.ds-247420 .ds-prose blockquote {
  margin: var(--space-5) 0;
  padding: var(--space-3) var(--space-4);
  border-left: var(--bw-chunk, 3px) solid var(--panel-accent);
  background: var(--panel-hover);
  border-radius: var(--r-1);
  color: var(--panel-text-2);
  font-family: var(--ff-body);
}
.ds-247420 .ds-prose blockquote p:last-child { margin-bottom: 0; }
.ds-247420 .ds-prose .ds-prose-endnote,
.ds-247420 .ds-prose-endnote {
  margin-top: var(--space-5);
  margin-bottom: 0;
  font-family: var(--ff-mono);
  font-size: var(--fs-xs);
  line-height: var(--lh-base);
  color: var(--fg-3);
}

.ds-247420 .dim { color: var(--fg-3); }

.ds-247420 a { color: inherit; text-decoration: none; }
.ds-247420 a.ds-link-accent, .ds-247420 .ds-prose a {
  color: var(--accent-ink);
  text-decoration: underline;
  text-underline-offset: 4px;
  text-decoration-thickness: 2px;
}
/* Visited state was entirely absent -- a returning user had no way to tell an
   already-read link from an unread one, the one link state browsers don't
   fake for you. --purple-2 keeps it distinct from both the unvisited accent
   and any hover/danger tone already in the palette. */
.ds-247420 a.ds-link-accent:visited, .ds-247420 .ds-prose a:visited {
  color: var(--purple-2);
}
.ds-247420 a.ds-link-accent:hover, .ds-247420 .ds-prose a:hover {
  background: var(--accent); color: var(--accent-fg); text-decoration-color: transparent;
}

.ds-247420 kbd {
  font-family: var(--ff-mono); font-size: 0.85em;
  background: var(--bg-2); padding: 2px 6px; border-radius: var(--r-1);
}

.ds-247420 code {
  --code-pill-tier3-surface: var(--bg-3);
  font-family: var(--ff-mono); font-size: 0.92em;
  background: var(--code-pill-tier3-surface); padding: 0.15em 0.5em; border-radius: var(--r-1);
}

.ds-247420 pre {
  font-family: var(--ff-mono);
  background: var(--panel-bg);
  color: var(--panel-text, inherit);
  padding: var(--space-5);
  overflow-x: auto;
  line-height: 1.55;
  font-size: var(--fs-sm);
  margin: 0;
  border-radius: var(--r-3);
}
.ds-247420 pre .k { color: var(--code-str-alt, var(--mascot)); }
.ds-247420 pre .s { color: var(--code-string, var(--green)); }
.ds-247420 pre .c { color: var(--fg-3); }
.ds-247420 pre .n { color: var(--code-num); }

/* ============================================================
   AppShell — page frame. The topbar floats above a soft background;
   the body sits inside a rounded inner shell for a friendlier feel.
   ============================================================ */
.ds-247420 .app {
  display: flex; flex-direction: column;
  /* Offset parent for the sidebar drawer/scrim/toggle, which are absolute so
     they overlay THIS shell (a thebird WM window) rather than the viewport. */
  position: relative;
  /* Explicit at every width, not just the >=1400px override below: a block-
     level flex container's width:auto normally fills its containing block,
     but that only held reliably once html/body also carried width:100% (see
     base.css) -- restating it here removes any remaining ambiguity for the
     one box every sticky header/scrollbar-fill report traces back to. */
  width: 100%;
  min-height: 100vh;
  /* Definite height so the flex column resolves height:100% for descendants
     (e.g. a full-height chat) at every breakpoint, not just where a desktop
     media override happened to set explicit heights. dvh tracks mobile chrome. */
  min-height: 100dvh;
  height: 100dvh;
  background: var(--bg);
  color: var(--fg);
  /* Notched-device safe area padding (no-op on devices without notches) */
  padding-top: env(safe-area-inset-top);
  padding-bottom: env(safe-area-inset-bottom);
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}

/* When the app shell is embedded inside a windowed surface (e.g. the freddie
   dashboard mounted in a WM window via .fd-root) it is NOT a full-page app:
   `min-height:100vh` and the absence of an explicit width make it collapse to
   0 width at desktop, where the responsive single-column @media rules don't
   apply. Inside .fd-root it must fill its container instead. (At <=767px the
   grid already linearizes, which is why the bug only showed at desktop width.) */
.ds-247420 .fd-root .app { width: 100%; height: 100%; min-height: 0; flex: 1 1 auto; }
.ds-247420 .fd-root .app-body { min-width: 0; }

/* Opt-in for long-form/marketing content pages (e.g. a lore/article page)
   mounted in this same shell -- the default .app is a HARD height:100dvh
   clamp so its two children (.app-side-shell/.app-main) can each own
   independent internal scroll (the correct split-scroll architecture for
   dashboard-style kits like chat/dashboard/os, see .app-side-shell's own
   comment). A page whose content is simply longer than the viewport and
   should scroll as one normal document -- not clip at the fold -- opts in
   with .app-doc-scroll: height becomes min-height only, so .app (and every
   flex descendant relying on min-height:0 to cap its own height) can grow
   past 100dvh instead of clipping. Consumers set this by adding the class
   to whatever element carries .app (see AnEntrypoint/design#viewport-clamp
   for the reproduction this fixes -- a 247420.xyz content route clipped at
   exactly the viewport height with the rest of the page unreachable). */
.ds-247420 .app.app-doc-scroll { height: auto; min-height: 100dvh; overflow: visible; }
.ds-247420 .app-doc-scroll .app-body { min-height: auto; }
.ds-247420 .app-doc-scroll .app-main,
.ds-247420 .app-doc-scroll .app-side-shell { overflow-y: visible; min-height: auto; height: auto; }

/* Full-bleed canvas host (e.g. a game/3D viewport that fills the page behind the
   shell): the shell's opaque var(--bg) would paint OVER the canvas and hide it.
   Mark the host with `canvas-host` to make the shell surfaces transparent so the
   live canvas shows through. The canvas itself is the backdrop. */
.ds-247420 .canvas-host,
.ds-247420 .canvas-host .app,
.ds-247420 body.canvas-host { background: transparent !important; }

.ds-247420 {
  --app-status-h: 26px;
  --app-topbar-h: 64px;
  --app-crumb-h: 56px;
}

.ds-247420 .app-topbar {
  position: sticky; top: 0; z-index: var(--z-header);
  display: flex; flex-wrap: wrap;
  align-items: center; gap: var(--space-4);
  min-height: var(--app-topbar-h);
  padding: var(--space-2-5) var(--pad-x);
  border-bottom: var(--bw-hair, 1px) solid var(--rule);
  /* Explicit opaque fill, not "inherits var(--bg) by being transparent":
     a sticky header with no background of its own reads correctly only when
     nothing with a different fill ever scrolls under it. Any kit with a
     tinted section, hero gradient, or --bg-2 panel behind the topbar showed
     it as colorless/see-through once that content scrolled underneath —
     the "titlebar has no color" report. Every sticky header in this shell
     (crumb, merged chrome, status) must carry the same opaque var(--bg) so
     none of them silently depend on page order to look filled. */
  background: var(--bg);
}

/* Merged chrome: when AppShell gets both a topbar and a crumb it wraps them
   in .app-chrome and they share ONE sticky band instead of stacking as two
   bars. The breadcrumb provides the left identity (it already begins with the
   brand), so the topbar's standalone brand is hidden to avoid showing the name
   twice; nav and the crumb's right slot sit together on the right. */
.ds-247420 .app-chrome {
  position: sticky; top: 0; z-index: var(--z-header);
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--space-2) var(--space-4);
  padding: var(--space-2) var(--pad-x);
  border-bottom: var(--bw-hair, 1px) solid var(--rule);
  /* Same opaque-fill requirement as standalone .app-topbar above — the
     merged crumb+topbar band is sticky and must not depend on nothing
     scrolling under it to look filled. */
  background: var(--bg);
}
.ds-247420 .app-chrome > .app-topbar,
.ds-247420 .app-chrome > .app-crumb {
  position: static; background: none; backdrop-filter: none;
  -webkit-backdrop-filter: none; padding: 0; min-height: 0;
  flex: 1 1 auto;
  /* Standalone .app-topbar/.app-crumb each carry their own border-bottom for
     when they're the lone sticky bar -- inside a merged .app-chrome that hairline
     is still active on the CHILD box, which sits above the chrome's own bottom
     edge (crumb+topbar rows are shorter than the chrome's full height once
     padding/gap are added), painting a second, fainter line above the chrome's
     real bottom border. Only .app-chrome should own the merged band's edge. */
  border-bottom: none;
}
.ds-247420 .app-chrome > .app-crumb { order: 1; flex: 1 1 auto; min-width: 0; }
/* flex:1 1 auto + min-width:0 (not the previous 0 0 auto): a merged
 * chrome's topbar carries the search input, which has its own shrink
 * basis (.ds-topbar-search, kits-appended.css) meant to yield space before
 * the row wraps -- but a `0 0 auto` topbar box never shrinks itself, so the
 * search input's flexibility was inert and the whole topbar instead
 * dropped to a second line as one unit once combined crumb+topbar content
 * exceeded the row (witnessed live on the search kit at 1280px: crumb
 * 564px + topbar 596px overflowed a 1184px content box, wrapping the
 * topbar under the crumb and reading as a broken sticky header). Letting
 * the topbar itself shrink means the search input actually compresses
 * first, keeping both bars on one line at realistic content widths. */
.ds-247420 .app-chrome > .app-topbar { order: 2; flex: 1 1 auto; min-width: 0; justify-content: flex-end; }
.ds-247420 .app-chrome > .app-topbar > .brand { display: none; }
.ds-247420 .app-chrome > .app-topbar > nav { margin-left: 0; }
.ds-247420 .app-chrome > .app-crumb > .crumb-right { margin-left: auto; }
.ds-247420 .app-topbar > .brand { flex: 0 0 auto; }
.ds-247420 .app-topbar > .app-search { flex: 1 1 auto; }
/* SearchInput() passed directly as Topbar's search prop renders with no
   Topbar-owned wrapper (see Topbar's isElement branch). Its own outer span
   (.ds-search-input-wrap) is display:contents, so .ds-search-field becomes
   the actual direct .app-topbar flex-item child — an inline-flex box that
   otherwise shrinks to its content and starves the input's own width:100%
   of anything to grow into, truncating the placeholder. Matches
   .app-search's flex-basis above so both search-slot shapes size the same. */
.ds-247420 .app-topbar > .ds-search-field { flex: 1 1 auto; max-width: 360px; min-width: 0; }
.ds-247420 .app-topbar > nav { margin-left: auto; }
/* Utility cluster grouping: the theme toggle sits after the repo-info nav
   links with no visual separation between the two groups, so the row read
   as one undifferentiated strip of controls. A thin inset rule plus extra
   left gap reads as "these are a distinct utility group", matching the
   subtle divider already used between .app-status segments. */
.ds-247420 .app-topbar > .app-topbar-theme {
  flex: 0 0 auto;
  margin-left: var(--space-2);
  padding-left: var(--space-3);
  position: relative;
}
.ds-247420 .app-topbar > .app-topbar-theme::before {
  content: ''; position: absolute; left: 0; top: 50%;
  transform: translateY(-50%);
  width: 1px; height: 18px;
  background: var(--rule-strong);
}
.ds-247420 .app-topbar nav { display: flex; gap: var(--space-1); font-size: var(--fs-sm); flex-wrap: wrap; }
.ds-247420 .app-topbar nav a { flex: 0 0 auto; }
.ds-247420 .app-topbar nav a {
  color: var(--fg-2);
  /* Dense desktop chrome (was a 44px pill with 12px 14px pad — marketing-scale
     touch target in the top bar): --r-1 rounded rect, tighter padding. The 44px
     floor is UNCONDITIONAL, not behind @media(pointer:coarse) — switch and
     head-pointer users drive a fine pointer and are never reported as coarse,
     so a coarse-gated floor leaves them a 39px target. Measured live at 1920:
     the gated form rendered 39px with pointer:coarse false. Padding stays dense;
     only the floor moves, so the compact chrome reads unchanged. */
  padding: var(--space-2) var(--space-2-5);
  min-height: 44px;
  display: flex;
  align-items: center;
  border-radius: var(--r-1);
  transition: background var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease);
}
.ds-247420 .app-topbar nav a:hover { background: var(--bg-2); color: var(--fg); }
.ds-247420 .app-topbar nav a:focus-visible {
  outline: var(--focus-w) solid var(--focus-color);
  outline-offset: var(--focus-offset);
}
/* RESTYLED: borderless per explicit user request -- the active tab used to
   carry an inset box-shadow underline; that decorative line is gone. Active
   state is now conveyed by text color + weight alone (--accent-ink, 600),
   consistent with tonal-surfaces-over-borders and readable without relying
   on any drawn line. */
.ds-247420 .app-topbar nav a.active {
  color: var(--accent-ink);
  font-weight: 600;
}

.ds-247420 .brand {
  font-family: var(--ff-display);
  font-weight: 700; font-size: var(--fs-lg);
  letter-spacing: var(--tr-tight);
}
/* The wordmark separator printed in the electric lead — the brand tell. */

.ds-247420 .app-search {
  display: inline-flex; align-items: center; gap: var(--space-2-5);
  /* Snap to the single-line control ladder (--field-height == --ctl-md, 34px,
     density-scaled) so the topbar search aligns with sibling topbar controls and
     shrinks under [data-density]. Was raw 9px 16px padding off the ladder — the
     one front-door input the control-height port missed. */
  height: var(--field-height);
  padding: 0 var(--space-3); background: var(--bg-2);
  border-radius: var(--r-pill);
  font-size: var(--fs-sm); color: var(--fg-3);
  max-width: 360px;
  /* Match the system-wide input border language (.ds-field / .ds-search-input:
     tonal fill + printed baseline rule) instead of a borderless pill, so the
     topbar search reads as the same control family as every other input. */
  box-shadow: inset 0 0 0 var(--bw-hair, 1px) var(--rule),
              inset 0 calc(-1 * var(--bw-rule)) 0 var(--rule-strong);
  transition: box-shadow var(--dur-snap) var(--ease), background var(--dur-snap) var(--ease);
}
.ds-247420 .app-search .icon { opacity: 0.6; }
.ds-247420 .app-search input {
  border: 0; background: transparent;
  font-family: inherit; font-size: inherit; color: var(--fg);
  width: 100%;
}
/* Same focus language as .ds-field/.ds-search-input: no outline ring, the
   baseline rule thickens to the accent instead — matching every other form
   input in the system rather than a one-off outline ring. */
.ds-247420 .app-search:focus-within {
  background: var(--bg);
  box-shadow: inset 0 0 0 var(--bw-hair, 1px) var(--rule),
              inset 0 calc(-1 * var(--bw-chunk)) 0 var(--accent);
}

.ds-247420 .app-crumb {
  min-height: var(--app-crumb-h);
  padding: var(--space-1-75) var(--pad-x);
  display: flex; align-items: center; gap: var(--space-2);
  font-size: var(--fs-sm); color: var(--fg-3);
  min-width: 0;
}
.ds-247420 .app-crumb .sep { opacity: 0.5; flex: 0 0 auto; }
/* Trail segments and the current/leaf segment truncate independently instead
   of the whole crumb bar overflowing — a long segment (deep file path, long
   title) must not push the sibling segments or the right slot off-screen. */
.ds-247420 .app-crumb > span:not(.sep):not(.crumb-right) {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  min-width: 0; flex: 0 1 auto;
}
/* Ancestor trail segments previously had no rule of their own and simply
   inherited .app-crumb's base --fg-3 — the same tone the separators sit on
   top of (before their own 0.5 opacity dims them further). That leaves only
   two visually distinct tiers in practice (dim separator, bold leaf) rather
   than three, since the ancestor tone reads as "default text colour", not a
   deliberate middle step. --fg-3 is the crumb bar's OWN base, so explicitly
   restating it here is the anchor for the 3-tier read: separator (--fg-3 at
   0.5 opacity, dimmest) < ancestor (--fg-3, mid) < leaf (--fg bold, brightest). */
.ds-247420 .app-crumb > span:not(.sep):not(.leaf):not(.crumb-right) { color: var(--fg-3); }
.ds-247420 .app-crumb .leaf { color: var(--fg); font-weight: 600; flex: 0 1 auto; }
.ds-247420 .app-crumb .crumb-right { margin-left: auto; color: var(--fg-3); flex: 0 0 auto; }

/* The shell is the grid cell (.app-body's fixed sidebar column); it must own
   its own scroll, not just size to content, because .app-body's row track is
   `minmax(0, 1fr)` — a real height the sidebar can overflow once bins +
   labels + more exceed it. Without this the extra rows were silently chopped
   at the viewport edge with no way to reach them. */
/* .app-side-shell and .app-main (below) are two INDEPENDENT scroll regions
   side by side -- correct by design (a sidebar nav and a main panel each own
   their own overflow), but with default invisible/overlay OS scrollbars a
   user hovering the boundary has no visual cue which region the wheel will
   scroll, or that there even are two scrollable zones rather than one page
   scroll. Thin always-visible scrollbar tracks make each region's own scroll
   ownership legible without changing the (correct) two-region architecture. */
.ds-247420 .app-side-shell {
  /* RESTYLED: a lone border-right (removed below) read as an accidental
     floating line rather than deliberate framing, since the sidebar's
     background was identical to the page's and nothing else on the page had
     a matching border for visual balance. A tonal-surface fill (--bg-2, one
     step off the page background) makes the split-scroll boundary legible
     the same way every other panel/card edge in this system already is --
     via a fill difference, not a hairline -- consistent with the
     tonal-surfaces-over-borders principle instead of contradicting it. */
  background: var(--bg-2); overflow-y: auto; min-height: 0;
  scrollbar-width: thin; scrollbar-color: var(--rule-strong) transparent;
  /* Bottom-edge fade: a rail longer than the viewport otherwise shows only
     the thin scrollbar track as its sole "there is more below" cue. Mirrors
     the horizontal fade already used by .os-instances (src/kits/os/theme.css)
     for its own overflow edge, oriented top-to-bottom for this vertical
     scroller. The top edge stays fully opaque (rows start flush under the
     topbar with no need to hint upward scroll on initial load); only the
     trailing 24px softens toward transparent. */
  -webkit-mask-image: linear-gradient(to bottom, black calc(100% - 24px), transparent);
  mask-image: linear-gradient(to bottom, black calc(100% - 24px), transparent);
}
.ds-247420 .app-side-shell::-webkit-scrollbar { width: 8px; }
.ds-247420 .app-side-shell::-webkit-scrollbar-thumb { background: var(--rule-strong); border-radius: var(--r-pill, 999px); }
.ds-247420 .app-side-shell::-webkit-scrollbar-track { background: transparent; }
.ds-247420 .app-body { display: grid; grid-template-columns: var(--sidebar-width) minmax(0, 1fr); grid-template-rows: minmax(0, 1fr); align-items: stretch; align-content: stretch; flex: 1; min-height: 0; }
.ds-247420 .app-body.no-side { grid-template-columns: minmax(0, 1fr); }
/* When there is no sidebar, the empty side-shell must not occupy a grid row,
   otherwise main wraps to a second (collapsed) row. Remove it from layout. */
.ds-247420 .app-body.no-side .app-side-shell { display: none; }

.ds-247420 .app-side {
  display: flex; flex-direction: column; gap: var(--space-4);
  padding: var(--space-5) var(--space-2) var(--space-5) var(--space-3);
  font-size: var(--fs-sm);
}
/* Sidebar section wrapper emitted by Side() (src/components/shell/app-shell.js).
   It is the `role="group"` that owns the heading + its links, so it must be the
   thing that groups them visually too: without a rule it was a bare block, and
   the only separation between sections came from .app-side's own flex gap —
   which meant the heading floated equidistant between the group it labels and
   the one above it. Stacking the group's own children tightly re-attaches the
   heading to its links. */
.ds-247420 .app-side-group { display: flex; flex-direction: column; gap: var(--space-hair); }
.ds-247420 .app-side .group {
  font-size: var(--fs-xs); font-weight: 600;
  color: var(--fg-3); padding: 0 var(--space-3);
  margin-bottom: var(--space-1-75);
}
.ds-247420 .app-side a {
  display: grid; grid-template-columns: 18px 1fr auto;
  gap: var(--space-2-5); align-items: center;
  /* Dense rounded-rect nav row (was a 44px pill with 12px 14px padding — tall
     and consumer-shaped). A --r-1 rounded rect reads as compact app chrome.
     The 44px floor is UNCONDITIONAL for the same reason as .skip-link's: a
     pointer:coarse gate misses switch and head-pointer users, who report a fine
     pointer. Measured live at 1920 the gated form rendered 39px. Padding is
     unchanged, so the row stays dense — only the floor moves. */
  padding: var(--space-2) var(--space-2-5);
  min-height: 44px;
  border-radius: var(--r-1);
  color: var(--fg-2);
  margin-bottom: var(--space-hair);
}
@media (pointer: coarse) {
  .ds-247420 .app-side a { min-height: 44px; padding: var(--space-2-75) var(--space-3); }
}
/* A sidebar row with no href and no handler is a static label, not a control:
   Side() emits it as an <a> only so it keeps the grid layout above. It must not
   claim to be pressable — no pointer, no hover highlight. (An anchor without
   href is already skipped by the tab order and exposed as generic, not link.) */
.ds-247420 .app-side a:not([href]) { cursor: default; }
/* An entry naming a section/tag that does not exist yet — in the sidebar tree
   or the topbar nav. It stays visible so the intended shape of the docs is
   legible, but it is not a link and must not read as one: dimmed, and inert
   like any other href-less anchor. Both selectors are needed because the two
   navs style their anchors separately. */
.ds-247420 .app-side a.is-unwritten,
.ds-247420 .app-topbar nav a.is-unwritten { color: var(--fg-3); cursor: default; }
.ds-247420 .app-topbar nav a.is-unwritten:hover { background: none; color: var(--fg-3); }
.ds-247420 .app-side a[href]:hover { background: var(--bg-2); color: var(--fg); }
.ds-247420 .app-side a:focus-visible {
  outline: var(--focus-w) solid var(--focus-color);
  outline-offset: var(--focus-offset);
}
.ds-247420 .app-side a.active {
  color: var(--fg);
  background: var(--bg-3);
  font-weight: 500;
}
.ds-247420 .app-side .count {
  background: color-mix(in oklab, var(--fg) 10%, transparent);
  color: var(--fg-2);
  padding: 3px 10px; font-size: var(--fs-micro); font-weight: 700;
  border-radius: var(--r-pill);
}
/* Active state is a flat neutral highlight (--bg-3/--fg), not an accent
   fill -- no hue, just a perceivable shade step up from the plain a[href]:hover
   (--bg-2). --fg over --bg-3 is the same body-text pairing used everywhere
   else in the sidebar, so it inherits that AA contrast for free instead of
   depending on a per-[data-accent] retune. The count chip stays on its
   default neutral pairing above; there is no accent-on-ink inversion to
   double down on anymore. */
.ds-247420 .app-side a.active .count {
  background: color-mix(in oklab, var(--fg) 16%, transparent);
  color: var(--fg-2);
}

.ds-247420 .app-main {
  /* Bottom padding reserves clearance equal to the status bar's own height so
     the last scrolled child never sits flush behind .app-status (a flex
     sibling pinned below .app-main in the .app column, not a child of it).
     --app-status-h-live tracks the bar's collapsed/expanded state (set by
     the toggle in app-shell.js) and falls back to the full bar height when
     unset (bar starts expanded / JS hasn't run yet). */
  padding: var(--space-5) var(--pad-x) var(--app-status-h-live, var(--app-status-h));
  min-width: 0;
  /* Full-height flex column so a single flex:1 child (e.g. .chat) fills the
     region between crumb and status instead of floating at content height. */
  display: flex; flex-direction: column;
  min-height: 0;
  align-self: stretch;
  height: 100%;
}
/* Document children (hero, sections, panels) keep their natural height in the
   scrolling flex column. Without flex-shrink:0 the flex algorithm shrinks them
   toward min-height:0 once total content exceeds the fixed-height .app-main,
   collapsing every section to 0 and piling content on top of itself — the
   failure mode any long-scroll page (a marketing/landing route) hits. Grow
   children (.chat/.grow/etc.) re-assert flex:1 1 auto below via higher
   specificity, so a single full-height pane still fills the region. */
.ds-247420 .app-main > * { min-height: 0; flex: none; }
/* The main region scrolls its own overflow at every breakpoint (previously
   only ≥901px), so a fixed-height .app never clips route content and inner
   panels don't fight the page scroll. */
.ds-247420 .app-main { overflow-y: auto; }
/* Reserve the scrollbar track so routes with/without overflow don't shift
   the layout horizontally when the bar appears. */
.ds-247420 .app-main { scrollbar-gutter: stable; }
/* Same visible-scroll-ownership rationale as .app-side-shell above -- makes
   this the visually distinct SECOND independent scroll region rather than
   an invisible-scrollbar mystery next to the sidebar. */
.ds-247420 .app-main {
  scrollbar-width: thin; scrollbar-color: var(--rule-strong) transparent;
}
.ds-247420 .app-main::-webkit-scrollbar { width: 8px; }
.ds-247420 .app-main::-webkit-scrollbar-thumb { background: var(--rule-strong); border-radius: var(--r-pill, 999px); }
.ds-247420 .app-main::-webkit-scrollbar-track { background: transparent; }
/* Jump-target anchors clear the inner-scroll top edge (no chrome overlap,
   but a little breathing room when deep-linked within .app-main). */
.ds-247420 .app-main [id] { scroll-margin-top: var(--space-4); }
/* Full-height route children fill the region instead of scrolling the page.
   .aicat-focus-col was missing from this list: .app-main > * defaults every
   direct child to flex:none (content height) a few rules up, so without a
   grant here .chat-thread inside it never becomes the actual scrolling
   ancestor -- .app-main scrolls instead, .chat-thread grows to full content
   height, and thread-scroll.js's IntersectionObserver (rooted at
   .chat-thread) never fires because .chat-thread never overflows. New
   messages landed off-screen with no auto-scroll, live-confirmed via CDP. */
.ds-247420 .app-main > .chat,
.ds-247420 .app-main > .chat-area,
.ds-247420 .app-main > .chat-kit-page,
.ds-247420 .app-main > .aicat-focus-col,
.ds-247420 .app-main > .ds-file-stage,
.ds-247420 .app-main > .ds-files-stack,
.ds-247420 .app-main > .grow { flex: 1 1 auto; min-height: 0; }
/* width:100% is load-bearing, not redundant with max-width: .app-body is a
   CSS grid, and margin:auto on a grid item overrides the default stretch,
   shrinking the item to its max-content width instead of filling the track
   and then clamping -- confirmed live at 2560px viewport where mainWidth
   measured 688px against a correctly-resolved 1400px max-width. Explicit
   width:100% restores fill-then-cap for every .narrow page, not just wide
   viewports (the shrink-to-fit bug undersized even the base 760px case). */
.ds-247420 .app-main.narrow { width: 100%; max-width: var(--measure-narrow); margin: 0 auto; }
/* .narrow means "prose reading measure", not "shrink the whole shell on wide
   screens". The old clamp(760px, 46vw, 1000px) grew too slowly to track the
   real available column: at 1920px viewport the .app-body grid's second
   track (.app width minus --sidebar-width) is ~1556px, but 46vw=883px sat
   well under the 1000px ceiling, leaving a ~340px dead gutter between the
   sidebar and the centered narrow column on an ordinary desktop screen (not
   just ultrawide) -- confirmed live via a real 1920x1080 CDP viewport, a
   project page (#/p/<slug>). 62vw tracks the actual column growth far closer
   (1920*0.62=1190px, much nearer the 1556px track than 883px was) while the
   1200px ceiling still keeps prose from stretching into an unreadable
   full-bleed line -- sidebar-present narrow pages read the same width class
   as the no-side variant below instead of visibly starving next to it. */
.ds-247420 .app-body:not(.no-side) .app-main.narrow {
  max-width: clamp(var(--measure-narrow), 62vw, 1200px);
}
/* A shell with no sidebar has no competing column, so the prose clamp there was
   leaving the whole sidebar's worth of width empty -- 760px of content inside a
   1413px shell on a real page. Widen the clamp by the space the absent sidebar
   would have taken so the measure still reads as prose, just not starved.
   The floor stays a fixed 980px (no-side is still readable narrow); the
   ceiling grows with viewport instead of staying pinned at 980px forever --
   on a 2560px ultrawide the old fixed value left over half the shell as dead
   side margin. 1400px ceiling keeps prose measure sane (nobody wants a
   2400px-wide paragraph) while letting non-prose content (index rows, meta)
   actually use the extra room. 60vw sat below the 1016px floor on every
   viewport under ~1693px, so ordinary desktop widths (1440, 1536) always
   resolved to the bare floor -- a live CDP measurement at 1440x900 found a
   ~424px dead gutter (212px each side) around the blog's narrow column,
   read by users as "not filling the page". 74vw crosses the floor around
   1373px so real desktop widths actually grow past it, same reasoning as
   the sidebar variant above. */
.ds-247420 .app-body.no-side .app-main.narrow {
  max-width: clamp(calc(var(--measure-narrow) + var(--sidebar-width)), 74vw, 1400px);
}
/* Article-body heading rhythm and lede spacing, consumed by flatspace-rendered
   long-form pages (docs, papers) that drop headings/panels directly into .app-main. */
.ds-247420 .app-main > h1,
.ds-247420 .app-main > h2,
.ds-247420 .app-main > h3 { margin-top: 36px; margin-bottom: 12px; }
.ds-247420 .app-main > h1:first-child { margin-top: 8px; }
.ds-247420 .app-main > .panel,
.ds-247420 .app-main > .work-detail-chips,
.ds-247420 .app-main > .cli { margin-top: 18px; margin-bottom: 18px; }
.ds-247420 .app-main .ds-lede { margin-top: 4px; margin-bottom: 18px; max-width: 64ch; line-height: 1.6; }
.ds-247420 .work-detail-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; row-gap: 10px; }
@media (max-width: 720px) {
  .ds-247420 .app-main > h1,
  .ds-247420 .app-main > h2,
  .ds-247420 .app-main > h3 { margin-top: 28px; }
}

@media (min-width: 1400px) {
  /* No outer ceiling: .app tracks 100vw at every width, including past 1920px
     and 2560px+ ultrawide displays, so the shell always fills the real screen
     edges instead of leaving dead margin on either side. Row/prose measure
     caps (--measure-wide, --stage-wide, 64ch, etc.) own line-length
     readability independently of the shell's own width, so letting .app grow
     unbounded does not create unreadable rows -- text still wraps at its own
     cap; only the surrounding chrome (panels, sidebar, status bar) stretches
     to use the extra space. Measured live at 2560px: the prior 1920px cap
     left a symmetric 320px dead margin on each side (640px total) -- this was
     the root cause of pages appearing to "shrink to content" rather than fill
     the screen on wide monitors. */
  .ds-247420 .app { width: 100%; margin-left: auto; margin-right: auto; }
  .ds-247420 .app-main .chat,
  .ds-247420 .app-main > .chat-area,
  .ds-247420 .app-main > .main-content { max-width: none; margin: 0; width: 100%; }
  /* Status bar spans the full-width .app edge-to-edge; no separate width
     constraint (which previously narrowed it). */
  .ds-247420 .app-status { width: 100%; }
  /* Cap row text measure inside the wide main (ultrawide readability). */
  .ds-247420 .row .title, .ds-247420 .row .sub { max-width: var(--measure-wide); }
}

.ds-247420 .app-status {
  display: flex; align-items: center; gap: var(--space-3);
  width: 100%;
  height: var(--app-status-h); min-height: var(--app-status-h);
  /* A thin desktop status strip, not a webpage footer band. */
  padding: 0 var(--space-4);
  /* Status chrome is prose, not code - mono is reserved for code/paths. */
  font-family: var(--ff-body); font-size: var(--fs-xs); line-height: 1.2;
  color: var(--fg-3);
  border-top: 1px solid var(--rule);
}
.ds-247420 .app-status .item { color: inherit; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; display: flex; align-items: center; gap: var(--space-3); }
.ds-247420 .app-status .item:first-of-type { color: var(--accent-ink); }
/* Subtle divider between adjacent status segments — a thin inset rule, not a
   full-height border, so it reads as a quiet separator rather than boxing
   each item. */
.ds-247420 .app-status .item:not(:first-of-type)::before {
  content: ''; flex: 0 0 auto;
  width: 1px; height: 10px;
  background: var(--rule-strong);
}
.ds-247420 .app-status .spread { flex: 1; }
/* The status bar NEVER wraps: at narrow widths the trailing items (hints,
   agent target) drop in priority order instead of breaking mid-word onto a
   second line. */
.ds-247420 .app-status { flex-wrap: nowrap; overflow: hidden; }

/* Collapse control — the status bar permanently claims --app-status-h of
   viewport on every page; this toggle lets it shrink to a thin reachable
   strip instead. The bar itself stays a fixed-height flex child (never
   display:none) so the toggle remains reachable to re-expand. */
.ds-247420 .app-status-toggle {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  /* 24px matches this codebase's established icon-only-control floor
     (.ds-chip-remove-btn, .ds-avatar-sm) — 18px measured under the
     interactive-target minimum every other micro-control here already
     clears. */
  width: 24px; height: 24px; padding: 0; margin-right: var(--space-1);
  background: transparent; border: none; color: var(--fg-3); cursor: pointer;
  border-radius: var(--r-0);
}
.ds-247420 .app-status-toggle:hover { color: var(--fg); background: var(--bg-3); }
.ds-247420 .app-status-toggle:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
.ds-247420 .app-status.is-collapsed {
  height: var(--space-4); min-height: var(--space-4);
  overflow: hidden;
}
.ds-247420 .app-status.is-collapsed .item,
.ds-247420 .app-status.is-collapsed .spread { display: none; }
@media (max-width: 1100px) {
  .ds-247420 .app-status .item:last-of-type:not(:only-of-type) { display: none; }
}

/* The "dateline" — ASCII bar used at the top of every preview/brand page.
   3-4 flex slots; a .spread filler pushes the trailing slot to the far
   edge instead of every span running together with no gap. */
.ds-247420 .dateline {
  display: flex; align-items: center; gap: var(--space-3);
  flex-wrap: nowrap; overflow: hidden;
  font-family: var(--ff-mono); font-size: var(--fs-xs); line-height: 1.2;
  letter-spacing: 0.02em;
  color: var(--fg-3);
}
.ds-247420 .dateline span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
/* The spreader itself only ever set flex-basis -- an empty div with no
   background renders as blank space, not the "dotted spreader" the preview
   page's own caption describes. border-bottom (not background-image) keeps
   the dots crisp at 1px without a repeating-gradient banding artifact. */
.ds-247420 .dateline .spread {
  flex: 1 1 auto; min-width: var(--space-3); align-self: stretch;
  display: flex; align-items: center;
}
.ds-247420 .dateline .spread::after {
  content: ''; flex: 1 1 auto;
  border-bottom: var(--bw-hair, 1px) dotted var(--rule-strong);
}

.ds-247420 .btn, .ds-247420 .btn-primary, .ds-247420 .btn-ghost {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  height: var(--ctl-md); min-height: var(--ctl-md);
  padding: 0 var(--space-2-75);
  font-family: var(--ff-body);
  font-weight: 600; font-size: var(--fs-sm);
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  border-radius: var(--r-1);
  border: var(--bw-hair) solid transparent;
  box-shadow: none;
  transition: transform var(--dur-base) var(--ease-spring), background var(--dur-snap) var(--ease),
    color var(--dur-snap) var(--ease), box-shadow var(--dur-base) var(--ease-spring),
    border-color var(--dur-snap) var(--ease);
}
.ds-247420 .btn { background: var(--bg-2); color: var(--fg); border-color: var(--rule); }
.ds-247420 .btn:hover { background: var(--bg-3); color: var(--fg); transform: translateY(-1px); box-shadow: var(--shadow-1); }
.ds-247420 .btn-primary {
  background: var(--accent); color: var(--accent-fg);
  font-weight: 600;
}
.ds-247420 .btn-primary:hover { background: var(--fg); color: var(--bg); transform: translateY(-1px); box-shadow: var(--shadow-2); }
.ds-247420 .btn-ghost { background: transparent; color: var(--fg); box-shadow: inset 0 0 0 var(--bw-hair) var(--fg); }
.ds-247420 .btn-ghost:hover { background: color-mix(in oklab, var(--fg) 12%, transparent); transform: translateY(-1px); }
.ds-247420 .btn:active, .ds-247420 .btn-primary:active, .ds-247420 .btn-ghost:active { transform: translateY(1px); box-shadow: none; }
/* Disabled state: --fg-3 (the tuned tertiary-text token, AA-clear against
   every panel tier) instead of a blind opacity halving -- opacity multiplies
   the SURROUNDING contrast too, so a light-on-dark button can drop under 3:1
   even though "disabled" only needs to stay legible, not full-strength. */
.ds-247420 .btn:disabled, .ds-247420 .btn-primary:disabled, .ds-247420 .btn-ghost:disabled, .ds-247420 .btn.is-disabled, .ds-247420 .btn-primary.is-disabled, .ds-247420 .btn-ghost.is-disabled {
  background: var(--bg-2); color: var(--fg-3); border-color: var(--rule);
  box-shadow: none; cursor: not-allowed; pointer-events: none;
}
.ds-247420 .btn:focus-visible, .ds-247420 .btn-primary:focus-visible, .ds-247420 .btn-ghost:focus-visible, .ds-247420 .ds-icon-btn:focus-visible {
  outline: var(--focus-w) solid var(--focus-color);
  outline-offset: var(--focus-offset);
}

.ds-247420 .btn-sm, .ds-247420 .btn-sm.btn-primary, .ds-247420 .btn-sm.btn-ghost {
  height: var(--ctl-sm); min-height: var(--ctl-sm);
  padding: 0 var(--space-2); font-size: var(--fs-tiny); gap: var(--space-1);
}

/* Link-styled button: an underline (not just color) is the affordance that
   separates "this navigates" from "this triggers an action" -- .btn-ghost's
   outline reads as a secondary ACTION button, not a link, at a glance. */
.ds-247420 .btn-link {
  display: inline-flex; align-items: center; gap: var(--space-1);
  background: none; border: none; box-shadow: none; padding: 0;
  height: auto; min-height: 0;
  font-family: var(--ff-body); font-weight: 600; font-size: var(--fs-sm);
  color: var(--accent-ink); text-decoration: underline; text-underline-offset: 3px;
  cursor: pointer;
}
.ds-247420 .btn-link:hover { color: var(--fg); }
.ds-247420 .btn-link:focus-visible {
  outline: var(--focus-w) solid var(--focus-color);
  outline-offset: var(--focus-offset);
}
.ds-247420 .btn-lg, .ds-247420 .btn-lg.btn-primary, .ds-247420 .btn-lg.btn-ghost {
  height: var(--ctl-lg); min-height: var(--ctl-lg);
  padding: 0 var(--space-4); font-size: var(--fs-body); gap: var(--space-2);
}

/* App chrome is compact/dense (see --fs-h1-app etc in colors_and_type.css) --
   the marketing-scale hover lift/shadow reads as an oversized flourish at
   this density, so app-typescale buttons opt out of it deliberately. */
.ds-247420[data-typescale="app"] .btn:hover,
.ds-247420[data-typescale="app"] .btn-primary:hover,
.ds-247420[data-typescale="app"] .btn-ghost:hover { transform: none; box-shadow: none; }
.ds-247420[data-typescale="app"] .btn:active,
.ds-247420[data-typescale="app"] .btn-primary:active,
.ds-247420[data-typescale="app"] .btn-ghost:active { transform: none; }
.ds-247420[data-typescale="app"] .panel { transition: box-shadow var(--dur-snap) var(--ease), border-color var(--dur-snap) var(--ease); }
.ds-247420[data-typescale="app"] a.row:hover .meta,
.ds-247420[data-typescale="app"] .row[role="button"]:hover .meta { transform: none; }

.ds-247420 .ds-icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0; border: none; cursor: pointer;
  border-radius: var(--r-1, 8px);
  background: transparent; color: var(--fg);
  width: 32px; height: 32px;
  transition: background var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease);
}
.ds-247420 .ds-icon-btn:hover { background: var(--bg-2); }
.ds-247420 .ds-icon-btn-xs { width: 22px; height: 22px; }
.ds-247420 .ds-icon-btn-sm { width: 26px; height: 26px; }
.ds-247420 .ds-icon-btn-base { width: 32px; height: 32px; }
.ds-247420 .ds-icon-btn-lg { width: 40px; height: 40px; }
.ds-247420 .ds-icon-btn-xl { width: 48px; height: 48px; }
@media (pointer: coarse) {
  .ds-247420 .ds-icon-btn-xs { width: 32px; height: 32px; }
  .ds-247420 .ds-icon-btn-sm { width: 32px; height: 32px; }
}
.ds-247420 .ds-icon-btn-ghost { background: transparent; }
.ds-247420 .ds-icon-btn-ghost:hover { background: var(--fg); color: var(--bg); }
.ds-247420 .ds-icon-btn-primary { background: var(--accent); color: var(--accent-fg); }
.ds-247420 .ds-icon-btn-primary:hover { background: var(--fg); color: var(--accent-ink); }
/* --warn-fg (not a bare --ink/--on-color literal): --ink only clears AA
   against dark theme's lighter --warn (#FF5A52, 6.27:1); light theme's
   --warn is a darker red (#C41C0C) where --ink measures just 2.92:1, well
   under the 4.5:1 floor. --warn-fg is the theme-paired token (light: paper
   text 5.97:1, dark: ink text 6.27:1) -- same fix as .btn-primary.danger in
   app-shell/files.css, which had this exact light-theme regression live. */
.ds-247420 .ds-icon-btn-danger { background: var(--warn); color: var(--warn-fg); }
.ds-247420 .ds-icon-btn-danger:hover { filter: brightness(0.92); }
.ds-247420 .ds-icon-btn:active { transform: translateY(1px); }
.ds-247420 .ds-icon-btn:disabled, .ds-247420 .ds-icon-btn.is-disabled {
  opacity: 0.5; cursor: not-allowed; pointer-events: none;
}

.ds-247420 .ds-badge {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 18px; height: 18px; padding: 0 6px;
  font-size: var(--fs-micro); font-weight: 600; line-height: 1;
  border-radius: var(--r-pill);
  background: var(--bg-3); color: var(--fg-2);
}
/* --fs-nano (11px), not --fs-pico (10px): --fs-pico sits below the 11px
   legibility floor other small-scale labels in this system respect (see
   .ds-stat-lbl, .ds-session-row-id), and this size is used live for the
   workspace rail item-count badge (atoms.js Badge size="sm"). */
.ds-247420 .ds-badge.ds-badge--sm { min-width: var(--icon-xs); height: var(--icon-xs); padding: 0 var(--space-1); font-size: var(--fs-nano); }
.ds-247420 .ds-badge.ds-badge--lg { min-width: 22px; height: 22px; padding: 0 var(--space-2); font-size: var(--fs-tiny); }
.ds-247420 .ds-badge.tone-green, .ds-247420 .ds-badge.tone-success {
  background: var(--green-tint); color: var(--green-deep);
}
/* tone-live is its own hue (sky), split out of the success/green group:
   "live/broadcasting now" and "operation succeeded" are different facts and
   read identically when both borrow the brand's accent green — a LIVE badge
   next to an "ok" chip must not look like the same state. --sky is already
   used for informational surfaces (ds-alert-info, audio-file icons) and is
   never pinned by the thebird brand's `--accent:var(--green)!important`
   override, so it stays visually distinct under every accent preset. */
.ds-247420 .ds-badge.tone-live {
  background: color-mix(in oklab, var(--sky) 22%, var(--bg)); color: var(--sky);
}
.ds-247420 .ds-badge.tone-flame, .ds-247420 .ds-badge.tone-error {
  background: var(--flame); color: var(--on-color);
}
.ds-247420 .ds-badge.tone-wip, .ds-247420 .ds-badge.tone-neutral {
  background: var(--bg-3); color: var(--fg-2);
}
.ds-247420 .ds-badge.tone-purple  { background: var(--purple-tint); color: var(--purple-deep); }
.ds-247420 .ds-badge.tone-mascot  { background: var(--mascot-tint); color: var(--ink); }
.ds-247420 .ds-badge.tone-sun, .ds-247420 .ds-badge.tone-yellow {
  background: var(--sun); color: var(--ink);
}
.ds-247420 .ds-badge.tone-blue    { background: var(--accent-tint); color: var(--accent-ink); }
.ds-247420 .ds-badge.tone-orange  { background: color-mix(in oklab, var(--flame) 22%, var(--bg)); color: var(--flame); }

.ds-247420 .chip {
  display: inline-flex; align-items: center; gap: var(--space-1-75);
  padding: 3px 10px;
  background: var(--bg-2); color: var(--fg-2);
  font-family: var(--ff-body);
  font-size: var(--fs-tiny); font-weight: 600;
  letter-spacing: var(--tr-caps); text-transform: uppercase;
  border-radius: var(--r-pill);
}
.ds-247420 .chip.chip--sm { padding: var(--space-half) var(--space-2); font-size: var(--fs-micro); gap: var(--space-1); }
.ds-247420 .chip.chip--lg { padding: var(--space-1-75) var(--space-2-75); font-size: var(--fs-xs); }
.ds-247420 .chip.chip--tag {
  text-transform: none; letter-spacing: 0; font-weight: 500;
  border-radius: var(--r-0); padding: var(--space-half) var(--space-2);
}
.ds-247420 .chip.tone-green, .ds-247420 .chip.tone-success, .ds-247420 .chip.tone-ok {
  background: var(--green-tint); color: var(--green-deep);
}
/* tone-live: its own sky hue, split from tone-ok/tone-success — see the
   .ds-badge.tone-live comment above for the rationale. A small pulsing dot
   precedes the label so "live" reads as an active broadcast state, not a
   static status chip. */
.ds-247420 .chip.tone-live {
  background: color-mix(in oklab, var(--sky) 16%, var(--bg));
  color: var(--sky);
  position: relative;
  padding-left: 22px;
}
.ds-247420 .chip.tone-live::before {
  content: '';
  position: absolute;
  left: 10px; top: 50%;
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--sky);
  transform: translateY(-50%);
}
@media (prefers-reduced-motion: no-preference) {
  .ds-247420 .chip.tone-live::before {
    animation: chip-live-pulse 1.6s ease-in-out infinite;
  }
}
@keyframes chip-live-pulse {
  0%, 100% { opacity: 1; box-shadow: 0 0 0 0 color-mix(in oklab, var(--sky) 50%, transparent); }
  50% { opacity: 0.7; box-shadow: 0 0 0 4px transparent; }
}
@media (prefers-reduced-motion: reduce) {
  .ds-247420 .chip.tone-live::before { animation: none; }
}
.ds-247420 .chip.tone-flame, .ds-247420 .chip.tone-error, .ds-247420 .chip.tone-miss {
  background: var(--flame); color: var(--on-color);
}
.ds-247420 .chip.tone-warn {
  background: color-mix(in oklab, var(--amber) 22%, var(--bg));
  color: var(--amber);
}
.ds-247420 .chip.tone-wip, .ds-247420 .chip.tone-neutral {
  background: var(--bg-3); color: var(--fg-2);
}
.ds-247420 .chip.accent, .ds-247420 .chip.tone-accent {
  background: var(--accent-tint); color: var(--accent-ink);
}
.ds-247420 .chip.tone-dim {
  background: var(--bg-3); color: var(--fg-3);
}
.ds-247420 .chip.tone-purple  { background: var(--purple-tint); color: var(--purple-deep); }
.ds-247420 .chip.tone-mascot  { background: var(--mascot-tint); color: var(--ink); }
.ds-247420 .chip.tone-sun, .ds-247420 .chip.tone-yellow {
  background: var(--sun); color: var(--ink);
}
.ds-247420 .chip.tone-disabled { background: var(--bg-3);        color: var(--fg-3); }
.ds-247420 .chip.tone-blue     { background: var(--accent-tint); color: var(--accent-ink); }
.ds-247420 .chip.tone-orange   { background: color-mix(in oklab, var(--flame) 30%, var(--sun) 70%); color: var(--ink); }

.ds-247420 .chip.ds-chip-removable { padding-right: var(--space-1); }
.ds-247420 .ds-chip-remove-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; margin-left: var(--space-half);
  padding: 0; border: none; border-radius: var(--r-pill);
  background: transparent; color: currentColor; opacity: 0.7;
  cursor: pointer;
}
.ds-247420 .ds-chip-remove-btn:hover, .ds-247420 .ds-chip-remove-btn:focus-visible { opacity: 1; background: color-mix(in oklab, currentColor 16%, transparent); }
.ds-247420 .ds-chip-remove-btn svg { width: var(--icon-xs); height: var(--icon-xs); }
@media (pointer: coarse) {
  .ds-247420 .ds-chip-remove-btn { width: 24px; height: 24px; margin-left: 0; }
  .ds-247420 .ds-chip-remove-btn svg { width: var(--icon-xs); height: var(--icon-xs); }
}

.ds-247420 .ds-pill {
  display: inline-block;
  padding: 1px 7px;
  border-radius: var(--r-pill);
  font-size: var(--fs-micro);
  background: var(--bg-3); color: var(--fg-2);
  margin: 1px;
}
.ds-247420 .ds-pill.tone-accent { background: var(--accent-tint); color: var(--accent-ink); }
.ds-247420 .ds-pill.tone-muted  { background: transparent; color: var(--fg-3); box-shadow: inset 0 0 0 1px var(--rule); }

/* .glyph-* sizes (14/18/22px) are for small inline text-adjacent glyphs
   (single characters/icons sitting in a text flow), a different scale and
   purpose than the .ds-icon-btn-* tap-target scale (22/26/32/40/48px) below --
   the -sm/-lg naming overlap is coincidental, not a shared system. */
.ds-247420 .glyph {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px;
  font-family: var(--ff-mono); font-size: var(--fs-tiny);
  opacity: 0.8;
}
.ds-247420 .glyph { --glyph-size-sm: var(--fs-nano); --glyph-size-base: var(--fs-tiny); --glyph-size-lg: var(--fs-body); }
.ds-247420 .glyph-sm { width: var(--icon-xs); height: var(--icon-xs); }
.ds-247420 .glyph-base { width: 18px; height: 18px; }
.ds-247420 .glyph-lg { width: 22px; height: 22px; }

/* Shared empty-state base -- hero-content.css's .empty and community.css's
   .fd-empty previously duplicated this full rule set; both are joined into
   this selector list so the shared declarations live in one place, with
   each keeping its own file-local rule for context-specific extras
   (background/radius/font-size for .empty; padding for .fd-empty). New
   consumers can add .ds-empty-state directly instead of a fresh copy. */
.ds-247420 .ds-empty-state, .ds-247420 .empty, .ds-247420 .fd-empty {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--space-2);
  padding: var(--space-6) var(--space-3);
  text-align: center;
  color: var(--fg-3);
}
.ds-247420 .ds-empty-state--panel { background: var(--bg-2); border-radius: var(--r-1); }
.ds-247420 .ds-empty-state--compact { padding: var(--space-5) var(--space-3); }

/* .ds-tooltip — shared floating bubble (overlay-primitives/tooltip.js appends
   one instance to <body> and repositions it via useFloating). Fade timing
   matches the component's own 350ms hover-intent delay. */
.ds-247420 .ds-tooltip {
  position: fixed;
  z-index: var(--z-top);
  max-width: 240px;
  padding: var(--space-1) var(--space-2);
  border-radius: var(--r-1);
  background: var(--fg);
  color: var(--bg);
  font-size: var(--fs-xs);
  line-height: 1.35;
  box-shadow: var(--shadow-2);
  pointer-events: none;
  opacity: 0;
  transform: scale(0.96);
  transition: opacity var(--dur-snap) var(--ease), transform var(--dur-snap) var(--ease);
}
.ds-247420 .ds-tooltip:not([hidden]) { opacity: 1; transform: scale(1); }
.ds-247420 .ds-tooltip::after {
  content: '';
  position: absolute;
  width: 6px;
  height: 6px;
  background: var(--fg);
  transform: rotate(45deg);
}
.ds-247420 .ds-tooltip[data-placement^="top"]::after    { bottom: -3px; left: 50%; margin-left: -3px; }
.ds-247420 .ds-tooltip[data-placement^="bottom"]::after { top: -3px; left: 50%; margin-left: -3px; }
.ds-247420 .ds-tooltip[data-placement^="left"]::after   { right: -3px; top: 50%; margin-top: -3px; }
.ds-247420 .ds-tooltip[data-placement^="right"]::after  { left: -3px; top: 50%; margin-top: -3px; }
.ds-247420 .ds-tooltip.kind-danger { background: var(--warn); color: var(--warn-fg); }
.ds-247420 .ds-tooltip.kind-danger::after { background: var(--warn); }
@media (prefers-reduced-motion: reduce) {
  .ds-247420 .ds-tooltip { transition: none; }
}

/* -- Menubar -------------------------------------------------------------
   Row of closed triggers (mirrors .ds-dp-trigger's affordance); without
   this the wrapper and its item divs fall back to block-level UA defaults,
   stacking the triggers vertically and rendering them as filled buttons
   abutting any adjacent status text. */
.ds-247420 .ds-menubar { display: inline-flex; align-items: center; gap: var(--space-1); }
.ds-247420 .ds-menubar-item { display: inline-flex; }
.ds-247420 .ds-menubar-trigger {
  display: inline-flex;
  align-items: center;
  min-height: 32px;
  padding: var(--space-1-5) var(--space-2-5);
  border: 0;
  border-radius: var(--r-2);
  background: var(--bg-2);
  color: var(--fg);
  font-family: inherit;
  font-size: var(--fs-sm);
  cursor: pointer;
  transition: background-color var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease);
}
.ds-247420 .ds-menubar-trigger:hover { background: var(--bg-3); }
.ds-247420 .ds-menubar-trigger:focus-visible { box-shadow: var(--focus-ring-inset); outline: none; }
.ds-247420 .ds-menubar-trigger.is-open { background: var(--bg-3); }
@media (prefers-reduced-motion: reduce) {
  .ds-247420 .ds-menubar-trigger { transition: none; }
}

/* ============================================================
   Panel — soft tonal container, no border decoration
   ============================================================ */
.ds-247420 .panel {
  /* PilotDeck-parity: the reference canvas has NO visible container edge
     anywhere except a single hairline (sidebar only) — page bg and panel
     fill read as the same tone. Stacking a tonal fill + a border + a shadow
     (the previous three-layer treatment) is what critics independently
     flagged as "every card reads as a boxed gray tile" / "grid of gray
     cards" — the single most-repeated, highest-impact gap. Panels are now
     flat (transparent, no border): content groups are separated by
     whitespace and an optional --panel-shadow-1 whisper only, never a fill.
     Use .panel-tonal to opt back into the filled/bordered look for surfaces
     that genuinely need a floating card (e.g. dropdowns, dialogs already
     have their own elevation scale and are unaffected). */
  background: transparent;
  border: none;
  border-radius: var(--r-2);
  /* AGENTS.md "outer 2:1 inner": outer separation is --space-6, inner
     sibling gap --space-4 (48:24 = 2:1), matching .ds-panel-trio/
     .ds-panel-duo. Bumped up one rung from --space-5/--space-3 for more
     PilotDeck-style airy whitespace while preserving the 2:1 ratio. */
  margin: 0 0 var(--space-6);
  padding: var(--space-4);
  position: relative;
  transition: box-shadow var(--dur-base) var(--ease-spring), border-color var(--dur-snap) var(--ease),
    transform var(--dur-base) var(--ease-spring);
}
/* Opt-in tonal/bordered card — the pre-restyle .panel look, kept for
   surfaces that need to read as a discrete floating card rather than a
   text grouping on flat canvas (e.g. a composer input, which PilotDeck
   itself DOES give a faint 1px border+no fill — see .panel-inline below
   for that exact treatment). */
.ds-247420 .panel.panel-tonal {
  background: var(--panel-1, var(--bg-2));
  border: var(--bw-hair) solid var(--rule);
  box-shadow: var(--panel-shadow-1);
}
/* Editorial accent spine — an opt-in printed-ink rule down the left edge that
   reads as a magazine pull-quote bar, not a glow card. */
.ds-247420 .panel.panel-spine {
  border-radius: 0 var(--r-2) var(--r-2) 0;
  padding-left: calc(var(--space-3) + var(--bw-chunk));
}
.ds-247420 .panel.panel-spine::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0;
  width: var(--bw-chunk); background: var(--accent);
  border-radius: var(--bw-chunk) 0 0 var(--bw-chunk);
}
/* PilotDeck's composer is the ONE element with a visible edge on an
   otherwise flat canvas — a faint 1px border, no tonal fill. */
.ds-247420 .panel.panel-inline { background: transparent; border: var(--bw-hair) solid var(--rule); padding: var(--space-3) var(--space-3); }
.ds-247420 .panel.panel-wide   { background: transparent; box-shadow: none; border: none; }
/* Changelog (and any other panel-wide content) needs its OWN container-query
   context: the existing `@container (max-width: 760px) .ds-changelog-row`
   rule in hero-content.css measures the nearest CQ ancestor, which without
   this is `.app`/`.ds-stage` (base.css) — the whole shell width, not this
   panel's actual rendered width. A changelog panel sitting inside
   .ds-panel-trio (three equal siblings) can be far narrower than 760px
   while the shell itself stays well over that, so the row-collapse rule
   never engages and the message column's `minmax(0, 1fr)` track collapses
   to 0 — the text then renders past the panel's right edge, clipped
   mid-word instead of wrapping. Scoping the container here (not on
   .panel generally) keeps the blast radius to panel-wide content only. */
.ds-247420 .panel.panel-wide   { container-type: inline-size; }
/* Dense panel — a compact container for app surfaces (inspector sections,
   sidebars) that want the .panel look without the roomy 16px pad + 24px stack
   gap. Tighter corner + padding + head, so it sits between .panel and the
   flush editor .ds-ep-panel. */
.ds-247420 .panel.panel--dense {
  padding: var(--space-2); margin-bottom: var(--space-2-75);
  border-radius: var(--r-1);
}
.ds-247420 .panel.panel--dense .panel-head { padding: var(--space-1-5) var(--space-2); }
.ds-247420 .panel.panel--dense .panel-body { padding: var(--space-1-5) var(--space-2) var(--space-2); }
.ds-247420 .panel.panel--dense .panel-body > * + * { margin-top: var(--space-2); }
.ds-247420 .panel-head {
  display: flex; align-items: baseline; justify-content: space-between;
  /* Shares the .panel-body 16px side padding so the caps label and the body
     rows sit on ONE left axis (the old 24/32 padding was authored for the
     pre-padding .panel-wide era and indented the label 16px past its content). */
  padding: var(--space-2) var(--space-3);
  font-size: var(--fs-sm); font-weight: 600;
  color: var(--fg-2);
  letter-spacing: 0;
  /* A hairline divider is the header's only structural separator from the
     body — without it, a sentence-case label at fs-sm/600 reads as just
     another dim meta line once the eye drops past the top of the panel. */
  border-bottom: var(--bw-hair) solid var(--rule);
  margin-bottom: var(--space-1);
}
.ds-247420 .panel.panel-wide .panel-head { padding-left: 0; padding-right: 0; }
/* A borderless .panel-wide (no card edge to lean on) would otherwise show a
   divider floating with nothing to visually anchor it — drop it there and
   rely on spacing alone, matching panel-wide's already-transparent chrome. */
.ds-247420 .panel.panel-wide .panel-head { border-bottom: none; margin-bottom: 0; }
.ds-247420 .panel.panel--dense .panel-head { border-bottom: var(--bw-hair) solid var(--rule); margin-bottom: 0; }
.ds-247420 .panel-head > :last-child:not(.ds-badge) {
  font-weight: 500; color: var(--fg-3);
  font-family: var(--ff-mono); letter-spacing: 0;
}
/* Panel titles render as a real heading (h2 by default) for AT heading-jump
   navigation, but must read identically to the old plain span — reset the
   UA heading margin/size/weight so it inherits .panel-head's own type rules
   instead of the browser's h2 stylesheet. */
.ds-247420 .panel-title {
  margin: 0; font: inherit; font-size: inherit; font-weight: inherit;
  letter-spacing: inherit; text-transform: inherit; color: inherit;
}
/* Category glyph beside a panel title (kits / previews / decks / docs on the
   homepage) — same inline-svg vocabulary as icons.js, sized to sit on the
   text baseline rather than dominating it. */
.ds-247420 .ds-panel-title-glyph {
  display: inline-flex; align-items: center; gap: var(--space-1-5, 6px);
}
.ds-247420 .ds-panel-title-glyph .ds-icon { flex-shrink: 0; color: var(--fg-3); }
.ds-247420 .panel-body { padding: var(--space-2) var(--space-3) var(--space-3); }
.ds-247420 .panel-body > * { margin: 0; }
/* One-line explanatory caption under a panel's title — e.g. distinguishing
   two visually-similar panels (lore palette vs semantic tokens) in words
   instead of leaving the reader to infer it from the title alone. */
.ds-247420 .ds-panel-caption {
  font-size: var(--fs-sm); color: var(--fg-3); margin: 0 0 var(--space-2);
}
/* Inner sibling rhythm scales with density like the outer panel rhythm, so a
   compact/spacious surface stays proportional instead of only its outer
   intervals moving while the panel interiors hold still. Kept strictly below
   the outer .ds-panel-gap interval at every density — that ordering is what
   makes proximity report grouping truthfully. */
.ds-247420 .panel-body > * + * { margin-top: calc(var(--space-3) * var(--density)); }
.ds-247420 .panel.panel-wide .panel-body { padding: 0; }

/* ============================================================
   Row — primary list pattern. Indicator rail on the left,
   no hairlines between rows.
   ============================================================ */
.ds-247420 .row {
  display: grid;
  grid-template-columns: minmax(80px, 12ch) minmax(0, 1fr) auto;
  gap: var(--space-3);
  align-items: baseline;
  /* App density is the BASE: a list row is quiet working chrome, so it defaults
     to the compact 12px/16px rhythm (density-scaled) rather than the old
     16px/24px marketing padding. .row--lede restores the roomier marketing
     voice for editorial index pages. */
  padding: calc(var(--space-2-75) * var(--density)) var(--space-3);
  background: var(--bg);
  border-radius: var(--r-1);
  color: var(--fg);
  position: relative;
  /* Only background + the leading rail animate; padding/size never change on
     interaction, so they are deliberately absent (a padding transition here was
     dead and misleading). */
  transition: background var(--dur-snap) var(--ease), box-shadow var(--dur-base) var(--ease-spring);
}
.ds-247420 .row + .row { margin-top: 2px; }
/* Mobile tap-target floor: at compact/comfortable density on a touch
   viewport the padding-only row height can fall under the 44px effective
   tap target (WCAG 2.5.5), especially at [data-density="compact"] (0.75x).
   Force a min-height on narrow/coarse-pointer viewports only, so the dense
   desktop mono/list layout (file-row, index-row) is untouched above 640px. */
@media (max-width: 640px), (pointer: coarse) {
  .ds-247420 .row, .ds-247420 a.row, .ds-247420 button.row, .ds-247420 .row[role="button"] { min-height: 44px; }
}
/* Interactive rows (anchor/button wrappers) get a hairline lift on hover,
   matching the btn/panel elevation language — non-interactive rows (plain
   divs used as static list items) are unaffected since the selector needs a
   real interactive role. */
.ds-247420 a.row:hover, .ds-247420 button.row:hover, .ds-247420 .row[role="button"]:hover {
  box-shadow: var(--shadow-1);
}
/* `detail` drops to its own full-width line. The row is a grid, so span every
   track (grid-column 1/-1); flex-basis:100% covers the flex-row fallback. */
.ds-247420 .ds-row-detail {
  white-space: pre-wrap;
  font-family: var(--ff-mono);
  font-size: var(--fs-sm);
  line-height: var(--lh-snug);
  margin-top: var(--space-2);
  padding: 10px 12px;
  background: var(--bg);
  border-radius: var(--r-1);
  overflow-x: auto;
  flex-basis: 100%;
  grid-column: 1 / -1;
}
/* RESTYLED: borderless per explicit user request -- the left-edge indicator
   rail (.row::before, plus every rail-tone / active / error variant of it) is gone.
   Status/active/error state is now conveyed by tonal background alone
   (already the case for .active/.row-state-error below); the screen-reader
   status word Row() already emits alongside the rail is untouched, so
   assistive tech loses nothing. */
/* A clickable row (onClick -> role=button, or a link row) is interactive, so it
   carries a pointer cursor; without this the click affordance is invisible. */
.ds-247420 .row[role="button"], .ds-247420 a.row { cursor: pointer; }
.ds-247420 .row[role="button"]:hover, .ds-247420 a.row:hover { background: var(--bg-2); }
/* Editorial hover-reveal — the row's meta (cta) slides toward the lead accent
   and nudges right, the printed-index "open" affordance. */
.ds-247420 .row .meta { transition: color var(--dur-base) var(--ease), transform var(--dur-base) var(--ease-spring); }
.ds-247420 a.row:hover .meta, .ds-247420 .row[role="button"]:hover .meta { color: var(--accent-ink); transform: translateX(3px); }
.ds-247420 .row.active { background: color-mix(in oklab, var(--accent) 10%, var(--bg-2)); }
.ds-247420 .row.active .title { color: var(--accent-ink); }
/* ConversationList rows use `ds-session-row`, not `row` -- CI's a11y-audit
   caught --fg-3 (--ink-3) on `.ds-session-sub` failing AA (measured 4.43:1)
   inside a rail-tinted row after the warm-paper retune. --fg-2 (--ink-2)
   clears comfortably; scoped to `.ds-session-sub` specifically so plain rows
   without a rail tone keep the lighter --fg-3 tone. */
.ds-247420 .ds-session-row.active .ds-session-sub,
.ds-247420 .ds-session-row[class*="rail-"] .ds-session-sub { color: var(--fg-2); }
.ds-247420 .row.row-state-disabled { opacity: 0.5; pointer-events: none; }
.ds-247420 .row.row-state-error { background: color-mix(in oklab, var(--flame) 10%, var(--bg-2)); }
.ds-247420 .row.rail-purple { background: color-mix(in oklab, var(--purple-2) 8%, var(--bg-2)); }
.ds-247420 .row.rail-flame { background: color-mix(in oklab, var(--flame) 10%, var(--bg-2)); }
.ds-247420 .row-grid { /* explicit grid-template-columns set inline */ }
/* A row with no leading code/index drops the empty first column so the title
   takes the full width instead of wrapping in the narrow code gutter. */
.ds-247420 .row.row-nocode { grid-template-columns: minmax(0, 1fr) auto; }
/* Template-rendered index/example rows (site/theme.mjs panels, via
   panelNode()/examplesNode() in src/page-html/client-script.js) share one
   row template across every panel-driven section on the homepage — "ui
   kits", "previews", "file browser", "web components", "public api", "why
   design", "live examples". Every one of those rows carries an inline
   ".meta.dim" description and a trailing ".ds-row-arrow"; some ALSO carry a
   leading ".code" span (file_browser/web_components/api_exports items have
   a `code` field, "why design"/"live examples" items do not) — the
   selector below has to handle both shapes as ONE template, not two.
   Un-augmented, the base 3-track .row grid (minmax(80px,12ch) /
   minmax(0,1fr) / auto) either crams the title into the narrow 12ch code
   gutter (no-code rows) or, worse, auto-places an EXPLICIT-grid orphan:
   once .title/.ds-row-arrow/.meta.dim below are pinned to explicit
   grid-column/grid-row, a .code span left on `auto` placement can't share
   row 1 with .title (already occupying column 1) and gets pushed into a
   phantom row 3, rendering as a stray floating "fb"/"wc"/"api" line under
   the description — the exact bug this rule exists to prevent, previously
   left unfixed for the has-code case. Rows carrying a dim meta description
   and an arrow always get the two-row template: code + title + arrow share
   row 1 (code first, in its own narrow track), the description drops to
   its own full-width row 2 and wraps normally — matching how every other
   multi-line row pattern in this file (.ds-row-detail) already gets a
   dedicated line. */
.ds-247420 .row:has(.meta.dim):has(.ds-row-arrow) {
  grid-template-columns: minmax(80px, 12ch) minmax(0, 1fr) auto;
  grid-template-rows: auto auto;
  row-gap: var(--space-1);
}
.ds-247420 .row:has(.meta.dim):has(.ds-row-arrow) .code { grid-column: 1; grid-row: 1; }
.ds-247420 .row:has(.meta.dim):has(.ds-row-arrow) .title { grid-column: 2; grid-row: 1; }
.ds-247420 .row:has(.meta.dim):has(.ds-row-arrow) .ds-row-arrow { grid-column: 3; grid-row: 1; }
.ds-247420 .row:has(.meta.dim):has(.ds-row-arrow) .meta.dim { grid-column: 1 / -1; grid-row: 2; }
/* No-code rows ("why design", "live examples") drop the empty leading
   gutter so the title starts flush left instead of leaving a dead 12ch
   column — same fix .row-nocode already applies to the base template. */
.ds-247420 .row:has(.meta.dim):has(.ds-row-arrow):not(:has(.code)) {
  grid-template-columns: minmax(0, 1fr) auto;
}
.ds-247420 .row:has(.meta.dim):has(.ds-row-arrow):not(:has(.code)) .title { grid-column: 1; }
.ds-247420 .row:has(.meta.dim):has(.ds-row-arrow):not(:has(.code)) .ds-row-arrow { grid-column: 2; }
.ds-247420 .row .meta.dim {
  white-space: normal;
  min-width: 0;
  text-align: left;
}
.ds-247420 .ds-row-arrow {
  font-family: var(--ff-mono);
  font-size: var(--fs-xs);
  color: var(--fg-3);
  white-space: nowrap;
  align-self: center;
  justify-self: end;
}
.ds-247420 a.row:hover .ds-row-arrow, .ds-247420 .row[role="button"]:hover .ds-row-arrow {
  color: var(--accent-ink);
}

/* `.row-code` / `.row-title` / `.row-meta` are the BEM-ish spelling of the same
   three cells as `.code` / `.title` / `.meta`. Both spellings are live in the
   wild (preview/index-row.html uses the hyphenated one), and a cell that
   matches no rule falls back to browser defaults — 16px body text in all three
   tracks, which is precisely the undifferentiated render the specimen page was
   shipping. Aliased rather than renamed so neither spelling can silently go
   unstyled again. */
.ds-247420 .row .code,
.ds-247420 .row .row-code  { font-family: var(--ff-mono); font-size: var(--fs-xs); color: var(--fg-3); font-variant-numeric: tabular-nums; letter-spacing: 0.01em; }
/* Title defaults to the quiet app voice (fs-sm/500) — the 18px/600 marketing
   voice is now opt-in via .row--lede. */
.ds-247420 .row .title,
.ds-247420 .row .row-title { font-family: var(--ff-body); font-weight: 500; font-size: var(--fs-sm); line-height: var(--lh-snug); display: flex; align-items: baseline; gap: var(--space-2-5); flex-wrap: wrap; min-width: 0; }
.ds-247420 .row .title .sub { font-family: var(--ff-body); font-weight: 400; font-size: var(--fs-sm); color: var(--fg-3); }
/* Marketing/editorial index row — restores the roomier padding + larger title
   the default carried before app-scale became the base. */
.ds-247420 .row--lede { padding: var(--space-3) var(--space-4); }
.ds-247420 .row--lede .title { font-size: var(--fs-lg); font-weight: 600; }
/* App typescale: list rows are quiet working chrome (the 18px/600 default is a
   marketing-surface voice). One size for ALL app list rows - matches
   .ds-file-row/.ds-session-title at fs-sm/500; values mirror the kit's own
   mobile block below. Marketing pages without data-typescale keep the default. */
.ds-247420[data-typescale="app"] .row { padding: 12px 16px; }
.ds-247420[data-typescale="app"] .row .title { font-size: var(--fs-sm); font-weight: 500; }
.ds-247420 .row .sub   { font-family: var(--ff-body); font-weight: 400; font-size: var(--fs-sm); color: var(--fg-3); }
/* Context-pane cwd fact reads as a path (like .ds-dash-cwd/.ds-session-agent/
   .ds-dash-model) -- scoped to that one row so no other Row .sub is affected. */
.ds-247420 .ds-context-cwd-row .row .sub { font-family: var(--ff-mono); }
.ds-247420 .row .meta,
.ds-247420 .row .row-meta  { font-family: var(--ff-mono); font-size: var(--fs-xs); color: var(--fg-3); text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; align-self: center; }
/* `.row-meta` carries free prose ("state machine · 3k · shipping"), unlike
   `.meta`'s short tabular counts — so it wraps instead of forcing the row's
   third track wide enough to overflow a phone viewport. */
.ds-247420 .row .row-meta { white-space: normal; }
/* Hairline-divided index list. `.row` is deliberately hairline-FREE (the rail
   is the affordance), but an editorial index reads as a printed table of
   contents: the divider IS the structure, which is what
   preview/index-row.html's caption claims. Opt-in on the container so no
   existing rail-based list changes. */
.ds-247420 .row-list > .row + .row { margin-top: 0; border-top: 1px solid var(--rule); border-radius: 0; }
.ds-247420 .row-list > .row:first-child { border-radius: var(--r-1) var(--r-1) 0 0; }
.ds-247420 .row-list > .row:last-child { border-radius: 0 0 var(--r-1) var(--r-1); }
/* WorksList meta pairs a label with a disclosure chevron, inline-aligned. */
.ds-247420 .ds-works-meta { display: inline-flex; align-items: center; gap: .4em; }

.ds-247420 a.row { text-decoration: none; }

/* ============================================================
   Hero
   ============================================================ */
/* RESTYLED 2026 (webjsx-toolkit look): centered-stack hero, replacing the
   prior asymmetric two-column editorial grid (title-wide / body+aside-offset)
   per a deliberate house-rule reversal -- shadcn/webjsx-toolkit itself has no
   asymmetric-grid opinion, and the prior "no centered stack, ever" rule was
   specific to the retired Acid Editorial identity. All child classes/markup
   are UNCHANGED (no JS component edit needed) -- only the grid arrangement
   and each part's own layout (aside becomes a full-width card below the body
   instead of a right-column card) changed. Centered text, capped measure.

   RE-CENTERED 2026-08-09: reverted the 2026-07-31 de-centering (flex-start +
   left text-align + left inset) back to a centered stack -- the airy,
   dead-centered arrangement is the intended house look after all, matching
   the plain centered-lead reference this hero is now aligned to. Aside card
   dropped its filled background/accent bar for a plain top-rule divider. */
.ds-247420 .ds-hero {
  padding: var(--space-9) 0 var(--space-8);
  display: flex; flex-direction: column; align-items: center;
  gap: var(--space-5);
  max-width: var(--stage-wide);
  text-align: center;
  position: relative; isolation: isolate;
}
.ds-247420 .ds-hero-head { display: grid; gap: var(--space-3); justify-items: center; margin-inline: auto; }
.ds-247420 .ds-hero-body  { margin-inline: auto; }
/* The stat/badge/CTA cluster now sits as a full-width card BELOW the body
   copy, left-aligned with the rest of the stack, rather than a right-offset
   column -- still carries its own visual weight via the top accent bar
   (rotated from the former left spine) so it still reads as a deliberate
   block, not an afterthought. */
.ds-247420 .ds-hero-aside {
  position: relative; isolation: isolate;
  width: 100%; max-width: 46ch;
  margin-inline: auto;
  display: flex; flex-direction: column;
  justify-content: space-between; gap: var(--space-5);
  background: transparent;
  border-top: var(--bw-hair, 1px) solid var(--rule);
  padding: var(--space-4) var(--space-5) var(--space-5);
  text-align: left;
}
.ds-247420 .ds-hero-stats { display: flex; flex-direction: column; gap: var(--space-2); }
.ds-247420 .ds-hero-stat {
  font-family: var(--ff-mono); font-size: var(--fs-sm);
  color: var(--fg-2); letter-spacing: var(--tr-tight);
  padding-bottom: var(--space-2); border-bottom: var(--bw-hair) solid var(--bg-3);
}
.ds-247420 .ds-hero-stat:last-child { border-bottom: 0; padding-bottom: 0; }
/* HeroFromPageData's richer badge shape ({label, desc}) renders a strong
   figure plus a muted descriptor inside the same .ds-hero-stat row — give
   the pair a baseline-aligned inline layout instead of falling back to
   unstyled default inline flow. */
.ds-247420 .ds-hero-stat-n { font-weight: 700; color: var(--fg); margin-right: var(--space-2); }
.ds-247420 .ds-hero-stat-l { color: var(--fg-3); font-size: var(--fs-xs); }
.ds-247420 .ds-hero-title {
  font-family: var(--ff-display); font-weight: 700;
  /* Bespoke display clamp, deliberately off the --fs-* ladder: --fs-hero is
     clamp(42px,7cqi,96px) and --fs-mega is clamp(56px,11cqi,168px). This lead
     runs a steeper 9cqi slope with a LOWER 40px floor so a two-line 16ch title
     still fits a narrow column, and a 116px ceiling between the two tiers.
     Snapping to either changes the hero's whole optical weight. */
  font-size: clamp(40px, 9cqi, 116px);
  /* 0.96 let ascenders/descenders visibly touch between wrapped lines at the
     upper end of this clamp (measured on "the creative department of the
     internet" at the 116px ceiling) -- 1.02 keeps the tight, no-air display
     feel this title wants while clearing enough vertical room that letters
     stop colliding. */
  line-height: 1.02; letter-spacing: var(--tr-tighter);
  margin: 0; max-width: 16ch; margin-inline: auto; text-wrap: balance;
}
.ds-247420 .ds-hero-body {
  font-family: var(--ff-body); font-size: var(--fs-xl);
  line-height: 1.45; margin: 0; max-width: 46ch;
  color: var(--fg-2);
}
/* The lead phrase in the title — printed in the electric lead, not glowing. */
.ds-247420 .ds-hero-accent { color: var(--accent-ink); font-weight: 700; }
.ds-247420 .ds-hero-actions {
  display: flex; gap: var(--space-2, 10px); flex-wrap: wrap;
  justify-content: flex-start; margin-top: var(--space-4);
}
/* Hero CTA labels are content-driven (e.g. "GitHub", proper nouns from
   home.yaml) and the site's voice is deliberately all-lowercase everywhere
   else. Force the rendered case here rather than hand-lowercasing every
   proper-noun label at the content layer, so a future CTA label doesn't
   silently reintroduce Title Case. */
.ds-247420 .ds-hero-actions .btn { text-transform: lowercase; }
/* Filter input above the homepage's "ui kits" panel — reuses the same
   .input token the search kit's own topbar query box uses, just placed
   inline above the panel instead of in a topbar slot. */
.ds-247420 .ds-kits-panel-wrap { display: flex; flex-direction: column; gap: var(--space-2, 10px); }
.ds-247420 .ds-kits-filter { display: flex; }
.ds-247420 .ds-kits-filter-input { width: 100%; max-width: 32ch; }
/* Category pill row above the kits grid — same tonal-surface convention as
   .chip (primitives.css), never a bespoke color. Active state fills with
   --accent/--accent-fg (the same pairing .btn-primary already uses) rather
   than inventing a new "selected" hue. */
.ds-247420 .ds-cat-pills { display: flex; flex-wrap: wrap; gap: var(--space-1-75); }
.ds-247420 .ds-cat-pill {
  display: inline-flex; align-items: center; gap: var(--space-1-75);
  padding: var(--space-1) var(--space-2-5);
  background: var(--bg-2); color: var(--fg-2);
  border: none; border-radius: var(--r-pill);
  font-family: var(--ff-body); font-size: var(--fs-tiny); font-weight: 500;
  cursor: pointer;
  transition: background var(--dur-base) var(--ease), color var(--dur-base) var(--ease);
}
.ds-247420 .ds-cat-pill:hover { background: var(--bg-3); }
.ds-247420 .ds-cat-pill.is-active { background: var(--accent); color: var(--accent-fg); }
.ds-247420 .ds-cat-pill-count {
  font-family: var(--ff-mono); font-size: var(--fs-nano);
  opacity: 0.7;
}
@media (prefers-reduced-motion: reduce) {
  .ds-247420 .ds-cat-pill { transition: none; }
}
/* Below this width the aside card's own internal rhythm (stat rows still
   stacked vertically at full width) is already correct for a narrow column —
   no separate breakpoint override needed now that .ds-hero is always a
   single-column left-aligned stack at every width. */
@container (max-width: 900px) {
  .ds-247420 .ds-hero { padding: var(--space-7) 0 var(--space-6); }
}
/* .ds-hero already supplies its own --space-8 (96px) trailing padding, same
   "gap owned by one side only" pattern as * + .ds-section below. A .panel
   right after it still added its own --space-6-scaled top margin PLUS its
   panel-head top padding on top of that -- measured live, the stat block sat
   ~138px from "currently shipping" (96 hero pad + panel's own lead) versus
   the ~42px any other block gets ahead of a .panel, and well past the 96px
   .ds-section itself uses for its "more space above than below" lead. Zero
   the panel's own top margin here so the hero's trailing padding is the only
   contributor, matching every other panel/section transition on the page. */
.ds-247420 .ds-hero + .panel { margin-top: 0; }
.ds-247420 .ds-chat-title { margin: 0; font-size: inherit; }

/* ============================================================
   Section grouping
   ============================================================ */
.ds-247420 .ds-section { margin: var(--space-8) 0; }
/* A .ds-section's own margin-top exists so a BARE first section (no element
   above it inside .app-main/.ds-app-surface) still gets the --space-8 lead
   from something. But .app-main and .ds-tier are flex/grid containers, where
   sibling margins never collapse -- so once ANY preceding sibling supplies
   its own trailing space (a .panel's --space-4 bottom margin, or a prior
   .ds-section's own --space-8 bottom margin), that top margin stacks ON TOP
   of it instead of coordinating into one gap: measured live, two adjacent
   .ds-section elements sat 192px apart (96 + 96) instead of 96, and a .panel
   followed by a .ds-section sat ~136px apart instead of the intended single
   gap. Zeroing the top margin on every NON-first .ds-section collapses the
   pair back to one coordinated gap, supplied by whichever side already
   carries it (the preceding sibling's own bottom margin, or the .ds-section's
   trailing margin below) -- same "gap is owned by one side only" pattern as
   .ds-panel-gap + .ds-panel-gap / .row + .row elsewhere in this file. */
.ds-247420 * + .ds-section { margin-top: 0; }
/* Compact section (e.g. a PageHeader used as a page's first element): no large
   leading margin, modest trailing margin — top-aligns without consumer overrides. */
.ds-247420 .ds-section-compact { margin-top: 0; margin-bottom: var(--space-4); }
.ds-247420 .ds-section > h3 {
  font-family: var(--ff-body); font-weight: 600; font-size: var(--fs-h3);
  /* Zero top margin looks like it inverts the more-space-above-than-below rule,
     but it does not: this h3 is always the section's first child and .ds-section
     itself carries the --space-8 (96px) lead, so the effective space above is
     96px against 32px below. Adding a top margin here would double the lead.
     Measured live on project_page: gapAboveInSection 0 with the section margin
     supplying the separation, gapBelow 32. */
  margin: 0 0 var(--space-5);
  letter-spacing: -0.01em;
}

@media (max-width: 900px) {
  .ds-247420 .ds-section { margin: var(--space-4) 0; }
}

/* ============================================================
   Print texture — the signature surface treatment. Opt-in overlay that
   reads as risograph/newsprint grain, layered above the surface fill but
   below content. Faint by default so type stays crisp; reduced-data and
   reduced-motion are unaffected (texture is static).
   ============================================================ */
.ds-247420 .ds-grain { position: relative; isolation: isolate; }
.ds-247420 .ds-grain::after {
  content: ''; position: absolute; inset: 0; z-index: var(--z-below);
  pointer-events: none;
  background-image: var(--grain);
  background-size: 160px 160px;
  opacity: var(--grain-opacity);
  mix-blend-mode: var(--grain-blend);
}
/* ============================================================
   Marquee / ticker — brand kinetic device ("always open 24/7"). The track
   holds two identical runs so the loop is seamless; reduced-motion freezes it
   to a single static run. Editorial, not a decorative flourish.
   ============================================================ */
.ds-247420 .ds-marquee {
  overflow: hidden; width: 100%;
  border-block: var(--bw-hair) solid var(--rule-strong);
  padding: var(--space-2) 0;
  font-family: var(--ff-display); font-weight: 600;
  text-transform: uppercase; letter-spacing: var(--tr-caps);
  mask-image: linear-gradient(90deg, transparent 0, var(--fg) 28px, var(--fg) calc(100% - 28px), transparent 100%);
  -webkit-mask-image: linear-gradient(90deg, transparent 0, var(--fg) 28px, var(--fg) calc(100% - 28px), transparent 100%);
}
.ds-247420 .ds-marquee-track {
  display: inline-flex; gap: var(--space-5);
  white-space: nowrap; will-change: transform;
}
@media (prefers-reduced-motion: no-preference) {
  .ds-247420 .ds-marquee-track { animation: ds-marquee-run 28s linear infinite; }
  .ds-247420 .ds-marquee:hover .ds-marquee-track,
  .ds-247420 .ds-marquee:focus-within .ds-marquee-track,
  .ds-247420 .ds-marquee:active .ds-marquee-track { animation-play-state: paused; }
}
.ds-247420 .ds-marquee-run { display: inline-flex; align-items: center; gap: var(--space-5); }
.ds-247420 .ds-marquee-item { display: inline-flex; align-items: center; gap: var(--space-5); }
.ds-247420 .ds-marquee-sep { color: var(--accent-ink); }
@keyframes ds-marquee-run {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}
/* Touch has no hover-pause; slow the ticker so it burns less attention. */
@media (hover: none), (pointer: coarse) {
  .ds-247420 .ds-marquee-track { animation-duration: 40s; }
}
@media (prefers-reduced-motion: reduce) {
  /* Static: wrap to a readable strip instead of a frozen half-offscreen run. */
  .ds-247420 .ds-marquee-track { animation: none; flex-wrap: wrap; white-space: normal; }
  .ds-247420 .ds-marquee-run-b { display: none; }
}

/* ============================================================
   Install — single CLI command card
   ============================================================ */
.ds-247420 .cli {
  display: flex; align-items: center; gap: 14px;
  overflow-x: auto;
  padding: var(--space-3-5) var(--space-4);
  background: var(--ink); color: var(--paper);
  border-radius: var(--r-3);
  font-family: var(--ff-mono); font-size: var(--fs-lg);
}
/* user-select:none (matching .ds-cli-row .prompt below) so selecting/copying
   a command line grabs only the command text, not the prompt mark itself --
   otherwise every copy-pasted line carries a leading '$ ' that has to be
   stripped by hand before it can be re-run. */
.ds-247420 .cli .prompt { color: var(--green-2); user-select: none; }
.ds-247420 .cli .cmd    { flex: 1; min-width: 0; overflow-x: auto; white-space: nowrap; color: var(--paper); }
.ds-247420 .cli .copy   {
  padding: var(--space-2) var(--space-3); background: transparent; color: var(--paper);
  font-size: var(--fs-tiny); letter-spacing: var(--tr-tight); font-weight: 500;
  cursor: pointer; box-shadow: inset 0 0 0 1px var(--rule-strong);
  border-radius: var(--r-pill);
}
.ds-247420 .cli .copy:hover { background: var(--fg-3); }
.ds-247420 .cli .copy:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }

/* Multi-line CLI block: when .cli holds .ds-cli-row / .ds-cli-comment children
   (quickstart scripts, multi-command snippets — see CliBlock in
   src/components/content.js) it stacks as a column instead of the
   single-line install row. */
.ds-247420 .cli:has(.ds-cli-row),
.ds-247420 .cli:has(.ds-cli-comment) {
  flex-direction: column; align-items: stretch; gap: 0;
  font-size: var(--fs-sm); line-height: 1.6;
}
.ds-247420 .ds-cli-row {
  display: flex; gap: 10px;
  font-family: var(--ff-mono); font-size: var(--fs-tiny); line-height: 1.6;
  padding: 3px 0;
}
.ds-247420 .ds-cli-row .prompt { color: var(--green-2); flex: 0 0 auto; user-select: none; }
/* `overflow-wrap: break-word` only breaks a token when it genuinely cannot
   fit the line at all (last resort); `word-break: break-word` (its
   predecessor here) breaks ANY overflowing token eagerly, which is what
   split "anentrypoint-design" mid-identifier inside the importmap JSON line.
   `min-width: 0` lets the flex child actually shrink to trigger wrapping
   instead of pushing the panel wider. */
.ds-247420 .ds-cli-row .cmd { flex: 1 1 auto; min-width: 0; white-space: pre-wrap; overflow-wrap: break-word; word-break: normal; color: var(--paper); }
.ds-247420 .ds-cli-comment {
  color: var(--fg-3);
  font-family: var(--ff-mono); font-size: var(--fs-tiny); line-height: 1.6;
  padding: 3px 0; white-space: pre-wrap; overflow-wrap: break-word; word-break: normal;
}

/* Quickstart copy button: sits below the multi-line block (Install's inline
   copy button assumes a single-line row with room beside it; the stacked
   quickstart block has no such side room). */
.ds-247420 .ds-quickstart-cli { flex-direction: column; align-items: stretch; gap: var(--space-2); }
.ds-247420 .ds-quickstart-copy {
  align-self: flex-end;
  padding: var(--space-2) var(--space-3); background: transparent; color: var(--paper);
  font-family: var(--ff-body); font-size: var(--fs-tiny); letter-spacing: var(--tr-tight); font-weight: 500;
  cursor: pointer; box-shadow: inset 0 0 0 1px var(--rule-strong);
  border-radius: var(--r-pill); border: none;
}
.ds-247420 .ds-quickstart-copy:hover { background: var(--fg-3); }
.ds-247420 .ds-quickstart-copy:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }

/* ============================================================
   Receipt — key/value table
   ============================================================ */
.ds-247420 table.kv { width: 100%; border-collapse: collapse; }
.ds-247420 table.kv td {
  padding: 8px 0;
  font-family: var(--ff-body); font-size: var(--fs-sm);
}
.ds-247420 table.kv tr + tr td { border-top: 1px solid var(--rule); }
.ds-247420 table.kv td:first-child {
  color: var(--fg-3);
  font-size: var(--fs-xs);
  text-transform: uppercase; letter-spacing: var(--tr-caps);
  width: 14ch;
  /* The 14ch column width is a target, not a guarantee — a label whose
     uppercase-transformed width fills the column exactly (e.g. "environment")
     leaves zero slack before the value cell, so label and value render with
     no visible gap between them ("ENVIRONMENTproduction") while shorter
     labels (region, build, by) are saved only by leftover column slack.
     An explicit right-side padding guarantees a gap regardless of label
     length, instead of relying on slack that a long label can consume
     entirely. */
  padding-right: var(--space-3);
}
.ds-247420 table.kv td:last-child {
  font-weight: 600;
  overflow-wrap: anywhere; min-width: 0;
}
/* Tiny screens: stack the key over the value instead of squeezing a 14ch
   label column beside long unbroken values (URLs, hashes). */
@media (max-width: 400px) {
  .ds-247420 table.kv td { display: block; width: auto; padding: 4px 0; }
  .ds-247420 table.kv td:first-child { padding-top: 8px; }
  .ds-247420 table.kv tr + tr td { border-top: 0; }
  .ds-247420 table.kv tr + tr td:first-child { border-top: 1px solid var(--rule); }
}

/* ============================================================
   Generic table
   ============================================================ */
.ds-247420 table {
  width: 100%; border-collapse: collapse;
  font-family: var(--ff-body); font-size: var(--fs-sm);
  /* A data grid, not a card: a tight --r-0 corner (was --r-3/22px, which read
     as a rounded card). Cells drop to 8px 12px for a dense, scannable row
     (was 12/14 x 16 => ~43px rows). Density scales the vertical padding. */
  border-radius: var(--r-0); overflow: hidden;
}
/* Panel heads / KPI labels / table headers were all identical uppercase-
   tracked-mono weight (600), so a page with several of these side by side
   (a dashboard) reads as one flat volume with no anchor -- the most
   repetitive, least-singular label (a column header, one of many per
   table) drops to a lighter 500 with slightly tighter tracking, keeping the
   system's uppercase-mono voice but giving panel titles (still 600) a real
   step up as the "read this first" tier. */
.ds-247420 table th {
  text-align: left;
  padding: calc(var(--space-2) * var(--density)) var(--space-2-75);
  font-size: var(--fs-tiny); font-weight: 500;
  color: var(--fg-3); text-transform: uppercase; letter-spacing: calc(var(--tr-caps) * 0.7);
  background: var(--bg-2);
}
/* letter-spacing adds trailing space AFTER the last glyph too, but the table's
   own border-radius+overflow:hidden (above) clips flush to the card edge --
   the last header's positive tracking bleeds past its own padding box and
   gets cropped mid-word ("STATUS" -> "STATI"). Absorb that bleed with a bit
   of extra right padding on the last column only, so no header text sits
   closer to the clip edge than --tr-caps can push it. */
.ds-247420 table th:last-child { padding-right: calc(var(--space-2-75) + var(--tr-caps, 0px)); }
.ds-247420 table td { padding: calc(var(--space-2) * var(--density)) var(--space-2-75); border-top: 1px solid var(--rule); }
.ds-247420 table tr.clickable { cursor: pointer; }
.ds-247420 table tr.clickable:hover td { background: var(--bg-2); }
.ds-247420 table tr.clickable:focus-visible {
  outline: var(--focus-w) solid var(--focus-color);
  outline-offset: -2px;
}
.ds-247420 table tr.clickable:focus-visible td { background: var(--bg-2); }
/* Non-clickable (static) rows still get a subtle hover cue — most dashboard
   tables (freddie's sessions-by-platform/model, tool distribution, commands)
   never pass onRowClick, so they had zero row-hover feedback at all. A dim
   hover on ANY row (not just .clickable) reads as "you're scanning this
   row" without implying it's actionable. */
.ds-247420 table tr:not(.clickable):hover td { background: color-mix(in oklab, var(--bg-2) 60%, transparent); }

/* Table() striped/compact opt-in density modifiers (webgeist g-table parity).
   Scoped under .ds-table-wrap so they never leak onto an unrelated bare
   <table> a consumer hand-rolls outside this component. */
.ds-247420 .ds-table-wrap.is-striped table tr:nth-child(even) td { background: var(--bg-2); }
.ds-247420 .ds-table-wrap.is-striped table tr.clickable:nth-child(even):hover td,
.ds-247420 .ds-table-wrap.is-striped table tr.clickable:nth-child(even):focus-visible td { background: var(--bg-3, var(--bg-2)); }
.ds-247420 .ds-table-wrap.is-striped table tr:not(.clickable):nth-child(even):hover td { background: color-mix(in oklab, var(--bg-3, var(--bg-2)) 60%, transparent); }
.ds-247420 .ds-table-wrap.is-compact table th { padding: var(--space-1-5, 5px) var(--space-2, 8px); }
.ds-247420 .ds-table-wrap.is-compact table td { padding: var(--space-1-5, 5px) var(--space-2, 8px); }
/* Numeric-column right-alignment: Table() (content/table.js) tags a cell
   td.is-num when its raw content is a plain integer/decimal, so digit
   columns (session counts, tool counts, etc.) line up on the ones place
   instead of ragged-left like prose columns. th.is-num mirrors it on the
   matching header so the label sits over its column. */
.ds-247420 table td.is-num, .ds-247420 table th.is-num { text-align: right; font-variant-numeric: tabular-nums; }

/* ============================================================
   Changelog
   ============================================================ */
.ds-247420 .ds-changelog-row {
  /* The !important is load-bearing and was empirically confirmed so -- do not
     remove it on a specificity argument. A changelog row carries three tracks
     (date, version, message) where a plain .row carries two, and the responsive
     blocks in responsive.css legitimately rewrite .row's template at several
     breakpoints. Those blocks are later in the cascade, so at tablet width the
     two-track override wins on source order and collapses the version column:
     measured live, removing this flag changes the computed template from
     "120.75px 77.625px 70.125px" to "103.5px 0px 165px" -- a zero-width column
     and the version silently gone. Scoping to .app does not help, because the
     competing rules would need to be scoped in lockstep. */
  grid-template-columns: minmax(110px, 14ch) minmax(60px, 9ch) minmax(0, 1fr) !important;
}
.ds-247420 .ds-changelog-ver {
  font-family: var(--ff-body); font-weight: 600; font-size: var(--fs-sm);
}
/* Belt-and-suspenders for the message column: `.row .title` (panel-row.css)
   is already `white-space: normal` (no nowrap/ellipsis), so long entries wrap
   by word as expected whenever the message track has any real width. This
   only matters in the narrow zone between "row still on 3 columns" and the
   panel's own `@container` breakpoint kicking in (see .panel-wide's new
   container-type in panel-row.css) — overflow-wrap:anywhere is the fallback
   so a single long unbroken token still wraps instead of overflowing the
   track, rather than depending solely on the breakpoint transition. */
.ds-247420 .ds-changelog-row .title {
  overflow-wrap: anywhere;
  min-width: 0;
}

/* ============================================================
   Works
   ============================================================ */
.ds-247420 .work-detail {
  padding: var(--space-4) var(--space-5) var(--space-5);
  margin: 4px 0 var(--space-2);
  background: var(--bg-2);
  border-radius: var(--r-3);
  display: flex; flex-direction: column; gap: var(--space-3);
}
.ds-247420 .ds-work-body { margin: 0; max-width: 60ch; }
.ds-247420 .ds-work-actions { display: flex; gap: 10px; flex-wrap: wrap; }

/* ============================================================
   Manifesto / pull moment
   ============================================================ */
.ds-247420 .ds-manifesto { max-width: 60ch; }
.ds-247420 .ds-manifesto-para {
  font-family: var(--ff-body); font-size: var(--fs-lg);
  line-height: var(--lh-long); margin: 0 0 var(--space-3);
}
.ds-247420 .ds-manifesto-para.dim { color: var(--fg-3); }

/* ============================================================
   Currently shipping dots
   ============================================================ */
.ds-247420 .ds-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: currentColor; flex-shrink: 0; vertical-align: middle; }
.ds-247420 .ds-dot-on  { color: var(--green-2); }
/* Off/idle reads as a HOLLOW ring (transparent fill, currentColor outline)
   instead of a solid dot in a dimmer color -- filled-vs-outline is a shape
   cue that survives grayscale/colorblind simulation, where a solid green dot
   and a solid gray dot at the same size can look identical in luminance. */
.ds-247420 .ds-dot-off { color: var(--fg-3); background: transparent; box-shadow: inset 0 0 0 1.5px currentColor; }
/* live is a distinct hue from a plain "on" dot — see .chip.tone-live's
   comment for the success-vs-live rationale. */
.ds-247420 .ds-dot-live { color: var(--sky); }
/* warn is solid but smaller with a ring outset, a distinct silhouette from
   both -on's plain fill and -off's plain ring so tone doesn't rely on hue
   alone to differentiate from either neighbor. */
.ds-247420 .ds-dot-warn { color: var(--amber); box-shadow: 0 0 0 2px color-mix(in oklab, var(--amber) 35%, transparent); }
.ds-247420 .ds-dot-idle { color: var(--fg-3); }

/* ============================================================
   KPI
   ============================================================ */
.ds-247420 .kpi {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: var(--space-2);
}
.ds-247420 .kpi-card {
  /* Was --space-5 (32px) pad + --r-3 (22px) — four cards ate a full fold. A
     16px pad on a --r-1 corner keeps them prominent but dense; the number drops
     from a 48px marketing display to a 32px app-scale figure. */
  padding: var(--space-3);
  background: var(--bg-2);
  border-radius: var(--r-1);
  /* A thin border + hover elevation gives the tile real edges and a "this is
     a distinct object" affordance instead of a flat color patch that blends
     into the surrounding panel — matches the "no border, no hover state"
     gap named against the stat tiles. */
  border: 1px solid var(--rule);
  position: relative;
  transition: border-color 120ms ease, transform 120ms ease, box-shadow 120ms ease;
}
.ds-247420 .kpi-card:hover {
  border-color: color-mix(in oklab, var(--accent) 40%, var(--rule));
  transform: translateY(-1px);
  box-shadow: var(--shadow-1, 0 2px 8px rgba(0,0,0,.12));
}
/* Number+caption stack, isolated from .kpi-foot below it. Previously the
   number and label were plain block children of .kpi-card with the label's
   own margin-top providing the gap — visually fine for a single card, but
   across a row of four the caption's WRAPPED height (one line vs two, e.g.
   "requests · 24h" vs "error rate · 5xx+4xx") pushed .kpi-foot down by a
   different amount per card, so the deltas/sparklines never lined up on one
   shared baseline. A dedicated flex column groups exactly the two elements
   that must stay glued together, independent of whatever sits below. */
.ds-247420 .kpi-stack {
  display: flex; flex-direction: column; align-items: flex-start;
}
.ds-247420 .kpi-glyph {
  position: absolute;
  top: var(--space-2);
  right: var(--space-2);
  color: var(--fg-3);
  opacity: 0.6;
  display: inline-flex;
}
.ds-247420 .kpi-card .num {
  font-family: var(--ff-body); font-weight: 600;
  /* Numeric display size, not a heading tier: --fs-h4 is clamp(19px,1.8cqi,
     24px) — too small and too flat a slope for a KPI figure that must read as
     the loudest thing in its card, and --fs-h3 (22/30) tops out short of 32.
     Deliberate off-scale value tuned to the .kpi-card box. */
  font-size: clamp(22px, 3cqi, 32px); line-height: 1;
  letter-spacing: var(--tr-tight);
}
.ds-247420 .kpi-card .lbl {
  font-size: var(--fs-xs); color: var(--fg-3);
  font-weight: 600; margin-top: 8px;
  letter-spacing: var(--tr-tight);
}
.ds-247420 .kpi-foot {
  display: flex; align-items: center; justify-content: space-between;
  margin-top: var(--space-2);
}
.ds-247420 .kpi-delta {
  display: inline-flex; align-items: center; gap: var(--space-hair);
  font-size: var(--fs-xs); font-weight: 600;
}
/* --green, not --success (--green-2): --success measures 3.2:1 on --paper-2,
   below the 4.5:1 AA floor for text -- --success is tuned for non-text uses
   (fills/backgrounds, where 3:1 suffices), --green clears 5.2:1 as text. */
.ds-247420 .kpi-delta-up { color: var(--green); }
.ds-247420 .kpi-delta-down { color: var(--danger); }
.ds-247420 .ds-sparkline path { stroke: currentColor; }
.ds-247420 .ds-sparkline-up { color: var(--green); }
.ds-247420 .ds-sparkline-down { color: var(--danger); }

/* ============================================================
   BarChart
   ============================================================ */
.ds-247420 .ds-barchart { display: flex; flex-direction: column; gap: var(--space-2); }
.ds-247420 .ds-barchart-row {
  display: grid; grid-template-columns: minmax(90px, 25%) 1fr auto;
  align-items: center; gap: var(--space-2);
}
.ds-247420 .ds-barchart-label { font-size: var(--fs-sm); color: var(--fg-2); }
.ds-247420 .ds-barchart-track {
  /* 6px unifies with the system's other bar tracks (.ds-upload-bar,
     .ds-bar-bg); was --space-2 (8px), the lone thicker slab in the bar family. */
  height: var(--space-1-75); border-radius: var(--r-pill);
  background: var(--bg-3); overflow: hidden;
}
.ds-247420 .ds-barchart-fill {
  height: 100%; width: var(--bar-pct, 0%);
  background: var(--fg-3); border-radius: var(--r-pill);
}
.ds-247420 .ds-barchart-fill.is-active { background: var(--accent); }
.ds-247420 .ds-barchart-value {
  font-size: var(--fs-xs); font-weight: 600; color: var(--fg-3);
  font-variant-numeric: tabular-nums;
}

/* ============================================================
   Empty state
   ============================================================ */
/* Shares its base declarations with .ds-empty-state (primitives.css) via the
   combined selector there -- .empty only adds its own font-size/background/
   radius modifiers on top. Was --space-8 (96px) padding; --space-6 (48px)
   still centres a message with air without the empty state dominating the
   fold. */
.ds-247420 .empty {
  background: var(--bg-2);
  border-radius: var(--r-1);
  font-size: var(--fs-sm);
}

/* ============================================================
   Form
   ============================================================ */
.ds-247420 .row-form {
  display: grid; gap: var(--space-2-75);
  padding: var(--space-5);
  background: var(--bg-2);
  border-radius: var(--r-3);
  /* Grid's default justify-items:stretch was silently stretching the submit
   * button (a direct grid child, same as every input) to the full column
   * width -- other primary buttons elsewhere (e.g. session-ui's "new
   * session") are inline/compact by contrast, since they're not grid
   * children. justify-self:start keeps inputs/textareas at their natural
   * grid-stretched width (a form field SHOULD fill its column) while the
   * submit button sizes to its own content, matching the compact convention
   * used everywhere else primary buttons appear. */
}
.ds-247420 .row-form > button[type="submit"] { justify-self: start; }
.ds-247420 .row-form input,
.ds-247420 .row-form textarea {
  font-family: inherit; font-size: var(--fs-sm);
  padding: 12px 16px;
  /* --bg here previously resolved to the SAME color as .row-form's own
   * --bg-2 background in the dark theme, leaving only a 14%-opacity
   * currentColor ring (var(--rule)) as the sole edge between an input and
   * the panel behind it -- effectively invisible. --bg-3 (one step darker
   * than the panel) plus a stronger border restores a real, panel-distinct
   * field surface.
   * LOAD-BEARING !important: `.row-form input` and states-interactions.css's
   * bare `input[type="text"]:where(...)` rule are both (0,1,1) specificity
   * (the :where() wrapper there is intentionally zero-specificity) --
   * states-interactions.css @imports AFTER this file in app-shell.css, so on
   * this exact tie it wins by source order and silently discarded these two
   * declarations. Only !important beats that without restructuring the
   * shared bare-input rule's own selector, which many other consumers rely on. */
  background: var(--bg-3) !important; color: var(--fg);
  border: 0; border-radius: var(--r-2);
  box-shadow: inset 0 0 0 1px color-mix(in oklab, currentColor 28%, transparent) !important;
}
.ds-247420 .row-form input:focus-visible,
.ds-247420 .row-form textarea:focus-visible { box-shadow: var(--focus-ring-inset); }

/* Field char counter (TextField maxLength) — canonical definition is with
   the rest of the .ds-field family below (states-interactions.css); this
   duplicate was removed to stop the two bodies fighting on stylesheet load
   order. */

/* Multi-column form layout (Form columns prop) */
.ds-247420 .row-form[data-columns="2"] { grid-template-columns: repeat(2, 1fr); }
.ds-247420 .row-form[data-columns="3"] { grid-template-columns: repeat(3, 1fr); }
@media (max-width: 600px) {
  .ds-247420 .row-form[data-columns="2"],
  .ds-247420 .row-form[data-columns="3"] { grid-template-columns: 1fr; }
}

/* ============================================================
   Responsive Design — Mobile (480px), Tablet (1024px), Desktop (1440px+)
   ============================================================ */

/* Breakpoint reference (CSS custom properties can't be used in @media queries,
   so these are documented here for consistency — always use these exact values):
   - Mobile/tablet boundary: 768px (max-width) — used for compact nav/sidebar
     collapse (catalog-theme.css, sidebar-misc.css). Note: kits-appended.css
     uses 760px for the same kind of collapse in a few places — a near-miss
     of this value, worth aligning to 768px when touched.
   - Tablet/desktop boundary: 900px (max-width) / 901px (min-width) — used for
     main layout reflow (hero-content.css, responsive2-workspace.css)
   - Tablet landscape band: 768px–1024px (min-width 768px / max-width 1024px) —
     used in this file (responsive.css) for a specific tablet treatment;
     aligned to the 768px mobile/tablet boundary above (was 481px, a near-miss).
*/

/* --------------------------------------------------------------------
   Mobile Portrait Breakpoint (480px and below)
   ---------------------------------------------------------------------- */
@media (max-width: 480px) {
  /* App Layout: single-column + drawer is handled once in the ≤900px block;
     no need to re-declare grid-template-columns here (was a redundant !important). */

  /* Topbar Navigation: stack the nav onto its own full-width row below the
     brand and let it scroll horizontally. The brand stays on the first row;
     nav wraps to a second row at 100% width (flex-wrap on .app-topbar). */
  .ds-247420 .app-topbar {
    gap: var(--space-2); padding: var(--space-2-75) var(--space-3);
  }
  /* .app-chrome (the merged topbar+crumb band) inherits --pad-x — 32-48px of
     desktop-scale horizontal padding — with no mobile override anywhere,
     unlike .app-topbar above. On a 375px phone that reads as the title/
     controls floating inset from the screen edges instead of flush. */
  .ds-247420 .app-chrome { padding: var(--space-2) var(--space-3); }
  /* When the topbar is folded into .app-chrome it is a flex:0 0 auto child, so
     it sizes to its nav's content and overflows the viewport instead of letting
     the nav's own overflow-x:auto engage. Constrain it to the chrome width so
     the horizontally-scrolling nav stays inside the screen. */
  .ds-247420 .app-chrome > .app-topbar { max-width: 100%; min-width: 0; flex-basis: 100%; }
  .ds-247420 .app-topbar > nav {
    flex: 1 1 100%; margin-left: 0; order: 3; min-width: 0; max-width: 100%;
    display: flex; gap: var(--space-hair); flex-wrap: nowrap;
    overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch;
    /* Fade the trailing edge so a clipped last item (e.g. "source ->" cut to
       "sour") reads as scrollable content, not a broken/truncated label. */
    mask-image: linear-gradient(90deg, var(--fg) 0 calc(100% - 28px), transparent 100%);
    -webkit-mask-image: linear-gradient(90deg, var(--fg) 0 calc(100% - 28px), transparent 100%);
  }
  .ds-247420 .app-topbar nav::-webkit-scrollbar { display: none; }
  .ds-247420 .app-topbar nav a {
    flex: 0 0 auto; padding: var(--space-2-5) var(--space-2-75); min-height: 44px; white-space: nowrap;
  }
  .ds-247420 .brand { font-size: var(--fs-tiny); font-weight: 600; }

  /* Search */
  .ds-247420 .app-search {
    display: none; /* Hide search on mobile unless explicitly pinned */
  }

  /* Main Content */
  .ds-247420 .app-main { padding: var(--space-4) var(--space-2); }
  .ds-247420 .app-main.narrow { max-width: 100%; margin: 0; }

  /* Headings stay on the fluid base clamps (cqi) — no mobile re-clamp. */

  /* Panel & Row */
  .ds-247420 .panel { margin: 0 0 var(--space-3); }
  .ds-247420 .panel-head { padding: var(--space-3) var(--space-2); gap: var(--space-2); }
  .ds-247420 .panel-body { padding: var(--space-2); overflow-x: auto; -webkit-overflow-scrolling: touch; }
  .ds-247420 .panel-body > table { min-width: 100%; }

  /* Scoped to the `.app` shell so this wins on SPECIFICITY (0,2,0) rather than
     on an !important flag. Three rules would otherwise beat a bare `.row`
     (0,1,0) here: `.row.row-nocode` (0,2,0, panel-row.css) out-specifies it,
     and the `@media(481-1024)` + `@media(max-width:768px)` `.row` rules later
     in THIS file win on source order (a media query adds no specificity).
     `.app` is safe to require: `.app-body`/`.row` only ever render inside it
     (single call site, shell.js AppShell). */
  .ds-247420 .app .row {
    grid-template-columns: minmax(0, 1fr) auto;
    gap: var(--space-2); padding: var(--space-2-75) var(--space-3);
    row-gap: var(--space-1);
  }
  .ds-247420 .row .sub { grid-column: 1 / -1; order: 3; }
  .ds-247420 .row .title { font-size: var(--fs-sm); }
  /* Changelog carries 3 children: give date+ver line one, message wraps below
     (the generic 2-col .row override staggers it into an implicit row).
     Needs (0,3,0) to clear BOTH the `.app .row` rule directly above and
     `.ds-changelog-row`'s own !important base in hero-content.css — an
     !important base can only be beaten by another !important, so this one
     stays until that upstream flag is removed (hero-content.css is outside
     this change's scope). */
  .ds-247420 .app .row.ds-changelog-row { grid-template-columns: minmax(70px, 10ch) minmax(0, 1fr) !important; }
  .ds-247420 .ds-changelog-row .title { grid-column: 1 / -1; }

  .ds-247420 .btn:not(.btn-sm):not(.btn-lg), .ds-247420 .btn-primary:not(.btn-sm):not(.btn-lg), .ds-247420 .btn-ghost:not(.btn-sm):not(.btn-lg) {
    /* 11px vertical is optical, not scale drift: --space-2-75 (12) would tip
       the intrinsic box past the 44px min-height floor below and make the
       floor dead, changing the mobile button rhythm. */
    padding: 11px var(--space-3); font-size: var(--fs-tiny);
    min-height: 44px; /* Touch target minimum */
  }

  /* File Grid */
  .ds-247420 .ds-file-grid {
    display: flex;
    flex-direction: column;
    gap: var(--space-half);
  }
  .ds-247420 .ds-file-row {
    gap: var(--space-2); padding: var(--space-2-5) var(--space-2-75);
    font-size: var(--fs-xs);
  }
  .ds-247420 .ds-file-open { gap: var(--space-2-5); }
  /* At mobile row widths, a fixed-size meta column (date/size) plus the
     always-visible action buttons leave the flexible title only a few px —
     not enough for even one legible character before the two collide. Drop
     the metadata column here; the title + actions still fully identify and
     operate on the row, and meta stays available in the file viewer. */
  .ds-247420 .ds-file-row .ds-file-meta { display: none; }
  .ds-247420 .ds-file-row .ds-file-actions { opacity: 1; }
  /* H4: floor finger-sized controls so file actions/sort/filter/breadcrumb are
     reachable by touch (cohesion sweep). */
  .ds-247420 .ds-file-act, .ds-247420 .ds-file-sort-btn, .ds-247420 .ds-crumb-seg { min-height: 44px; }
  .ds-247420 .ds-file-filter-input { min-height: 44px; }
  .ds-247420 .ds-file-check, .ds-247420 .ds-file-selectall, .ds-247420 .ds-density-btn { min-height: 44px; min-width: 44px; }

  /* Chat */
  .ds-247420 .chat-stack { max-width: 100%; min-width: 0; }
  .ds-247420 .chat-bubble {
    padding: var(--space-2-5) var(--space-2-75); font-size: var(--fs-sm);
  }
  .ds-247420 .chat-avatar { width: 28px; height: 28px; font-size: 11px; }
  .ds-247420 .chat-msg:hover { padding: var(--space-1) 0; margin: 0; background: transparent; }
  .ds-247420 .chat-composer { padding: var(--space-2); gap: var(--space-1-75); }
  .ds-247420 .chat-composer textarea { padding: var(--space-2-5) var(--space-2-75); font-size: var(--fs-sm); }

  /* KPI Cards */
  .ds-247420 .kpi {
    grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
    gap: var(--space-2);
  }
  .ds-247420 .kpi-card { padding: var(--space-3); }
  /* Container-relative like the base rule (5vw sized against the viewport and
     jumped across the breakpoint). */
  .ds-247420 .kpi-card .num { font-size: clamp(22px, 8cqi, 32px); }

  /* Empty State */
  .ds-247420 .empty { padding: var(--space-5); font-size: var(--fs-xs); }

  /* Form */
  .ds-247420 .row-form { gap: var(--space-2-5); padding: var(--space-3); }
  .ds-247420 .row-form input,
  
  .ds-247420 .row-form textarea { padding: 11px var(--space-2-75); font-size: var(--fs-sm); }

  /* Hero Section — keep fluid base font-size, just unconstrain width on mobile. */
  .ds-247420 .ds-hero { padding: var(--space-6) 0 var(--space-5); }
  .ds-247420 .ds-hero-title { max-width: 100%; }
  .ds-247420 .ds-hero-body { max-width: 100%; }

  /* Table Responsiveness */
  .ds-247420 table { font-size: var(--fs-xs); }
  .ds-247420 table th,
  .ds-247420 table td { padding: var(--space-2-5) var(--space-2-75); }

  /* CLI Block */
  .ds-247420 .cli {
    flex-direction: column; align-items: flex-start; gap: var(--space-2-5);
    padding: var(--space-3) var(--space-2-75); font-size: var(--fs-xs);
  }
  .ds-247420 .cli .copy { padding: var(--space-1-75) var(--space-2-75); font-size: var(--fs-micro); }
}

/* --------------------------------------------------------------------
   Container Query Responsive (720px and below)
   ---------------------------------------------------------------------- */
@container (max-width: 760px) {
  /* `.app`-scoped for specificity, not an !important flag: the bare `.app-body`
     (0,1,0) was out-specified by `.app-body.no-side` (0,2,0) in topbar.css, so
     a sidebar-less shell kept its base template. `.app .app-body` is (0,2,0)
     and lands later in source, beating both topbar.css rules on merit. */
  .ds-247420 .app .app-body { grid-template-columns: 1fr; }
  .ds-247420 .app-side-shell { border-right: 0; border-bottom: 1px solid var(--rule); }
  .ds-247420 .app-topbar nav { font-size: var(--fs-xs); gap: var(--space-2-75); }
  /* Same (0,2,0) lift as the 480px block above — clears `.row.row-nocode` and
     the later `@media` `.row` rules in this file without a flag. */
  .ds-247420 .app .row { grid-template-columns: minmax(0, 1fr) auto; row-gap: var(--space-1); }
  .ds-247420 .row .sub { grid-column: 1 / -1; order: 3; }
  /* (0,3,0) + !important: still required to beat `.ds-changelog-row`'s
     !important base in hero-content.css (see the 480px block). */
  .ds-247420 .app .row.ds-changelog-row { grid-template-columns: minmax(70px, 10ch) minmax(0, 1fr) !important; }
  .ds-247420 .ds-changelog-row .title { grid-column: 1 / -1; }
}

/* --------------------------------------------------------------------
   Tablet Landscape Breakpoint (768px to 1024px)
   ---------------------------------------------------------------------- */
@media (min-width: 768px) and (max-width: 1024px) {
  /* App Layout — side handled by 900px drawer block; restore for 901-1024 */
  .ds-247420 .app-side a {
    padding: var(--space-2-75) var(--space-2-75); gap: var(--space-2);
    grid-template-columns: 16px 1fr auto;
    font-size: var(--fs-xs);
    min-height: 44px;
  }

  /* Topbar Navigation (flex container — no grid-template here) */
  .ds-247420 .app-topbar {
    gap: var(--space-3); padding: var(--space-2-75) var(--space-4);
  }
  .ds-247420 .app-chrome { padding: var(--space-2-75) var(--space-4); }
  .ds-247420 .app-topbar nav {
    display: flex; gap: var(--space-2); font-size: var(--fs-xs);
    flex-wrap: wrap;
  }
  .ds-247420 .app-topbar nav a {
    padding: var(--space-2-75) var(--space-2-75); font-size: var(--fs-xs);
    min-height: 44px;
  }
  .ds-247420 .brand { font-size: var(--fs-sm); }

  /* Search */
  .ds-247420 .app-search {
    display: inline-flex;
    max-width: 280px; font-size: var(--fs-xs);
    /* 14px inline is between --space-2-75 (12) and --space-3 (16) with no tier;
       it is the width that centers the search glyph against the 280px max-width
       field. Snapping either way reads as a mis-centered control. */
    padding: var(--space-2) 14px;
  }

  /* Main Content */
  .ds-247420 .app-main { padding: var(--space-6) var(--space-4); }
  .ds-247420 .app-main.narrow { max-width: 100%; }

  /* Headings stay on the fluid base clamps (cqi) — no tablet re-clamp. */

  /* File list — single column rows on tablet (cards only via data-columns) */
  .ds-247420 .ds-file-row {
    grid-template-columns: 24px minmax(0, 1fr) auto auto;
    /* 14px inline (no tier between 12 and 16) is the tablet row inset that
       keeps the 24px icon column optically flush with the panel edge. */
    gap: var(--space-2-75); padding: var(--space-2-75) 14px;
  }

  /* Chat Bubbles */
  .ds-247420 .chat-stack { max-width: min(75%, 420px); }
  .ds-247420 .chat-bubble {
    /* 11px/14px are the tuned tablet bubble insets; neither is on the scale
       (11 sits between --space-2-5 and --space-2-75, 14 between --space-2-75
       and --space-3) and snapping visibly re-pitches every bubble in a thread. */
    padding: 11px 14px; font-size: var(--fs-sm);
  }
  .ds-247420 .chat-avatar { width: 32px; height: 32px; font-size: 12px; }

  /* KPI Cards */
  .ds-247420 .kpi {
    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
    gap: var(--space-3);
  }

  .ds-247420 .btn:not(.btn-sm):not(.btn-lg), .ds-247420 .btn-primary:not(.btn-sm):not(.btn-lg), .ds-247420 .btn-ghost:not(.btn-sm):not(.btn-lg) {
    /* 18px inline has no tier (--space-3 is 16, --space-3-5 is 20); it is the
       tablet CTA width tuned against the 12px vertical, so snapping changes
       the button aspect on the most visible control in the kit. */
    padding: var(--space-2-75) 18px; font-size: var(--fs-sm);
  }

  /* Panel - keep the head on the body's left axis at laptop widths too. */
  .ds-247420 .panel-head { padding: var(--space-2) var(--space-3); }
  .ds-247420 .panel-body { padding: var(--space-2) var(--space-3); }

  /* Hero — fluid base font-size; only width unconstrained on tablet. */
  .ds-247420 .ds-hero { padding: var(--space-7) 0 var(--space-6); max-width: 100%; }
  .ds-247420 .ds-hero-title { max-width: 100%; }

  /* Row */
  .ds-247420 .row {
    grid-template-columns: minmax(90px, 12ch) minmax(0, 1fr) auto;
    /* 14px block (no tier between 12 and 16) is the tablet row height tuned
       against the 12ch code column; snapping re-pitches the whole list. */
    gap: var(--space-2); padding: 14px var(--space-3);
  }
}

/* --------------------------------------------------------------------
   Mid Breakpoint (768px and below) — collapse sidebar + simplify row grid
   ---------------------------------------------------------------------- */
@media (max-width: 768px) {
  /* Sidebar stacks above main rather than sitting beside it */
  .ds-247420 .app-body { grid-template-columns: 1fr; }
  .ds-247420 .app-side { width: 100%; max-width: none; }

  /* Row drops the leading code column; title + meta share the line */
  .ds-247420 .row {
    grid-template-columns: minmax(0, 1fr) auto;
    gap: var(--space-2);
  }
  .ds-247420 .row .code { display: none; }
  /* Changelog's date lives in span.code — hiding it is data loss, not reflow. */
  .ds-247420 .ds-changelog-row .code { display: inline; font-size: var(--fs-micro); }
  .ds-247420 .row .sub { grid-column: 1 / -1; }
}

/* --------------------------------------------------------------------
   Desktop Breakpoint Enhancements (1025px and up)
   ---------------------------------------------------------------------- */
@media (min-width: 1025px) {
  /* Chat Bubbles */
  .ds-247420 .chat-stack { max-width: min(70%, 480px); }

  /* KPI Cards */
  .ds-247420 .kpi {
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
    gap: var(--space-3);
  }
}

/* ============================================================
   Chat — minimal styles (placeholder for full chat surface)
   ============================================================ */
.ds-247420 .ds-chat { display: flex; flex-direction: column; gap: 12px; padding: var(--space-4); }
.ds-247420 .ds-chat-msg { padding: 12px 18px; background: var(--bg-2); border-radius: var(--r-3); max-width: min(90vw, 36em); }
.ds-247420 .ds-chat-msg.user { background: var(--accent-tint); color: var(--fg); align-self: flex-end; }
.ds-247420 .ds-chat-md { font-size: var(--fs-sm); line-height: 1.6; }
.ds-247420 .ds-chat-composer { display: flex; gap: 8px; padding: var(--space-4); }
.ds-247420 .ds-chat-composer input { flex: 1; padding: 13px 18px; background: var(--bg-2); border: 0; border-radius: var(--r-pill); font-family: inherit; font-size: var(--fs-sm); color: var(--fg); }
.ds-247420 .ds-chat-composer input:focus-visible { box-shadow: var(--focus-ring-inset); }

/* ============================================================
   File surface — responsive grid + row layouts
   ============================================================ */

/* Default file grid — a vertical list of rows. Each row lays out its own
   columns internally; the container only stacks them. Card mode (multi-column)
   is opt-in via [data-columns]. */
.ds-247420 .ds-file-grid {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
.ds-247420 .ds-file-grid:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); border-radius: var(--r-2); }

/* Listing wrapper holds the optional filter + sort header above the grid. */
.ds-247420 .ds-file-listing { display: flex; flex-direction: column; gap: var(--space-2); min-height: 0; }
/* The quick filter is a compact control, not a full-width form row - a giant
   input above the grid reads as a search page and costs fold height. */
/* The filter is now an inline item in the single .ds-file-controls toolbar row
   (the standalone right-aligned .ds-file-filter strip was removed). */
.ds-247420 .ds-file-filter-input {
  width: min(280px, 100%); padding: var(--space-1-75) var(--space-2-75); font-size: var(--fs-sm);
  background: var(--bg); color: var(--fg);
  border: var(--bw-hair) solid var(--rule); border-radius: var(--r-1);
}
.ds-247420 .ds-file-controls .ds-file-filter-wrap { flex: 0 1 220px; display: contents; }
.ds-247420 .ds-file-controls .ds-file-filter-wrap .ds-file-filter-input { flex: 0 1 220px; }
.ds-247420 .ds-file-filter-input:focus-visible { outline: none; box-shadow: var(--focus-ring-inset); }
.ds-247420 .ds-file-sort { display: flex; gap: var(--space-2); padding: 0 var(--space-2) var(--space-1); }
.ds-247420 .ds-file-sort-btn {
  font-size: var(--fs-tiny); font-family: var(--ff-mono); color: var(--fg-3);
  background: none; border: none; cursor: pointer; padding: var(--space-hair) var(--space-1); border-radius: var(--r-1);
}
.ds-247420 .ds-file-sort-btn:hover { color: var(--fg-2); background: var(--bg-2); }
.ds-247420 .ds-file-sort-btn.active { color: var(--accent-ink); }
.ds-247420 .ds-file-sort-btn:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }

/* Skeleton shimmer rows shown while a directory loads. */
.ds-247420 .ds-file-row-skeleton { cursor: default; pointer-events: none; }
/* In-place refresh: dim the populated grid instead of swapping it for shimmer
   rows (the rows stay; the round-trip reads as a settle, not a reload). */
.ds-247420 .ds-file-grid.is-refreshing { opacity: .6; }
@media (prefers-reduced-motion: no-preference) {
  .ds-247420 .ds-file-grid { transition: opacity .15s var(--ease); }
}
/* EventList loading skeleton (shares the .ds-skel shimmer base above). */
.ds-247420 .ds-event-row-skeleton { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-2-75) var(--space-3); pointer-events: none; }
.ds-247420 .ds-skel-rank { height: 12px; width: 2.5ch; }
.ds-247420 .ds-event-state { padding: var(--space-2) var(--space-3); }
.ds-247420 .ds-skel { display: inline-block; border-radius: var(--r-1); background: var(--bg-2); position: relative; overflow: hidden; }
.ds-247420 .ds-skel::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, var(--bg-3), transparent);
  transform: translateX(-100%);
}
@media (prefers-reduced-motion: no-preference) {
  .ds-247420 .ds-skel::after { animation: ds-skel-shimmer 1.2s infinite; }
}
.ds-247420 .ds-skel-icon { width: 22px; height: 22px; }
.ds-247420 .ds-skel-title { height: 12px; width: 60%; }
.ds-247420 .ds-skel-meta { height: 10px; width: 64px; margin-left: auto; }
@keyframes ds-skel-shimmer { 100% { transform: translateX(100%); } }
@media (prefers-reduced-motion: reduce) { .ds-247420 .ds-skel::after { animation: none; } }

/* File row — linear layout (checkbox? · open-button[code · icon · title · meta] · actions).
   The row is a flex strip; the open <button> is the stretching member and lays
   out the entry's own columns. It needs a full reset (UA button chrome off)
   because FileRow wraps the row content in a real button for native keyboard
   semantics. */
.ds-247420 .ds-file-row {
  display: flex; align-items: center; gap: var(--space-2-5);
  /* 11px vertical is an optical half-step between --space-2-5 (10) and
     --space-2-75 (12), deliberately off-scale: with the 26px .ds-file-icon
     and the hairline borders it lands the row at ~50px, comfortably over the
     44px touch floor without the 52px a 12px pad would give. Snapping either
     way visibly re-pitches every row in a long listing. */
  padding: 11px var(--space-3); background: var(--bg);
  border: var(--bw-hair) solid transparent;
  border-radius: var(--r-2); color: var(--fg);
  cursor: pointer;
  transition: background var(--dur-snap) var(--ease),
              border-color var(--dur-snap) var(--ease),
              transform var(--dur-snap) var(--ease);
}
.ds-247420 .ds-file-open {
  display: flex; align-items: center; gap: var(--space-2-75);
  flex: 1 1 auto; min-width: 0;
  background: none; border: 0; padding: 0; margin: 0;
  color: inherit; font: inherit; text-align: left; cursor: pointer;
}
.ds-247420 .ds-file-open[disabled] { cursor: default; }
.ds-247420 .ds-file-open:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); border-radius: var(--r-1); }
.ds-247420 .ds-file-row:hover {
  background: var(--bg-2);
  border-color: var(--rule);
}
.ds-247420 .ds-file-row:active { transform: translateY(1px); }
/* file-grid's own header/caption both claim NEVER A BORDER -- border-color
   here silently broke that promise for exactly the row it's meant to call
   out (the row a screenshot/manual test would actually look at). Selected
   state stays legible via the tint alone; border-color stays transparent so
   the base rule's promise holds for every row, active or not. */
.ds-247420 .ds-file-row.active { background: var(--accent-tint); }
.ds-247420 .ds-file-row[data-file-type="dir"]      { background: color-mix(in oklab, var(--accent) 5%, var(--bg)); }
.ds-247420 .ds-file-row[data-file-type="image"],
.ds-247420 .ds-file-row[data-file-type="video"],
.ds-247420 .ds-file-row[data-file-type="audio"],
.ds-247420 .ds-file-row[data-file-type="code"],
.ds-247420 .ds-file-row[data-file-type="text"],
.ds-247420 .ds-file-row[data-file-type="archive"],
.ds-247420 .ds-file-row[data-file-type="document"],
.ds-247420 .ds-file-row[data-file-type="symlink"],
.ds-247420 .ds-file-row[data-file-type="other"]    { border-left-width: 3px; border-left-color: var(--rule); }

/* Row internals — these are the classes FileRow actually renders. */
.ds-247420 .ds-file-row .title {
  font-weight: 500; font-size: var(--fs-sm);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  min-width: 0; flex: 1 1 auto;
}
.ds-247420 .ds-file-row .code {
  font-family: var(--ff-mono); font-size: var(--fs-micro);
  color: var(--fg-3);
}
.ds-247420 .ds-file-row .ds-file-meta {
  font-family: var(--ff-mono); font-size: var(--fs-xs);
  color: var(--fg-3); white-space: nowrap;
  /* Never shrink below content: a flex sibling with white-space:nowrap and
     the default flex-shrink:1 silently overflows its own box (no ellipsis)
     when the row runs out of width, painting the date/size over the title
     and action icons at mobile widths. Fixed-size the meta column instead
     and let .title (flex:1, min-width:0, ellipsis) absorb the squeeze. */
  flex-shrink: 0;
}

/* File icon — glyph chip, tinted per type. */
.ds-247420 .ds-file-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; font-size: var(--fs-sm); line-height: 1;
  color: var(--fg-2);
}
.ds-247420 .ds-file-row[data-file-type="dir"]      .ds-file-icon { color: var(--accent-ink); }
.ds-247420 .ds-file-row[data-file-type="image"]    .ds-file-icon { color: var(--mascot); }
.ds-247420 .ds-file-row[data-file-type="video"]    .ds-file-icon { color: var(--purple-2); }
.ds-247420 .ds-file-row[data-file-type="audio"]    .ds-file-icon { color: var(--sky); }
.ds-247420 .ds-file-row[data-file-type="code"]     .ds-file-icon { color: var(--green-2); }
.ds-247420 .ds-file-row[data-file-type="archive"]  .ds-file-icon { color: var(--flame); }
.ds-247420 .ds-file-row[data-file-type="document"] .ds-file-icon { color: var(--amber, var(--sun)); }

/* Row actions — a faint baseline keeps them discoverable at rest (not fully
   hidden, which left mouse/trackpad users unable to find rename/move/delete
   without accidentally hovering a row), full opacity on hover/focus. */
.ds-247420 .ds-file-actions {
  display: inline-flex; gap: var(--space-hair); align-items: center;
  opacity: 0.35; transition: opacity var(--dur-base) var(--ease);
}
.ds-247420 .ds-file-row:hover .ds-file-actions,
.ds-247420 .ds-file-row:focus-within .ds-file-actions { opacity: 1; }
.ds-247420 .ds-file-act {
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; padding: 0;
  background: transparent; border: 0; border-radius: var(--r-1);
  color: var(--fg-3); font-size: var(--fs-xs); cursor: pointer;
  transition: background var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease);
}
.ds-247420 .ds-file-act:hover { background: var(--bg-3); color: var(--fg); }
.ds-247420 .ds-file-act-warn:hover { background: color-mix(in oklab, var(--warn) 18%, var(--bg)); color: var(--warn); }

/* Multi-select — per-row checkbox, header tri-state, bulk action strip. */
/* CSS-drawn multi-select checkbox: a bordered box that fills with the accent +
   a border-drawn tick when its parent button is .is-marked or aria-checked.
   Shared by FileRow/FileCell (.is-marked) and the file/session select-all +
   SessionCard select (aria-checked true/mixed) since the bundle concatenates
   all CSS. Replaces the old [x]/[ ] bracket text - no glyphs, AT keeps the
   role=checkbox/aria-checked name+state.
   See also .ds-check in editor-primitives.css — a separate native-input
   checkbox family for form/settings contexts; intentionally distinct
   rendering (native vs CSS-drawn), do not let one drift toward the other
   by accident. */
.ds-247420 .ds-check-box {
  width: 15px; height: 15px; box-sizing: border-box; flex: 0 0 auto; position: relative;
  border: 1.5px solid var(--fg-3); border-radius: var(--r-0);
  transition: background var(--dur-snap) var(--ease), border-color var(--dur-snap) var(--ease);
}
.ds-247420 .is-marked > .ds-check-box,
.ds-247420[aria-checked="true"] > .ds-check-box { background: var(--fg); border-color: var(--fg); }
.ds-247420 .is-marked > .ds-check-box::after,
.ds-247420[aria-checked="true"] > .ds-check-box::after {
  content: ""; position: absolute; left: 4px; top: 1px; width: 4px; height: 8px;
  border: solid var(--bg); border-width: 0 1.5px 1.5px 0; transform: rotate(45deg);
}
.ds-247420[aria-checked="mixed"] > .ds-check-box { background: var(--fg); border-color: var(--fg); }
.ds-247420[aria-checked="mixed"] > .ds-check-box::after {
  content: ""; position: absolute; left: 3px; right: 3px; top: 6px; height: 2px; background: var(--bg); border-radius: var(--r-hair);
}

.ds-247420 .ds-file-check {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 28px; height: 28px; padding: 0 var(--space-1);
  background: transparent; border: 0; border-radius: var(--r-1);
  color: var(--fg-3);
  cursor: pointer;
  transition: background var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease);
}
.ds-247420 .ds-file-check:hover { background: var(--bg-3); color: var(--fg); }
.ds-247420 .ds-file-check.is-marked { color: var(--accent-ink); }
.ds-247420 .ds-file-check:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
.ds-247420 .ds-file-check[disabled] { opacity: 0.4; cursor: default; }
.ds-247420 .ds-file-row.is-marked { background: var(--accent-tint); border-color: var(--accent); }
.ds-247420 .ds-file-selectall {
  display: inline-flex; align-items: center; gap: var(--space-1);
  font-family: var(--ff-mono); font-size: var(--fs-tiny); color: var(--fg-2);
  cursor: pointer; background: none; border: none;
  padding: var(--space-1); border-radius: var(--r-1);
}
.ds-247420 .ds-file-selectall:hover { background: var(--bg-2); color: var(--fg); }
.ds-247420 .ds-file-selectall:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
.ds-247420 .ds-file-controls { display: flex; align-items: center; gap: var(--space-2); padding: 0 var(--space-2) var(--space-1); flex-wrap: wrap; }
.ds-247420 .ds-file-controls .spread { flex: 1 1 auto; }
.ds-247420 .ds-file-controls .ds-file-sort { padding: 0; }
.ds-247420 .ds-bulkbar {
  display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
  padding: var(--space-1) var(--space-2);
  background: var(--bg-2); border: var(--bw-hair) solid var(--rule); border-radius: var(--r-2);
}
.ds-247420 .ds-bulkbar-count { font-size: var(--fs-sm); font-weight: var(--fw-medium, 500); color: var(--fg-2); }
/* BulkBar is a toolbar (it even declares role=toolbar): compact rect buttons,
   quiet destructive outline - the app arm-confirms destructive bulk actions
   via ConfirmDialog, so the strip never needs a loud CTA. */
.ds-247420 .ds-bulkbar .btn, .ds-247420 .ds-bulkbar .btn-primary {
  padding: var(--space-1-5) var(--space-2-75); min-height: 32px; border-radius: var(--r-1); font-weight: 500;
}
.ds-247420 .ds-bulkbar .btn { background: transparent; border: var(--bw-hair) solid var(--rule); color: var(--fg-2); }
.ds-247420 .ds-bulkbar .btn:hover { background: var(--bg-2); color: var(--fg); }
.ds-247420 .ds-bulkbar .btn-primary.danger { background: transparent; color: var(--warn); border: var(--bw-hair) solid var(--warn); }
.ds-247420 .ds-bulkbar .btn-primary.danger:hover { background: color-mix(in oklab, var(--warn) 12%, transparent); color: var(--warn); }
@media (pointer: coarse) {
  .ds-247420 .ds-bulkbar .btn, .ds-247420 .ds-bulkbar .btn-primary { min-height: 44px; }
}

/* Density picker — list / compact / thumbnails. */
.ds-247420 .ds-density {
  display: inline-flex; gap: var(--space-hair); padding: var(--space-hair);
  background: var(--bg-2); border: var(--bw-hair) solid var(--rule); border-radius: var(--r-1);
}
.ds-247420 .ds-density-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; color: var(--fg-2);
  background: none; border: none; cursor: pointer;
  padding: 0; border-radius: calc(var(--r-1) - 2px);
  transition: background var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease);
}
.ds-247420 .ds-density-btn:hover { color: var(--fg); }
.ds-247420 .ds-density-btn.active { background: var(--accent-tint); color: var(--fg); }
.ds-247420 .ds-density-btn:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }

/* Compact density — tighter rows for long listings. */
.ds-247420 .ds-file-grid[data-density="compact"] { gap: var(--space-hair); }
.ds-247420 .ds-file-grid[data-density="compact"] .ds-file-row { padding: var(--space-1-5) var(--space-2-75); gap: var(--space-2-5); }
.ds-247420 .ds-file-grid[data-density="compact"] .ds-file-row .title { font-size: var(--fs-xs); }
.ds-247420 .ds-file-grid[data-density="compact"] .ds-file-row .ds-file-meta { font-size: var(--fs-micro); }
.ds-247420 .ds-file-grid[data-density="compact"] .ds-file-icon { width: 20px; height: 20px; font-size: var(--fs-tiny); }

/* Thumbnail density — tile grid; image entries carry a real lazy thumbnail. */
.ds-247420 .ds-file-grid-thumb {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
  gap: var(--space-3); align-items: start;
}
/* Mobile fallback: auto-fill can still pack 2 cramped columns down to ~320px
   viewports; force a single column below the shared mobile breakpoint
   (docs/responsive.md) so thumbnails and file rails always read as a list. */
@media (max-width: 480px) {
  .ds-247420 .ds-file-grid-thumb { grid-template-columns: 1fr; }
}
.ds-247420 .ds-file-cell {
  position: relative;
  background: var(--bg); border: var(--bw-hair) solid var(--rule); border-radius: var(--r-2);
  overflow: hidden;
  transition: border-color var(--dur-snap) var(--ease), background var(--dur-snap) var(--ease);
}
.ds-247420 .ds-file-cell:hover { border-color: var(--fg-3); }
.ds-247420 .ds-file-cell.active { border-color: var(--accent); background: var(--accent-tint); }
.ds-247420 .ds-file-cell.is-marked { border-color: var(--accent); background: var(--accent-tint); }
.ds-247420 .ds-file-cell.is-locked { opacity: 0.6; }
.ds-247420 .ds-file-row.is-locked { opacity: 0.6; }
.ds-247420 .ds-file-row.is-restricted .title { color: var(--fg-2); }
/* 1px vertical pad is deliberately below --space-hair (2px): this mono tag
   sits inline in a file row and must not add height to it; --space-hair would
   double the vertical pad and push the tag past the row cap height.
   A fourth, deliberately bordered/mono tag variant for file-permission tags
   specifically (distinct from .ds-badge for numeric counts and .chip for
   status/label words) - is-noaccess reuses the shared --flame tone token
   so 'no access' reads the same red as chip.tone-error/miss elsewhere. */
.ds-247420 .ds-file-perm-tag { font-family: var(--ff-mono); font-size: var(--fs-micro); color: var(--fg-3); padding: 1px var(--space-1-75); border: var(--bw-hair) solid var(--rule); border-radius: var(--r-1); white-space: nowrap; }
.ds-247420 .ds-file-perm-tag.is-noaccess { color: var(--flame); border-color: var(--flame); }
.ds-247420 .ds-file-cell-open {
  display: flex; flex-direction: column; align-items: stretch; gap: var(--space-1);
  width: 100%; padding: var(--space-1); margin: 0;
  background: none; border: none; cursor: pointer; color: var(--fg);
}
.ds-247420 .ds-file-cell-open[disabled] { cursor: default; }
.ds-247420 .ds-file-cell-open:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: -2px; border-radius: var(--r-2); }
.ds-247420 .ds-file-cell-media {
  display: flex; align-items: center; justify-content: center;
  width: 100%; aspect-ratio: 4 / 3;
  background: var(--bg-2); border-radius: var(--r-1); overflow: hidden;
}
/* 24px is the ICON size inside a 34px chip, not a type tier — it is matched to
   the chip box, not to the text scale (the --fs-* rungs jump 21px -> fluid
   heading clamps, so neither substitutes without resizing the icon). Same
   reasoning for the other four .ds-*-glyph rules in this sheet. */
.ds-247420 .ds-file-cell-media .ds-file-icon { width: 34px; height: 34px; font-size: 24px; }
.ds-247420 .ds-file-cell-thumb { width: 100%; height: 100%; object-fit: cover; }
/* Per-type icon tints on thumb tiles so the grid keeps the colour-coding the
   list rows already carry (mirrors .ds-file-row[data-file-type] .ds-file-icon). */
.ds-247420 .ds-file-cell[data-file-type="dir"]      .ds-file-icon { color: var(--accent-ink); }
.ds-247420 .ds-file-cell[data-file-type="image"]    .ds-file-icon { color: var(--mascot); }
.ds-247420 .ds-file-cell[data-file-type="video"]    .ds-file-icon { color: var(--purple-2); }
.ds-247420 .ds-file-cell[data-file-type="audio"]    .ds-file-icon { color: var(--sky); }
.ds-247420 .ds-file-cell[data-file-type="code"]     .ds-file-icon { color: var(--green-2); }
.ds-247420 .ds-file-cell[data-file-type="archive"]  .ds-file-icon { color: var(--flame); }
.ds-247420 .ds-file-cell[data-file-type="document"] .ds-file-icon { color: var(--amber); }
.ds-247420 .ds-file-cell[data-file-type="symlink"]  .ds-file-icon { color: var(--purple); }
.ds-247420 .ds-file-cell-name {
  font-size: var(--fs-tiny); text-align: left;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ds-247420 .ds-file-cell-meta { font-family: var(--ff-mono); font-size: var(--fs-micro); color: var(--fg-3); text-align: left; }
.ds-247420 .ds-file-cell-check {
  position: absolute; top: 4px; left: 4px; z-index: var(--z-raised);
  background: color-mix(in oklab, var(--bg) 78%, transparent);
}

/* ============================================================
   File browser surface — stage, toolbar, breadcrumb, dropzone,
   upload progress, empty state, modals, preview. Authored for
   fsbrowse but lives in the shared kit under .ds-247420 scope.
   ============================================================ */

/* Stage — the scrollable content column. */
.ds-247420 .ds-file-stage {
  display: flex; flex-direction: column; gap: var(--space-3);
  width: 100%; max-width: var(--stage-wide); margin: 0 auto;
  padding: var(--space-2) var(--space-3) var(--space-4);
}
/* When the stage is the app-main child, the shell already supplies top
   padding — collapse the stage's own so content isn't pushed down. */
.ds-247420 .app-main > div > .ds-file-stage,
.ds-247420 .app-main .ds-file-stage { padding-top: 0; }

/* Full-width files stack: the stage's space-3 vertical beat without its
   --measure-wide cap/auto-margins, so the roots / toolbar / bulkbar / uploads / grid
   bands keep one consistent rhythm instead of touching edge-to-edge. Carries
   the same side/bottom padding as .ds-file-stage (top collapses the same way
   when mounted directly under .app-main, which already supplies top padding)
   so switching a consumer from the capped stage to this full-width stack is a
   pure width change, not a spacing regression. */
.ds-247420 .ds-files-stack {
  display: flex; flex-direction: column; gap: var(--space-3); min-height: 0;
  padding: 0 var(--space-3) var(--space-4);
}
.ds-247420 .app-main > div > .ds-files-stack,
.ds-247420 .app-main .ds-files-stack { padding-top: 0; }
/* file_browser mounts this stack as the .app-main child directly carrying
   .ds-app-surface too (ui_kits/file_browser/app.js). .ds-app-surface sets
   display:flow-root (kits-appended.css) at equal specificity and loads
   later in the sheet, so on that element flow-root wins the cascade and
   the flex gap above silently collapses to zero — the crumb-path, toolbar,
   dropzone and grid rows jam edge-to-edge with no rhythm between them.
   Same mechanism as .ds-app-surface.aicat-focus-col in chat-polish.css;
   two-class selector required for the same reason. */
.ds-247420 .ds-app-surface.ds-files-stack {
  display: flex;
  flex-direction: column;
}

/* Breadcrumb path */
.ds-247420 .ds-crumb-path {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-hair);
  font-size: var(--fs-sm);
}
.ds-247420 .ds-crumb-seg {
  background: transparent; border: 0; cursor: pointer;
  padding: var(--space-1) var(--space-2); border-radius: var(--r-1);
  color: var(--fg-3); font-family: inherit; font-size: var(--fs-sm);
  transition: background var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease);
  /* A single long unbroken segment (hash-named dir) must never force the
     whole path past the viewport. */
  max-width: min(40vw, 220px);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ds-247420 .ds-crumb-seg:hover { background: var(--bg-2); color: var(--fg); }
.ds-247420 .ds-crumb-seg:focus-visible { outline: none; box-shadow: var(--focus-ring-inset); }
.ds-247420 .ds-crumb-seg.leaf { color: var(--fg); font-weight: 600; }
.ds-247420 .ds-crumb-sep { color: var(--fg-3); opacity: 0.6; user-select: none; }

/* Toolbar */
.ds-247420 .ds-file-toolbar {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-2); flex-wrap: wrap;
}
/* Dense page header: one row, small heading, single-line muted lede. The
   working surfaces (files, live, history, settings) lead with content, not a
   display H1 + paragraph that costs the first 150px of every fold. */
.ds-247420 .ds-page-header-dense { margin: 0 0 var(--space-3); }
.ds-247420 .ds-page-header-dense-row { display: flex; align-items: baseline; gap: var(--space-3); min-width: 0; }
.ds-247420 .ds-page-header-dense-row h1 {
  margin: 0; font-size: var(--fs-lg); line-height: 1.3; font-weight: 600;
  letter-spacing: normal; flex: 0 0 auto;
}
.ds-247420 .ds-page-header-dense-lede {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--fs-sm); color: var(--fg-3);
}
.ds-247420 .ds-page-header-dense .ds-page-header-right { flex: 0 0 auto; margin-left: auto; }
/* Below ~480px the single-line ellipsis truncates the majority of the lede
   text with no way to recover it (no title attr, no expand) - let the row
   wrap and the lede break onto its own line instead of clipping. */
@media (max-width: 480px) {
  .ds-247420 .ds-page-header-dense-row { flex-wrap: wrap; }
  .ds-247420 .ds-page-header-dense-lede {
    flex-basis: 100%; overflow: visible; text-overflow: clip; white-space: normal;
  }
}
.ds-247420 .ds-file-toolbar-left,
.ds-247420 .ds-file-toolbar-right {
  display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
}
/* Toolbar-scoped buttons: quiet, compact, rectangular - a toolbar is chrome,
   not a CTA row; the stadium pill stays for real page-level actions. */
.ds-247420 .ds-file-toolbar .btn, .ds-247420 .ds-file-toolbar .btn-ghost {
  padding: var(--space-1-5) var(--space-2-75); min-height: 32px;
  border-radius: var(--r-1);
  font-weight: 500;
  background: transparent;
  border: var(--bw-hair) solid var(--rule); color: var(--fg-2);
}
.ds-247420 .ds-file-toolbar .btn:hover, .ds-247420 .ds-file-toolbar .btn-ghost:hover { background: var(--bg-2); color: var(--fg); }
@media (pointer: coarse) {
  .ds-247420 .ds-file-toolbar .btn, .ds-247420 .ds-file-toolbar .btn-ghost { min-height: 44px; }
}
.ds-247420 .ds-meta-mono {
  font-family: var(--ff-mono); font-size: var(--fs-xs);
  color: var(--fg-3); letter-spacing: var(--tr-caps);
}

/* Error banner — dismissible toast row. */
.ds-247420 .ds-error-banner {
  padding: var(--space-2-5) var(--space-2-75); border-radius: var(--r-2);
  background: color-mix(in oklab, var(--warn) 14%, var(--bg));
  border: var(--bw-hair) solid color-mix(in oklab, var(--warn) 40%, transparent);
  color: var(--warn); font-size: var(--fs-sm); cursor: pointer;
}
.ds-247420 .ds-error-banner:hover { background: color-mix(in oklab, var(--warn) 20%, var(--bg)); }

/* Dropzone — a tonal target, not a dashed-border affordance (preview/
   dropzone.html's own header/caption both describe this: "NO DASHED
   BORDERS" / "tonal panel that swaps to --panel-select on dragover"). A
   solid hairline plus the idle->dragover background swap is the actual
   design; dashed was this system's OTHER "drop target" vocabulary
   (.panel-docs, kits-appended.css) leaking in here by copy-paste. */
.ds-247420 .ds-dropzone {
  border: var(--bw-hair) solid var(--rule);
  border-radius: var(--r-3); background: var(--bg);
  transition: border-color var(--dur-base) var(--ease), background var(--dur-base) var(--ease);
}
/* Idle hover: a quiet pre-drag affordance (border firms up, faint tint) so a
   pointer resting over the zone reads as "droppable" before a drag even
   starts — distinct from and weaker than the .dragover treatment below. */
.ds-247420 .ds-dropzone:hover {
  border-color: var(--fg-3);
  background: var(--bg-2);
}
.ds-247420 .ds-dropzone-inner {
  display: flex; align-items: center; justify-content: center;
  gap: var(--space-2); flex-wrap: wrap;
  padding: var(--space-4) var(--space-3); text-align: center;
}
/* Icon size, not type — see .ds-file-cell-media .ds-file-icon above. */
.ds-247420 .ds-dropzone-glyph { font-size: var(--icon-lg, 24px); color: var(--fg-3); }
.ds-247420 .ds-dropzone-label { font-size: var(--fs-sm); color: var(--fg-3); }
.ds-247420 .ds-dropzone:not(.dragover):not(.rejected):hover {
  border-color: var(--fg-3);
}
.ds-247420 .ds-dropzone.dragover {
  border-color: var(--accent);
  background: var(--accent-tint);
}
@media (prefers-reduced-motion: no-preference) {
  .ds-247420 .ds-dropzone.dragover { animation: ds-dropzone-pulse 1.1s var(--ease) infinite; }
}
.ds-247420 .ds-dropzone.dragover .ds-dropzone-glyph,
.ds-247420 .ds-dropzone.dragover .ds-dropzone-label { color: var(--accent-ink); }
@keyframes ds-dropzone-pulse {
  0%, 100% { border-color: var(--accent); }
  50% { border-color: color-mix(in oklab, var(--accent) 45%, transparent); }
}
/* Rejected — an in-flight drag carrying a disallowed payload (wrong type,
   over the size cap). Distinct from dragover: warn tone, not accent, so the
   two "something is happening" states never read as the same affordance. */
.ds-247420 .ds-dropzone.rejected {
  border-color: var(--warn);
  background: color-mix(in oklab, var(--warn) 10%, var(--bg));
}
@media (prefers-reduced-motion: no-preference) {
  .ds-247420 .ds-dropzone.rejected { animation: ds-dropzone-shake 0.4s var(--ease); }
}
.ds-247420 .ds-dropzone.rejected .ds-dropzone-glyph,
.ds-247420 .ds-dropzone.rejected .ds-dropzone-label { color: var(--warn); }
@keyframes ds-dropzone-shake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-3px); }
  75% { transform: translateX(3px); }
}
.ds-247420 .ds-dropzone--wrap.rejected > .ds-dropzone-inner {
  outline-color: var(--warn);
}
/* Wrapper form (has children): no permanent chrome - the content renders as if
   the zone were not there, and the dashed affordance overlays it ONLY while a
   drag is in flight. */
.ds-247420 .ds-dropzone--wrap { position: relative; border: none; border-radius: 0; background: transparent; }
.ds-247420 .ds-dropzone--wrap > .ds-dropzone-inner { display: none; }
.ds-247420 .ds-dropzone--wrap.dragover > .ds-dropzone-inner {
  display: flex; position: absolute; inset: 0; z-index: var(--z-raised);
  margin: 0; padding: var(--space-3);
  align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--bg) 84%, transparent);
  outline: var(--bw-rule) dashed var(--accent); outline-offset: -6px;
  border-radius: var(--r-3);
}
.ds-247420 .ds-dropzone--wrap.dragover { animation: none; }
/* Touch devices can never fire dragover — keep the pick-files affordance
   visible as a compact static row so upload stays reachable in wrap mode. */
@media (hover: none) and (pointer: coarse) {
  .ds-247420 .ds-dropzone--wrap > .ds-dropzone-inner {
    display: flex; position: static; padding: var(--space-2);
    outline: none; background: transparent;
  }
}

/* Upload progress */
.ds-247420 .ds-upload-progress { display: flex; flex-direction: column; gap: var(--space-1-75); }
.ds-247420 .ds-upload-item {
  display: grid; grid-template-columns: minmax(0, 1fr) 120px 44px;
  gap: var(--space-2); align-items: center;
  padding: var(--space-2) var(--space-2-75); border-radius: var(--r-2);
  background: var(--bg-2); font-size: var(--fs-xs);
}
.ds-247420 .ds-upload-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ds-247420 .ds-upload-bar {
  height: 6px; border-radius: var(--r-pill);
  background: var(--bg-3); overflow: hidden;
}
.ds-247420 .ds-upload-fill {
  display: block; height: 100%; width: 0;
  background: var(--fg-3); border-radius: var(--r-pill);
  transition: width var(--dur-base) var(--ease);
}
.ds-247420 .ds-upload-fill.is-active { background: var(--accent); }
/* Indeterminate: percent is unknown mid-transfer (fetch gives no chunk-level
   progress) — a sliding marquee bar reads as "working", not stuck at 0%. */
.ds-247420 .ds-upload-bar.indeterminate .ds-upload-fill {
  width: 40%; transition: none;
}
@media (prefers-reduced-motion: no-preference) {
  .ds-247420 .ds-upload-bar.indeterminate .ds-upload-fill {
    animation: ds-upload-marquee 1.1s ease-in-out infinite;
  }
}
@keyframes ds-upload-marquee {
  0% { transform: translateX(-100%); }
  100% { transform: translateX(250%); }
}
@media (prefers-reduced-motion: reduce) {
  .ds-247420 .ds-upload-bar.indeterminate .ds-upload-fill { animation: none; width: 100%; opacity: 0.5; }
}
.ds-247420 .ds-upload-pct {
  font-family: var(--ff-mono); font-size: var(--fs-micro);
  color: var(--fg-3); text-align: right;
}
.ds-247420 .ds-upload-item.done .ds-upload-fill { background: var(--success); }
.ds-247420 .ds-upload-item.done .ds-upload-pct { color: var(--green); }
.ds-247420 .ds-upload-item.error .ds-upload-fill { background: var(--warn); }
.ds-247420 .ds-upload-item.error .ds-upload-pct { color: var(--warn); }
/* Per-row upload recovery actions (replace / dismiss) — error rows are not dead ends. */
.ds-247420 .ds-upload-item { grid-template-columns: minmax(0, 1fr) minmax(60px, 120px) 44px auto; }
@media (max-width: 480px) {
  .ds-247420 .ds-upload-item { grid-template-columns: minmax(0, 1fr) 44px; }
  .ds-247420 .ds-upload-bar { grid-column: 1 / -1; order: 3; }
  .ds-247420 .ds-upload-actions { grid-column: 1 / -1; justify-content: flex-end; }
}
.ds-247420 .ds-upload-actions { display: inline-flex; gap: var(--space-1); justify-content: flex-end; }
.ds-247420 .ds-upload-act {
  padding: var(--space-hair) var(--space-2-5); min-height: 24px; cursor: pointer;
  background: var(--bg); border: var(--bw-hair) solid var(--rule); border-radius: var(--r-1);
  color: var(--fg-2); font-family: var(--ff-body); font-size: var(--fs-micro);
}
.ds-247420 .ds-upload-act:hover { background: var(--bg-3); color: var(--fg); }
.ds-247420 .ds-upload-act:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
@media (hover: none), (pointer: coarse) {
  .ds-247420 .ds-upload-act { min-height: 44px; min-width: 44px; }
  /* 44px floor for file controls on ANY touch device, not only narrow ones
     (the width-based mobile block misses wide touch tablets). */
  .ds-247420 .ds-file-act, .ds-247420 .ds-file-sort-btn, .ds-247420 .ds-crumb-seg, .ds-247420 .ds-file-filter-input { min-height: 44px; }
  .ds-247420 .ds-file-act { min-width: 44px; }
  .ds-247420 .ds-file-check, .ds-247420 .ds-file-selectall, .ds-247420 .ds-density-btn { min-height: 44px; min-width: 44px; }
}

/* Empty state */
.ds-247420 .ds-file-empty {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--space-2); padding: var(--space-6) var(--space-3);
  color: var(--fg-3); text-align: center;
}
/* Icon size, not type — see .ds-file-cell-media .ds-file-icon above. */
.ds-247420 .ds-file-empty-glyph { font-size: 34px; opacity: 0.55; }
.ds-247420 .ds-file-empty-text { font-size: var(--fs-sm); }

/* -- Modals ------------------------------------------------- */
.ds-247420 .ds-modal-backdrop {
  position: fixed; inset: 0; z-index: var(--z-modal);
  display: flex; align-items: center; justify-content: center;
  /* Notch/home-indicator safe: near-fullscreen modals must not slide under. */
  padding: max(var(--space-3), env(safe-area-inset-top))
           max(var(--space-3), env(safe-area-inset-right))
           max(var(--space-3), env(safe-area-inset-bottom))
           max(var(--space-3), env(safe-area-inset-left));
  background: color-mix(in oklab, var(--ink) 55%, transparent);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
}
@media (prefers-reduced-motion: no-preference) {
  .ds-247420 .ds-modal-backdrop { animation: ds-modal-fade var(--dur-base) var(--ease); }
}
@keyframes ds-modal-fade { from { opacity: 0; } to { opacity: 1; } }
.ds-247420 .ds-modal {
  display: flex; flex-direction: column;
  width: 100%; max-width: 460px;
  max-height: calc(100vh - 2 * var(--space-4));
  max-height: calc(100dvh - 2 * var(--space-4));
  background: var(--bg); color: var(--fg);
  border: var(--bw-hair) solid var(--rule);
  border-radius: var(--r-3); box-shadow: var(--shadow-3);
  overflow: hidden;
}
@media (prefers-reduced-motion: no-preference) {
  .ds-247420 .ds-modal { animation: ds-modal-rise var(--dur-base) var(--ease); }
}
@keyframes ds-modal-rise {
  from { opacity: 0; transform: translateY(8px) scale(0.98); }
  to   { opacity: 1; transform: none; }
}
.ds-247420 .ds-modal-small { max-width: 380px; }
.ds-247420 .ds-modal-preview { max-width: 880px; }
.ds-247420 .ds-modal-head {
  padding: var(--space-3) var(--space-4);
  font-weight: 600; font-size: var(--fs-body);
  border-bottom: var(--bw-hair) solid var(--rule);
}
.ds-247420 .ds-modal-body { padding: var(--space-4); overflow: auto; font-size: var(--fs-sm); }
.ds-247420 .ds-modal-actions {
  display: flex; justify-content: flex-end; gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  border-top: var(--bw-hair) solid var(--rule);
}
.ds-247420 .ds-modal-input {
  width: 100%; padding: var(--space-2-5) var(--space-2-75);
  background: var(--bg-2); border: var(--bw-hair) solid var(--rule);
  border-radius: var(--r-2); color: var(--fg);
  font-family: inherit; font-size: var(--fs-sm);
}
.ds-247420 .ds-modal-input:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
/* iOS auto-zooms inputs under 16px on focus, breaking the modal layout.
   Deliberately a literal, NOT var(--fs-body): --fs-body is re-declared to
   15px under [data-typescale="sm"], which would silently drop this input back
   under the zoom threshold on any density-reduced page. The 16px here is a
   platform constant, not a point on the type scale. */
@media (hover: none), (pointer: coarse) {
  .ds-247420 .ds-modal-input { font-size: 16px; min-height: 44px; }
}
/* PromptDialog's optional roots-chip row (destination-path prompts with more
   than one allowed root) - same chip idiom the cwd editor's roots row uses. */
.ds-247420 .ds-prompt-roots { display: flex; align-items: center; gap: var(--space-1-75); flex-wrap: wrap; margin-top: var(--space-2); }
.ds-247420 .ds-prompt-root-chip {
  background: var(--bg-2); border: var(--bw-hair) solid var(--rule); color: var(--fg-2);
  border-radius: var(--r-pill); padding: var(--space-half) var(--space-2-5); font: inherit; font-size: var(--fs-tiny);
  cursor: pointer;
}
.ds-247420 .ds-prompt-root-chip:hover { border-color: var(--accent); color: var(--fg); }
.ds-247420 .ds-prompt-root-chip:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
@media (pointer: coarse) { .ds-247420 .ds-prompt-root-chip { min-height: 44px; } }
/* In-body modal error (role=alert): mutation failures (409/403) surface INSIDE
   the dialog, not behind the fixed overlay. */
.ds-247420 .ds-modal-error {
  margin: var(--space-2) 0 0; padding: var(--space-2) var(--space-2-5);
  border: var(--bw-hair) solid var(--flame); border-radius: var(--r-2);
  background: color-mix(in oklab, var(--flame) 12%, var(--bg));
  color: var(--flame); font-size: var(--fs-xs);
}
/* --ink, not --on-color: --on-color is a fixed #fff, which reads fine on a dark
   fill but measures 3.07:1 on --warn (#FF5A52, a light red) -- below the 4.5:1
   floor. --ink on the same fill measures 6.27:1. Same principle as the
   --accent/--accent-ink split the token layer documents: a light saturated fill
   takes the dark text tone. Measured in a live browser. */
/* Severity is never color-alone (gd-14, color-blindness/grayscale-print safe):
   the bold weight + widened letter-spacing is a shape cue that survives a
   colorless render, matching how .btn-primary.danger differs from every
   other .btn-primary at a glance even with color stripped out. */
.ds-247420 .btn-primary.danger {
  background: var(--warn);
  border-color: var(--warn);
  color: var(--warn-fg);
  box-shadow: inset 0 0 0 var(--bw-hair, 1px) color-mix(in oklab, var(--warn-fg) 25%, transparent);
  font-weight: 700;
  letter-spacing: 0.01em;
}
.ds-247420 .btn-primary.danger:hover { filter: brightness(0.92); border-color: var(--warn); }

/* -- Preview ------------------------------------------------ */
.ds-247420 .ds-preview-head {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-bottom: var(--bw-hair) solid var(--rule);
}
.ds-247420 .ds-preview-name {
  font-weight: 600; font-size: var(--fs-body);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
  flex: 1 1 auto;
}
.ds-247420 .ds-preview-meta {
  font-family: var(--ff-mono); font-size: var(--fs-xs);
  color: var(--fg-3); white-space: nowrap;
  flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis;
}
.ds-247420 .ds-preview-actions { display: inline-flex; gap: var(--space-hair); margin-left: auto; }
.ds-247420 .ds-preview-body {
  flex: 1; min-height: 0; overflow: auto;
  padding: var(--space-3) var(--space-4);
  display: flex; flex-direction: column;
}
.ds-247420 .ds-preview-media {
  max-width: 100%;
  max-height: 70vh; max-height: 70dvh;
  margin: 0 auto;
  border-radius: var(--r-2); object-fit: contain;
}
.ds-247420 .ds-preview-audio { width: 100%; }
.ds-247420 .ds-preview-code,
.ds-247420 .ds-preview-text {
  margin: 0; flex: 1; min-height: 0; overflow: auto;
  padding: var(--space-3);
  background: var(--ink); color: var(--paper);
  border-radius: var(--r-2);
  font-family: var(--ff-mono); font-size: var(--fs-xs); line-height: var(--lh-base);
  white-space: pre; tab-size: 2;
}
.ds-247420 .ds-preview-text { white-space: pre-wrap; word-break: break-word; }
.ds-247420 .ds-preview-code code, .ds-247420 .ds-preview-text code { font: inherit; color: inherit; background: none; }
/* wrap-lines toggle (ported from pi-web's DisplayMode word-wrap control) */
.ds-247420 .ds-preview-code.is-wrapped { white-space: pre-wrap; overflow-wrap: anywhere; }
.ds-247420 .ds-preview-wrap-toggle.active { color: var(--paper); background: color-mix(in oklab, var(--paper) 18%, transparent); }
/* source/preview mode switch (ported from pi-web's markdown/HTML DisplayMode
   tabs) — the host supplies already-rendered, already-sanitized HTML via
   `previewHtml`; this component only toggles visibility. */
.ds-247420 .ds-preview-mode-switch { display: inline-flex; gap: var(--space-1); margin-right: var(--space-1); }
.ds-247420 .ds-preview-mode-btn {
  font-family: var(--ff-mono); font-size: var(--fs-micro);
  padding: var(--space-1) var(--space-2); border-radius: var(--r-1);
  color: var(--paper-3); background: none; border: none; cursor: pointer;
}
.ds-247420 .ds-preview-mode-btn.active { color: var(--paper); background: color-mix(in oklab, var(--paper) 18%, transparent); }
.ds-247420 .ds-preview-html {
  flex: 1; min-height: 0; overflow: auto;
  padding: var(--space-3) var(--space-4);
  background: var(--bg-0); border-radius: var(--r-2);
}
.ds-247420 .ds-preview-truncated {
  margin-top: var(--space-2); padding-top: var(--space-2);
  border-top: var(--bw-hair) solid color-mix(in oklab, var(--paper) 20%, transparent);
  color: var(--paper-3); font-size: var(--fs-micro);
}
.ds-247420 .ds-preview-fallback {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--space-2); padding: var(--space-6); color: var(--fg-3);
}
/* Icon size, not type — see .ds-file-cell-media .ds-file-icon above. */
.ds-247420 .ds-preview-glyph { font-size: 34px; opacity: 0.6; }

/* Thin scrollbars inside preview bodies. */
.ds-247420 .ds-preview-code, .ds-247420 .ds-preview-text, .ds-247420 .ds-modal-body, .ds-247420 .ds-preview-body {
  scrollbar-width: thin;
}
.ds-247420 .ds-preview-code::-webkit-scrollbar,
.ds-247420 .ds-preview-text::-webkit-scrollbar,
.ds-247420 .ds-preview-body::-webkit-scrollbar,
.ds-247420 .ds-modal-body::-webkit-scrollbar { width: 8px; height: 8px; }
.ds-247420 .ds-preview-code::-webkit-scrollbar-thumb,
.ds-247420 .ds-preview-text::-webkit-scrollbar-thumb {
  background: color-mix(in oklab, var(--paper) 28%, transparent);
  border-radius: var(--r-pill); border: 2px solid transparent; background-clip: padding-box;
}
.ds-247420 .ds-preview-body::-webkit-scrollbar-thumb,
.ds-247420 .ds-modal-body::-webkit-scrollbar-thumb {
  background: var(--bg-3); border-radius: var(--r-pill);
}

/* Mobile — modals go near-fullscreen, preview media shrinks. */
@media (max-width: 560px) {
  .ds-247420 .ds-modal {
    max-width: 100%;
    max-height: calc(100vh - 2 * var(--space-3));
    max-height: calc(100dvh - 2 * var(--space-3));
  }
  .ds-247420 .ds-modal-preview {
    width: 100%; max-height: none;
    height: calc(100vh - 2 * var(--space-3));
    height: calc(100dvh - 2 * var(--space-3));
  }
  .ds-247420 .ds-preview-media { max-height: 50vh; max-height: 50dvh; }
  .ds-247420 .ds-file-stage { padding: var(--space-3) var(--space-2); }
  /* Preview head: let it wrap and drop the nowrap meta (duplicated in body)
     so the close button can never be pushed past the modal edge. */
  .ds-247420 .ds-preview-head { flex-wrap: wrap; row-gap: var(--space-1); }
  .ds-247420 .ds-preview-meta { display: none; }
}

/* ============================================================
   Catalog / demo shell — a shared, compact framing for the preview/*.html
   component galleries, so each preview stops hand-rolling its own \3c style>
   (body padding, section margins, demo boxes). Mirrors webgeist's dense
   catalog: a 240px sticky sidebar + capped main + a hairline-bordered,
   flex-wrapping .ds-demo cell that packs variants inline and reflows.
   ============================================================ */
.ds-247420 .ds-catalog { display: flex; min-height: 100vh; gap: 0; }
.ds-247420 .ds-catalog-side {
  width: 240px; flex-shrink: 0;
  border-right: var(--bw-hair) solid var(--rule);
  padding: var(--space-3);
  position: sticky; top: 0; height: 100vh; overflow-y: auto;
}
.ds-247420 .ds-catalog-side h1 { font-size: var(--fs-lg); margin: 0 0 var(--space-1); }
.ds-247420 .ds-catalog-side .subtitle { font-size: var(--fs-micro); color: var(--fg-3); margin: 0 0 var(--space-4); }
.ds-247420 .ds-catalog-side nav h4 {
  font-size: var(--fs-micro); letter-spacing: var(--tr-tight); font-weight: 600;
  color: var(--fg-3); margin: var(--space-3) 0 var(--space-1);
}
.ds-247420 .ds-catalog-side nav a {
  display: block; padding: var(--space-half) 0; font-size: var(--fs-tiny);
  color: var(--fg-2); text-decoration: none;
}
.ds-247420 .ds-catalog-side nav a:hover { color: var(--accent-ink); }
.ds-247420 .ds-catalog-main { flex: 1; padding: var(--space-5); max-width: var(--stage-wide); min-width: 0; }
.ds-247420 .ds-catalog-section { margin-bottom: var(--space-5); padding-top: var(--space-3); border-top: var(--bw-hair) solid var(--rule); }
.ds-247420 .ds-catalog-section > h2 { font-size: var(--fs-h3-app); margin: 0 0 var(--space-3); }

/* The gallery cell: a hairline-bordered, flex-wrapping bin that packs a
   component's variants inline and reflows — the signature "showcase" frame. */
.ds-247420 .ds-demo {
  border: var(--bw-hair) solid var(--rule);
  border-radius: var(--r-1);
  padding: var(--space-3);
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2);
  background: var(--bg);
}
.ds-247420 .ds-demo + .ds-demo { margin-top: var(--space-2-75); }
.ds-247420 .ds-demo-label { font-size: var(--fs-xs); font-weight: 500; margin: 0 0 var(--space-1); }
.ds-247420 .ds-demo-desc { font-size: var(--fs-tiny); color: var(--fg-3); margin: 0 0 var(--space-2); }

/* Per-preview backlink to the full token map/doc — names which token
   families the specimen above it demonstrates, then links out to the
   generated theme-map for the resolved values. */
.ds-247420 .tokens-backlink { font-family: var(--ff-mono); font-size: var(--fs-xs); color: var(--fg-3); margin-top: 2px; }
.ds-247420 .tokens-backlink a { color: inherit; }

@media (max-width: 768px) {
  .ds-247420 .ds-catalog { flex-direction: column; }
  .ds-247420 .ds-catalog-side { width: 100%; height: auto; position: static; border-right: 0; border-bottom: var(--bw-hair) solid var(--rule); }
  .ds-247420 .ds-catalog-main { padding: var(--space-3); }
}

/* -- File browser UX affordances ---------------------------- */
/* Toolbar filter input — compact search box. */
.ds-247420 .ds-filter-input {
  width: clamp(120px, 22vw, 220px);
  padding: 7px 12px;
  background: var(--bg-2); border: var(--bw-hair) solid var(--rule);
  border-radius: var(--r-pill); color: var(--fg);
  font-family: inherit; font-size: var(--fs-xs);
}
.ds-247420 .ds-filter-input:focus-visible { outline: none; box-shadow: var(--focus-ring-inset); }

/* Loading skeleton: FileSkeleton renders `.ds-file-grid.ds-file-skeleton` with
   `.ds-file-row.ds-file-row-skeleton` rows whose inner `.ds-skel` children shimmer
   (via .ds-skel::after). The container therefore inherits the .ds-file-grid layout
   and must NOT be a single 48px gradient bar (that collapsed every row and double-
   shimmered). The dead .ds-file-grid-loading rule + unused ds-skeleton-shimmer
   keyframe were removed; only the inner .ds-skel children animate. */

/* Drag-to-move drop target highlight on a directory row -- RESTYLED:
   borderless, the tonal --accent-tint fill alone carries the feedback. */
.ds-247420 .ds-file-row.ds-drop-target {
  background: var(--accent-tint);
}
.ds-247420 .ds-file-row[draggable="true"] { cursor: grab; }
.ds-247420 .ds-file-row[draggable="true"]:active { cursor: grabbing; }

/* (A stale duplicate multi-select block used to live here: an absolute-overlay
   .ds-file-check hidden until hover, plus .ds-file-check.on / .ds-file-row.selected
   / .ds-bulk-bar rules NO component ever emits. Being later in source at equal
   specificity it clobbered the real in-flow checkbox at ~1243 - the shipped
   multi-select rendered invisible-until-hover with clipped brackets. The live
   rules are the earlier block; only the still-load-bearing bits remain below.) */
.ds-247420 .ds-file-row { position: relative; }
/* Touch / coarse-pointer devices have no hover — keep the per-row action
   buttons (rename/delete/download) visible so the file manager is fully
   reachable without a pointer (e.g. an iPad in landscape). */
@media (hover: none), (pointer: coarse) {
  .ds-247420 .ds-file-actions { opacity: 1; }
}

/* Keyboard-shortcuts hint — compact two-column legend. */
.ds-247420 .ds-shortcuts-hint {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 4px var(--space-3);
  padding: var(--space-3) var(--space-4);
  background: var(--bg-2); border: var(--bw-hair) solid var(--rule);
  border-radius: var(--r-2); font-size: var(--fs-xs);
}
.ds-247420 .ds-shortcut-row { display: flex; align-items: center; gap: var(--space-2); }
.ds-247420 .ds-shortcuts-hint .ds-kbd-label { flex: 1 1 auto; opacity: .85; }
.ds-247420 .ds-shortcuts-hint .ds-kbd { white-space: normal; max-width: 100%; }
.ds-247420 .ds-kbd-caps { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 4px; }
.ds-247420 .ds-kbd-sep { color: var(--fg-3); font-size: var(--fs-micro); padding: 0 2px; }
/* .ds-kbd base definition lives in editor-primitives.css (canonical home —
   it owns the fuller ds-kbd-group/ds-kbd-row family this key chip belongs
   to); this file only carries the .ds-shortcuts-hint contextual overrides
   above. */

/* ============================================================
   Theme toggle (segmented + compact) — bound to src/theme.js
   ============================================================ */
.ds-247420 .ds-segmented {
  display: inline-flex; gap: 0; padding: 2px;
  background: var(--bg-2); border-radius: var(--r-pill);
  font-family: var(--ff-mono); font-size: var(--fs-xs);
}
.ds-247420 .ds-segmented .ds-seg-btn {
  background: transparent; border: 0; color: var(--fg-3);
  padding: 4px 12px; border-radius: var(--r-pill); cursor: pointer;
  display: inline-flex; align-items: center; gap: 6px;
  transition: color var(--dur-base) var(--ease), background var(--dur-base) var(--ease);
}
.ds-247420 .ds-segmented .ds-seg-btn:hover { color: var(--fg); }
/* Touch floor — a segmented control is a primary mode switch but renders ~30px
   tall from its 4px padding, below the 44px minimum the rest of the system
   holds. Grows the hit area only; the pill's visual size is unchanged at
   fine-pointer sizes. */
@media (pointer: coarse) {
  .ds-247420 .ds-segmented .ds-seg-btn { min-height: 44px; padding-inline: var(--space-3); }
}
.ds-247420 .ds-segmented .ds-seg-btn:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
.ds-247420 .ds-segmented .ds-seg-btn.is-on {
  background: var(--bg); color: var(--fg);
  box-shadow: inset 0 0 0 var(--bw-hair, 1px) var(--rule);
}
.ds-247420 .ds-theme-toggle.btn {
  font-family: var(--ff-mono); font-size: var(--fs-xs);
  display: inline-flex; align-items: center; gap: 6px;
}
/* CSS-drawn disc mark on the compact theme toggle: legibility fallback for
   when BOTH the SVG glyph and text label are hidden (icon-only rail strip).
   The glyph (.ds-theme-toggle-icon) already conveys mode whenever it is
   visible, so the disc stays hidden alongside it and only shows in the
   rail-collapsed state where responsive2-workspace.css hides the icon too. */
.ds-247420 .ds-theme-disc { display: none; }
.ds-247420 .ds-theme-disc[data-mode="auto"] {
  background: linear-gradient(90deg, var(--fg-3) 0 50%, transparent 50% 100%);
}
.ds-247420 .ds-theme-disc[data-mode="paper"] {
  background: var(--bg); border-color: var(--fg-3);
}
.ds-247420 .ds-theme-disc[data-mode="ink"] {
  background: var(--fg-3); border-color: var(--fg-3);
}

/* ============================================================
   Chat kit polish — styles every class emitted by Chat /
   ChatMessage / ChatComposer / AICat. Before this block the
   chat-* classes had no CSS and rendered as unstyled paragraphs.
   ============================================================ */
.ds-247420 .chat {
  display: flex; flex-direction: column;
  flex: 1; min-height: 0; gap: var(--space-3);
}
.ds-247420 .chat-head {
  display: flex; align-items: baseline; gap: var(--space-2);
  padding: var(--space-3) 0 var(--space-2);
  font-family: var(--ff-mono); font-size: var(--fs-xs);
  color: var(--fg-3); text-transform: lowercase; letter-spacing: 0.02em;
  border-bottom: 1px solid color-mix(in oklab, var(--fg) 8%, transparent);
  margin-bottom: var(--space-2);
}
.ds-247420 .chat-head .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); flex-shrink: 0; }
.ds-247420 .chat-head .ds-chat-title,
.ds-247420 .chat-head > span:nth-child(2) {
  font-family: var(--ff-body); font-size: var(--fs-h4);
  font-weight: 600; color: var(--fg);
  text-transform: none; letter-spacing: var(--tr-tight, -0.01em); line-height: 1.2;
}
.ds-247420 .chat-head .sub { color: var(--fg-3); }
.ds-247420 .chat-head .spread { flex: 1; }

.ds-247420 .chat-thread {
  display: flex; flex-direction: column; gap: var(--space-3);
  flex: 1; min-height: 0; overflow-y: auto;
  padding: var(--space-2) 0;
  scrollbar-width: thin; scrollbar-color: var(--bg-3) transparent;
}
@media (prefers-reduced-motion: no-preference) {
  .ds-247420 .chat-thread { scroll-behavior: smooth; }
}
.ds-247420 .chat-thread::-webkit-scrollbar { width: 8px; }
.ds-247420 .chat-thread::-webkit-scrollbar-thumb { background: var(--bg-3); border-radius: var(--r-0); }
.ds-247420 .chat-empty {
  margin: auto;
  text-align: center;
  color: var(--fg-3);
  font-family: var(--ff-mono);
  pointer-events: none;
}
.ds-247420 .chat-empty-title { font-size: var(--fs-sm); margin: 0 0 var(--space-1); }
.ds-247420 .chat-empty-sub { font-size: var(--fs-tiny); margin: 0; opacity: .82; } /* WCAG-verified 2026 restyle: .7 blended --fg-3 to 3.72:1 on dark --bg (#1a1a1a); .82 clears 4.53:1 */

.ds-247420 .chat-msg { display: flex; flex-shrink: 0; gap: var(--space-2-75); align-items: flex-start; padding: var(--space-1-75) 0; position: relative; min-width: 0; }
.ds-247420 .chat-msg.you { flex-direction: row-reverse; }
.ds-247420 .chat-msg.you .chat-stack { align-items: flex-end; }
.ds-247420 .chat-msg:hover { background: color-mix(in oklab, var(--fg) 4%, transparent); padding: var(--space-1-75) 0; margin: 0; border-radius: var(--r-0); transition: background var(--dur-base) var(--ease); }

.ds-247420 .chat-avatar {
  width: 36px; height: 36px; flex-shrink: 0;
  border-radius: 50%; background: var(--bg-3); color: var(--fg);
  display: inline-flex; align-items: center; justify-content: center;
  font-family: var(--ff-body); font-size: var(--fs-micro); font-weight: var(--fw-medium, 500);
  text-transform: lowercase; letter-spacing: 0.02em; user-select: none;
  transition: transform 0.12s ease, box-shadow 0.12s ease;
  position: relative;
  box-shadow: inset 0 0 0 1px var(--rule);
}
.ds-247420 .chat-msg:hover .chat-avatar { transform: scale(1.03); box-shadow: 0 1px 4px color-mix(in oklab, var(--fg) 6%, transparent); }
.ds-247420 .chat-msg.you .chat-avatar {
  background: color-mix(in oklab, var(--accent) 18%, var(--bg-3));
  color: var(--accent-ink);
}
.ds-247420 .chat-msg.aicat .chat-avatar {
  background: color-mix(in oklab, var(--mascot) 26%, var(--bg-2));
  color: var(--mascot-deep);
}
/* Plain assistant turns ('them', no aicat persona) previously fell through to
   the bare .chat-avatar gray disc with no role identity at all — 'you' and
   'aicat' both got a tinted background + ring, the default assistant got
   nothing. Give it the same tinted-background treatment via --color-success
   (the conventional assistant/bot association, distinct from the accent hue
   'you' owns and the mascot hue 'aicat' owns) plus a thin identity ring so
   role is readable from the avatar alone, not just bubble position/color. */
.ds-247420 .chat-msg.them:not(.aicat) .chat-avatar {
  background: color-mix(in oklab, var(--color-success) 20%, var(--bg-3));
  color: color-mix(in oklab, var(--color-success) 65%, var(--fg));
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--color-success) 35%, transparent);
}

.ds-247420 .chat-stack {
  display: flex; flex-direction: column; gap: var(--space-1);
  max-width: min(70%, 56ch); min-width: 0;
  flex: 0 1 auto; overflow: hidden;
}

.ds-247420 .chat-bubble {
  /* 14px inline has no tier (--space-2-75 is 12, --space-3 is 16). It is the
     shared bubble/attachment inset used by .chat-file/.chat-pdf-head below;
     snapping only this one would visibly desync the bubble from its cards. */
  padding: var(--space-2-5) 14px; background: var(--bg-2); color: var(--fg);
  max-width: 100%;
  border-radius: var(--r-2); line-height: 1.55;
  word-wrap: break-word; overflow-wrap: anywhere;
  font-size: var(--fs-sm);
  min-width: 0;
  transition: transform 0.12s ease, box-shadow 0.12s ease;
}
/* Tail corner: the bubble edge nearest its own avatar (bottom-left for
   'them', bottom-right for 'you') squares off slightly, matching bubble
   position to the sender's avatar the way a speech-bubble tail would — a
   consistent radius on all four corners reads as a generic card, not a
   turn in a conversation. */
.ds-247420 .chat-msg:not(.you) .chat-bubble { border-bottom-left-radius: var(--r-0); }
.ds-247420 .chat-msg.you .chat-bubble { border-bottom-right-radius: var(--r-0); }
.ds-247420 .chat-msg:hover .chat-bubble { transform: translateY(-1px); box-shadow: 0 2px 8px color-mix(in oklab, var(--fg) 6%, transparent); transition: transform var(--dur-base) var(--ease), box-shadow var(--dur-base) var(--ease); }
/* Own bubble — calm tinted lead, not a full electric fill (chat stays readable). */
.ds-247420 .chat-msg.you .chat-bubble { background: var(--accent-tint); color: var(--fg); }
.ds-247420 .chat-msg.aicat .chat-bubble {
  background: color-mix(in oklab, var(--mascot) 14%, var(--bg-2));
  border: 1px solid color-mix(in oklab, var(--mascot) 28%, transparent);
}
.ds-247420 .chat-bubble code {
  /* em-relative on purpose: inline code tracks the size of the prose it sits
     inside (the mono face runs optically large next to the system stack, hence
     0.92). An absolute --fs-* tier would decouple it from its own paragraph. */
  font-family: var(--ff-mono); font-size: 0.92em;
  background: color-mix(in oklab, var(--fg) 10%, transparent);
  /* 1px block is deliberately under --space-hair (2px): inline <code> must not
     grow the line box of the prose it sits inside. */
  padding: 1px var(--space-1-75); border-radius: var(--r-0);
}
.ds-247420 .chat-msg.you .chat-bubble code { background: color-mix(in oklab, var(--fg) 10%, transparent); }
.ds-247420 .chat-bubble a {
  color: inherit; text-decoration: underline;
  text-underline-offset: 2px; text-decoration-thickness: 1px;
  text-decoration-color: color-mix(in oklab, currentColor 50%, transparent);
}
.ds-247420 .chat-bubble a:hover { text-decoration-color: currentColor; }
.ds-247420 .chat-bubble strong { font-weight: 700; }
.ds-247420 .chat-bubble em { font-style: italic; }

.ds-247420 .chat-bubble.chat-md { line-height: 1.6; }
.ds-247420 .chat-bubble.chat-md > :first-child { margin-top: 0; }
.ds-247420 .chat-bubble.chat-md > :last-child { margin-bottom: 0; }
.ds-247420 .chat-bubble.chat-md h1,
.ds-247420 .chat-bubble.chat-md h2,
.ds-247420 .chat-bubble.chat-md h3,
.ds-247420 .chat-bubble.chat-md h4 {
  font-family: var(--ff-body); font-weight: 600;
  margin: var(--space-2) 0 var(--space-1); line-height: var(--lh-snug);
}
/* em-relative on purpose, NOT --fs-h1..h3: markdown headings inside a chat
   bubble are a hierarchy WITHIN the bubble's own type size, not page headings.
   The --fs-h* tiers are fluid clamp()s tuned to page container queries and
   would render a bubble heading at 34px+, dwarfing the message it labels.
   These stay a tight ratio ladder off whatever size the bubble is set at. */
.ds-247420 .chat-bubble.chat-md h1 { font-size: 1.15em; }
.ds-247420 .chat-bubble.chat-md h2 { font-size: 1.08em; }
.ds-247420 .chat-bubble.chat-md h3 { font-size: 1.04em; }
.ds-247420 .chat-bubble.chat-md p { margin: var(--space-2) 0; }
.ds-247420 .chat-bubble.chat-md ul,

.ds-247420 .chat-bubble.chat-md ol { margin: var(--space-2) 0; padding-left: 22px; }
.ds-247420 .chat-bubble.chat-md li { margin: var(--space-1) 0; }
.ds-247420 .chat-bubble.chat-md blockquote {
  margin: var(--space-2) 0; padding: var(--space-1) var(--space-2-75);
  /* DELIBERATE, but slimmed to the --bw-rule tier. This is a real markdown
     <blockquote> inside a chat bubble: the left rule is the standard
     typographic quotation mark, not a colored alert stripe (the tone is a
     currentColor mix that inherits the bubble's own ink — never an accent).
     A 3px off-scale literal read as the generic tell; 2px reads as type. */
  border-left: var(--bw-rule) solid color-mix(in oklab, currentColor 30%, transparent);
  color: color-mix(in oklab, currentColor 75%, transparent); font-style: italic;
}
.ds-247420 .chat-bubble.chat-md pre {
  background: var(--bg);
  border: 1px solid color-mix(in oklab, var(--fg) 12%, transparent);
  padding: var(--space-2-75) var(--space-3); border-radius: var(--r-1); overflow-x: auto;
  font-family: var(--ff-mono); font-size: var(--fs-xs); line-height: 1.55;
  color: var(--fg); margin: var(--space-2) 0;
}
/* When injectCodeCopy has wrapped the fence (.chat-code-block), reserve room
   for the absolute .chat-code-lang tab. This selector outranks the bare
   .chat-code-block pre reservation in chat.css, which the 2-class md pre
   selector above would otherwise override. */
.ds-247420 .chat-code-block .chat-bubble.chat-md pre,
.ds-247420 .chat-bubble.chat-md .chat-code-block pre {
  padding-top: calc(var(--space-3) + 4px);
}
/* Markdown tables + horizontal rules: agents routinely emit them, but they were
   unstyled (bare unspaced cells, vanished rules). */
.ds-247420 .chat-bubble.chat-md table { border-collapse: collapse; width: auto; max-width: 100%; margin: var(--space-2) 0; font-size: .95em; /* em-relative: a table inside a bubble reads one notch down from the bubble's own copy, not off an absolute tier */ display: block; overflow-x: auto; }
.ds-247420 .chat-bubble.chat-md th,
.ds-247420 .chat-bubble.chat-md td { border: var(--bw-hair) solid var(--rule); padding: var(--space-1) var(--space-2-5); text-align: left; }
.ds-247420 .chat-bubble.chat-md th { background: color-mix(in oklab, var(--fg) 5%, transparent); font-weight: 600; }
.ds-247420 .chat-bubble.chat-md hr { border: 0; border-top: var(--bw-hair) solid var(--rule); margin: var(--space-2-75) 0; }

/* Mermaid diagram blocks — rendered SVG swapped in over the fenced source by
   src/mermaid.js's renderMermaidBlocksUnder(); the source .pre stays in the
   DOM (display:none) behind a source/diagram toggle button. */
.ds-247420 .ds-mermaid-block {
  position: relative; margin: var(--space-2) 0;
  border: 1px solid color-mix(in oklab, var(--fg) 12%, transparent);
  border-radius: var(--r-1); background: var(--bg); padding: var(--space-2-75) var(--space-3);
}
.ds-247420 .ds-mermaid-diagram { overflow-x: auto; display: flex; justify-content: center; }
.ds-247420 .ds-mermaid-diagram svg { max-width: 100%; height: auto; }
.ds-247420 .ds-mermaid-toggle {
  position: absolute; top: 6px; right: 6px;
  font: inherit; font-size: var(--fs-xs); line-height: 1;
  background: color-mix(in oklab, var(--fg) 8%, transparent);
  color: var(--fg); border: none; border-radius: var(--r-1);
  padding: var(--space-1) var(--space-2); cursor: pointer;
}
.ds-247420 .ds-mermaid-toggle:hover { background: color-mix(in oklab, var(--fg) 14%, transparent); }

/* Inline/display math — KaTeX HTML swapped into text nodes by
   src/math.js's renderMathBlocksUnder(). Display math centers on its own line;
   inline math sits within running text. */
.ds-247420 .ds-math-display { display: block; margin: var(--space-2) 0; text-align: center; overflow-x: auto; }
.ds-247420 .ds-math-inline { display: inline-block; }

.ds-247420 .chat-msg .chat-bubble.chat-code,
.ds-247420 .chat-msg.you .chat-bubble.chat-code,
.ds-247420 .chat-bubble.chat-code {
  padding: 0; background: var(--bg);
  color: var(--fg);
  border: 1px solid color-mix(in oklab, var(--fg) 12%, transparent);
  overflow: hidden; max-width: 100%; min-width: 0;
}
.ds-247420 .chat-bubble.chat-code pre,
.ds-247420 .chat-bubble pre,
.ds-247420 .chat-msg pre {
  max-width: 100%;
  overflow-x: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--bg-3) transparent;
}
.ds-247420 .chat-bubble pre code,
.ds-247420 .chat-msg pre code {
  display: block;
  white-space: pre;
  min-width: 0;
}
.ds-247420 .chat-code-head {
  display: flex; align-items: center; gap: var(--space-2-5);
  padding: var(--space-2) var(--space-2-75);
  background: color-mix(in oklab, var(--fg) 6%, var(--bg-2));
  border-bottom: 1px solid color-mix(in oklab, var(--fg) 12%, transparent);
  font-family: var(--ff-mono); font-size: var(--fs-xs); color: var(--fg-3);
}
.ds-247420 .chat-code-head .lang { text-transform: uppercase; font-weight: 700; color: var(--accent-ink); letter-spacing: 0.06em; }
.ds-247420 .chat-code-head .filename,
.ds-247420 .chat-code-head .name { color: var(--fg-2); font-weight: 500; }
.ds-247420 .chat-code-head .spread { flex: 1; }
.ds-247420 .chat-bubble.chat-code pre {
  margin: 0; padding: var(--space-2-75) var(--space-3); background: transparent;
  font-family: var(--ff-mono); font-size: var(--fs-xs); line-height: 1.55;
  overflow-x: auto; color: var(--fg); border-radius: 0;
}
.ds-247420 .chat-bubble.chat-code code { background: transparent; padding: 0; font-size: inherit; }
.ds-247420 .chat-bubble.chat-code .token.keyword,
.ds-247420 .chat-bubble.chat-code .token.atrule,
.ds-247420 .chat-bubble.chat-code .token.selector { color: var(--accent-ink); }
.ds-247420 .chat-bubble.chat-code .token.string,
.ds-247420 .chat-bubble.chat-code .token.attr-value { color: var(--code-str-alt, var(--mascot)); }
.ds-247420 .chat-bubble.chat-code .token.comment { color: var(--fg-3); font-style: italic; }
.ds-247420 .chat-bubble.chat-code .token.number,
.ds-247420 .chat-bubble.chat-code .token.boolean { color: var(--code-num, var(--sun)); }
.ds-247420 .chat-bubble.chat-code .token.punctuation { color: var(--fg-2); }
.ds-247420 .chat-bubble.chat-code .token.property,
.ds-247420 .chat-bubble.chat-code .token.attr-name { color: var(--code-keyword, var(--sky)); }

.ds-247420 .chat-msg.you .chat-image,
.ds-247420 .chat-msg .chat-image,
.ds-247420 .chat-image {
  display: inline-block; border-radius: var(--r-1); overflow: hidden;
  max-width: min(100%, 480px); background: var(--bg-2);
  text-decoration: none; color: var(--fg);
  border: 1px solid color-mix(in oklab, var(--fg) 10%, transparent);
  transition: box-shadow 0.15s ease, border-color 0.15s ease;
}
.ds-247420 .chat-msg:hover .chat-image,
.ds-247420 .chat-image:hover {
  border-color: var(--rule);
}
.ds-247420 .chat-image img { display: block; width: 100%; height: auto; max-height: 360px; object-fit: cover; }
.ds-247420 .chat-image .cap,
.ds-247420 .chat-image .caption { display: block; padding: var(--space-2) var(--space-2-75); font-size: var(--fs-xs); color: var(--fg-3); font-family: var(--ff-mono); }

.ds-247420 .chat-msg.you .chat-file,
.ds-247420 .chat-msg .chat-file,
.ds-247420 .chat-file {
  display: flex; align-items: center; gap: var(--space-2-75); padding: var(--space-2-75) 14px;
  background: var(--bg-2); border: 1px solid color-mix(in oklab, var(--fg) 10%, transparent);
  border-radius: var(--r-1); text-decoration: none; color: var(--fg);
  max-width: min(100%, 360px);
  transition: background var(--dur-snap, .15s) var(--ease, ease), border-color var(--dur-snap, .15s) var(--ease, ease);
}
.ds-247420 .chat-file:hover { background: var(--bg-3); }
.ds-247420 .chat-file .glyph {
  /* 22px is the ICON size inside a 36px chip — matched to the chip box, not to
     the type scale (--fs-xl is 21px, and above it every rung is a fluid
     heading clamp). Snapping it visibly resizes the mark. */
  font-size: 22px; width: 36px; height: 36px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--bg-3);
  color: var(--fg-2); border-radius: var(--r-0); flex-shrink: 0; font-family: var(--ff-mono);
}
.ds-247420 .chat-file > div { display: flex; flex-direction: column; gap: var(--space-hair); min-width: 0; }
.ds-247420 .chat-file .name {
  font-weight: 600; font-size: var(--fs-sm);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0;
}
.ds-247420 .chat-file .meta { display: flex; flex-direction: column; gap: var(--space-hair); min-width: 0; flex: 1; }
.ds-247420 .chat-file .size { font-family: var(--ff-mono); font-size: var(--fs-xs); color: var(--fg-3); }
.ds-247420 .chat-file .go { font-family: var(--ff-mono); font-size: var(--fs-body); color: var(--fg-3); flex-shrink: 0; }
.ds-247420 .chat-file:hover .go { color: var(--accent-ink); }

.ds-247420 .chat-msg.you .chat-pdf,
.ds-247420 .chat-msg .chat-pdf,
.ds-247420 .chat-pdf {
  background: var(--bg-2);
  color: var(--fg);
  border: 1px solid color-mix(in oklab, var(--fg) 10%, transparent);
  border-radius: var(--r-1); overflow: hidden; max-width: min(100%, 480px);
  transition: box-shadow 0.12s ease, border-color 0.12s ease;
}
.ds-247420 .chat-msg:hover .chat-pdf { border-color: var(--rule); }
.ds-247420 .chat-pdf-head {
  display: flex; align-items: center; gap: var(--space-2-5); padding: var(--space-2-5) 14px;
  background: color-mix(in oklab, var(--fg) 5%, var(--bg-2));
  border-bottom: 1px solid color-mix(in oklab, var(--fg) 10%, transparent);
}
.ds-247420 .chat-pdf-head .glyph { font-size: var(--fs-lg); color: var(--mascot); flex-shrink: 0; }
.ds-247420 .chat-pdf-head .name { flex: 1; font-weight: 600; font-size: var(--fs-sm); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ds-247420 .chat-pdf-head .size { font-family: var(--ff-mono); font-size: var(--fs-xs); color: var(--fg-3); }
.ds-247420 .chat-pdf-head a { color: var(--accent-ink); font-family: var(--ff-mono); font-size: var(--fs-xs); text-decoration: none; }
.ds-247420 .chat-pdf embed,
.ds-247420 .chat-pdf iframe,
.ds-247420 .chat-pdf object { width: 100%; height: 360px; border: 0; display: block; }

.ds-247420 .chat-msg.you .chat-link,
.ds-247420 .chat-msg .chat-link,
.ds-247420 .chat-link {
  display: grid; grid-template-columns: 80px 1fr; gap: var(--space-2-75); padding: var(--space-2-5);
  background: var(--bg-2);
  border: 1px solid color-mix(in oklab, var(--fg) 10%, transparent);
  border-radius: var(--r-1); text-decoration: none; color: var(--fg);
  max-width: min(100%, 480px);
  transition: background var(--dur-snap, .15s) var(--ease, ease), border-color var(--dur-snap, .15s) var(--ease, ease), box-shadow 0.15s ease;
}
.ds-247420 .chat-msg:hover .chat-link,
.ds-247420 .chat-link:hover {
  background: var(--bg-3);
}
.ds-247420 .chat-link:not(:has(.thumb)):not(:has(img)) { grid-template-columns: 1fr; }
.ds-247420 .chat-link .thumb,
.ds-247420 .chat-link img.thumb { width: 80px; height: 80px; border-radius: var(--r-0); object-fit: cover; background: var(--bg-3) center/cover no-repeat; }
.ds-247420 .chat-link .meta { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
.ds-247420 .chat-link .host { font-family: var(--ff-body); font-size: var(--fs-xs); color: var(--fg-3); text-transform: lowercase; }
.ds-247420 .chat-link .title { font-weight: 600; font-size: var(--fs-sm); line-height: 1.3; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.ds-247420 .chat-link .desc { font-size: var(--fs-xs); color: var(--fg-2); line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }

.ds-247420 .chat-meta {
  display: flex; align-items: center; gap: var(--space-1-75);
  font-family: var(--ff-mono); font-size: var(--fs-xs); color: var(--fg-3);
  padding: 0 var(--space-1-75); letter-spacing: 0.02em;
}
.ds-247420 .chat-meta .who { font-weight: var(--fw-medium, 500); color: var(--fg-2); }
.ds-247420 .chat-meta .who::after { content: ' ·'; color: var(--fg-3); font-weight: 400; }
.ds-247420 .chat-meta .t { color: var(--fg-3); }
.ds-247420 .chat-meta .tick { color: var(--fg-3); margin-left: var(--space-hair); letter-spacing: -2px; font-family: var(--ff-body); }
.ds-247420 .chat-meta .tick.read { color: var(--accent-ink); }

.ds-247420 .chat-reactions {
  display: flex; gap: var(--space-1); flex-wrap: wrap; margin-top: var(--space-hair);
}
.ds-247420 .chat-reactions .rxn {
  display: inline-flex; align-items: center; gap: var(--space-1);
  padding: var(--space-hair) var(--space-2); background: var(--bg-2);
  border: 1px solid color-mix(in oklab, var(--fg) 10%, transparent);
  border-radius: var(--r-pill); font-size: var(--fs-xs); font-family: inherit;
  /* --space-hair padding + --fs-xs line-height measures ~23px, under this
     codebase's 24px icon-control floor — pin the floor explicitly rather
     than growing padding, which would widen the pill past its reference
     proportions. */
  min-height: 24px;
  cursor: pointer; user-select: none; appearance: none;
  transition: background var(--dur-snap, .12s) var(--ease, ease), border-color var(--dur-snap, .12s) var(--ease, ease), transform 0.12s ease;
  position: relative;
}
.ds-247420 .chat-reactions .rxn:hover { background: var(--bg-3); transform: scale(1.06); }
.ds-247420 .chat-reactions .rxn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.ds-247420 .chat-reactions .rxn.you {
  background: color-mix(in oklab, var(--accent) 18%, var(--bg-2));
  border-color: color-mix(in oklab, var(--accent) 55%, transparent);
  color: var(--accent-ink);
}
.ds-247420 .chat-reactions .rxn .e { font-size: var(--fs-xs); line-height: 1; }
.ds-247420 .chat-reactions .rxn .n { font-family: var(--ff-mono); font-weight: var(--fw-medium, 500); font-size: var(--fs-nano); color: var(--fg-2); }
.ds-247420 .chat-reactions .rxn.you .n { color: var(--accent-ink); }

/* Typing indicator renders through the same .chat-bubble container as a real
   message, but must never read as a bubble holding actual content — no
   border, no hover-lift shadow, no tail-corner cut; just a pill-shaped
   borderless dot cluster, visually lighter than any card/bubble on the page. */
/* Double-class selector (.chat-bubble.chat-bubble-typing) deliberately, not
   bare .chat-bubble-typing: chat.css (loaded after app-shell.css on every
   kit that links both) still carries a plain `.chat-bubble { background:
   var(--bg-2) }` rule at the same (0,1,0) specificity, and cascade order
   would let that later rule win over an equal-specificity selector here. */
.ds-247420 .chat-bubble.chat-bubble-typing {
  background: transparent;
  padding: var(--space-2) var(--space-1);
  border-radius: var(--r-pill);
}
.ds-247420 .chat-msg:hover .chat-bubble.chat-bubble-typing { transform: none; box-shadow: none; }
.ds-247420 .chat-msg:not(.you) .chat-bubble.chat-bubble-typing,
.ds-247420 .chat-msg.you .chat-bubble.chat-bubble-typing { border-bottom-left-radius: var(--r-pill); border-bottom-right-radius: var(--r-pill); }

.ds-247420 .chat-typing { display: inline-flex; gap: var(--space-1); align-items: center; }
.ds-247420 .chat-typing span {
  width: 8px; height: 8px; border-radius: 50%; background: var(--fg-3);
}

@media (prefers-reduced-motion: no-preference) {
  .ds-247420 .chat-typing span {
    animation: chat-typing-bounce 1.4s infinite ease-in-out;
  }
  .ds-247420 .chat-typing span:nth-child(1) { animation-delay: 0s; }
  .ds-247420 .chat-typing span:nth-child(2) { animation-delay: 0.18s; }
  .ds-247420 .chat-typing span:nth-child(3) { animation-delay: 0.36s; }
  @keyframes chat-typing-bounce {
    0%, 60%, 100% { transform: translateY(0); opacity: 0.55; }
    30% { transform: translateY(-5px); opacity: 1; }
  }
}

@media (prefers-reduced-motion: no-preference) {
  @keyframes chat-fade-in {
    from { opacity: 0; transform: translateY(8px); }
    to { opacity: 1; transform: translateY(0); }
  }

  .ds-247420 .chat-msg {
    animation: chat-fade-in 0.3s ease-out forwards;
  }
  .ds-247420 .chat-msg:nth-child(1) { animation-delay: 0s; }
  .ds-247420 .chat-msg:nth-child(2) { animation-delay: 0.05s; }
  .ds-247420 .chat-msg:nth-child(3) { animation-delay: 0.1s; }
  .ds-247420 .chat-msg:nth-child(n+4) { animation-delay: 0.15s; }
}

/* Calm composer (claude.ai/code): ONE bordered rounded-rect shell holding the
   context line, a borderless textarea, the hint, and an inline send button -
   not a stadium pill + a floating round send. The shell carries the border and
   the focus ring; the textarea fills it transparently. flex-wrap lets the
   context line and hint occupy their own rows above/below the input row. */
.ds-247420 .chat-composer {
  display: flex; align-items: flex-end; flex-wrap: wrap; gap: var(--space-2);
  padding: var(--space-2) var(--space-2) var(--space-2) var(--space-3);
  background: var(--bg);
  /* The composer sits bg-on-bg: the 14% --rule hairline is ~1.3:1 on paper,
     leaving the primary input near-invisible at rest - use the strong rule. */
  border: var(--bw-hair) solid var(--rule-strong);
  /* --r-3 (14px), not --r-2 (10px): the reference composer's corners read
     as a soft-rounded rectangle, not a tight card-radius box - --r-3 is the
     largest token in the scale short of --r-pill (a stadium, wrong shape
     entirely for a ~720px-wide multi-row box). */
  border-radius: var(--r-3);
  /* A flat hairline box on a flat background is the single most-repeated
     "looks unfinished" tell in the chat surface - the one control the user
     types into every turn had no more visual weight than a settings card.
     --shadow-1 (already defined kit-wide, just unused here) lifts it a hair
     off the page at rest; the existing focus-within box-shadow overrides it. */
  box-shadow: var(--shadow-1);
  flex-shrink: 0;
  transition: border-color var(--dur-snap) var(--ease), box-shadow var(--dur-snap) var(--ease);
}
.ds-247420 .chat-composer:focus-within {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px color-mix(in oklab, var(--accent) 18%, transparent);
}
.ds-247420 .chat-composer.is-error,
.ds-247420 .chat-composer:has(textarea[aria-invalid="true"]) {
  border-color: var(--flame);
}
.ds-247420 .chat-composer.is-error:focus-within,
.ds-247420 .chat-composer:has(textarea[aria-invalid="true"]):focus-within {
  box-shadow: 0 0 0 2px color-mix(in oklab, var(--flame) 22%, transparent);
}
.ds-247420 .chat-composer textarea {
  flex: 1; min-width: 0; padding: var(--space-2) var(--space-1);
  background: none; color: var(--fg);
  border: none; border-radius: 0;
  font-family: inherit; font-size: var(--fs-sm);
  /* 1.5 is the value that actually renders (a later polish block used to
     re-declare it; the sizing block now states the truth). */
  line-height: 1.5; resize: none;
  /* min-height must clear one real rendered line (fs-sm 15px * line-height
     1.5 = 22.5px) PLUS the vertical padding (space-2 top+bottom = 16px) =
     ~39px -- the previous 28px value was under that, so scrollHeight always
     exceeded clientHeight even on an EMPTY textarea, permanently showing a
     scrollbar-track sliver at rest (live-measured: offsetWidth-clientWidth
     was 10px on an empty composer). 40px clears the real content height with
     a hair of margin. */
  min-height: 40px; max-height: 200px;
  box-sizing: border-box;
  /* Always auto, not focus-only: a capped-height composer that loses focus
     while scrolled must still show its scrollbar - otherwise once the field
     blurs there is zero cue that content exists below the fold. */
  overflow-y: auto;
  scrollbar-width: thin;
}
.ds-247420 .chat-composer textarea::placeholder { color: var(--fg-3); }
.ds-247420 .chat-composer textarea:focus { background: none; border: none; box-shadow: none; outline: none; }
.ds-247420 .chat-composer .send {
  border-radius: var(--r-1);
  background: var(--accent); color: var(--accent-fg);
  border: 0; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0;
  transition: background var(--dur-snap, .15s) var(--ease, ease);
}
.ds-247420 .chat-composer .send:disabled { opacity: .5; cursor: not-allowed; }
/* Ghost icon buttons in the toolbar (attach / emoji / more). Size + radius are
   owned by .chat-composer-toolbar .composer-btn in chat.css (32px / --r-1,
   44px coarse); this base rule keeps only the shared appearance. */
.ds-247420 .composer-btn {
  background: transparent; color: var(--fg-3);
  border: 0; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0;
  transition: background var(--dur-snap, .15s) var(--ease, ease), color var(--dur-snap, .15s) var(--ease, ease);
}
.ds-247420 .composer-btn:hover { background: color-mix(in oklab, var(--fg) 8%, transparent); color: var(--fg); }
.ds-247420 .chat-composer .send:hover { filter: brightness(0.92); }
.ds-247420 .chat-composer .send:active { filter: brightness(0.94); }
.ds-247420 .chat-composer .send:focus-visible,
.ds-247420 .composer-btn:focus-visible {
  outline: var(--focus-w) solid var(--focus-color);
  outline-offset: var(--focus-offset);
}
.ds-247420 .chat-composer .send:disabled {
  background: var(--bg-3); color: var(--fg-3); cursor: not-allowed; transform: none;
}
@media (prefers-reduced-motion: reduce) {
  .ds-247420 .chat-composer,
  .ds-247420 .chat-composer textarea,
  .ds-247420 .chat-composer .send,
  .ds-247420 .composer-btn { transition: none; }
}

.ds-247420 .aicat-portrait { display: inline-flex; align-items: center; gap: var(--space-2-5); padding: var(--space-1) 0; }
.ds-247420 .aicat-face {
  /* The face is 3 lines of ascii art ( /\_/\ , ( o.o ) , > ^ < ), not a
     single icon glyph -- the previous 24px/line-height:normal sizing (sized
     like .chat-file .glyph's single-char icon) ran to ~3 line-heights tall
     and clipped the ear/whisker rows against this box's fixed 32px height
     + overflow:hidden. pico (10px) + line-height:1 fits all 3 lines in 30px.
     padding:0 overrides the global `pre { padding: var(--space-5) }` (32px)
     -- box-sizing:border-box + that padding alone exceeded the declared
     32x32 box, so the box never actually enforced 32x32 and rendered at its
     forced min-content size instead, clipping the art regardless of font
     size. */
  width: 32px; height: 32px; font-size: var(--fs-pico); line-height: 1;
  padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  background: color-mix(in oklab, var(--mascot) 22%, var(--bg-2));
  border-radius: 50%; flex-shrink: 0;
  /* Cancels the global `pre { overflow-x: auto }`. This is a fixed 32px
     decorative chip, so the art overflowing its own box made it an
     unreachable keyboard scroll container (WCAG scrollable-region-focusable)
     rather than something a user could ever usefully scroll. */
  overflow: hidden;
}
.ds-247420 .aicat-meta { display: flex; flex-direction: column; font-family: var(--ff-mono); font-size: var(--fs-xs); color: var(--fg-3); }
/* --mascot-deep is now theme-tuned at the token layer (paper #B81F63 /
   ink #FF5C9E), so this needs no per-theme override. The previous
   [data-theme="dark"],[data-theme="ink"] override missed data-theme="auto"
   under OS-dark, which is how the kits actually render — the author name
   fell back to the paper tone at 3.13:1 on ink. */
.ds-247420 .aicat-meta .name { color: var(--mascot-deep, var(--mascot)); font-weight: var(--fw-medium, 500); }

/* Mascot identity folded into AICat's own .chat-head row (passed as its
   `header` prop) instead of a standalone .aicat-portrait block stacked above
   the chat panel -- reuses the existing 32px .aicat-face circle, just placed
   inline before the title instead of above the whole surface. */
.ds-247420 .aicat-portrait-head { gap: var(--space-2-5); }
.ds-247420 .aicat-face-inline { margin-right: var(--space-1); }

/* Centers the transcript+composer as the page's dominant focal surface
   (hero-composer framing): a bounded column with generous top margin so the
   chat panel reads as the first thing the eye lands on, not one card in a
   top-aligned marketing stack. Docs/heading content is demoted into
   .ds-kit-controls below it (see kits-appended.css) so nothing above the
   panel competes with it. */
.ds-247420 .aicat-focus-col {
  max-width: 720px;
  margin-left: auto;
  margin-right: auto;
  padding-top: var(--space-6);
}
/* .ds-app-surface.aicat-focus-col (both classes together, matching every
   real call site in ui_kits/aicat/app.js): must be a flex column, not
   block flow. The AICat() surface reuses .chat (flex:1 1 0%; min-height:0)
   expecting to be sized against leftover space in a flex container, same
   as .ds-chat-layout in the chat kit. As a block-flow parent this column's
   own child .chat had no bounded height to flex against, .chat-thread grew
   to full content height instead of scrolling internally, and
   makeThreadAutoScroll's IntersectionObserver (rooted at .chat-thread)
   never fired because .chat-thread never actually overflowed -- new
   messages landed off-screen with nothing scrolling them into view. The
   suggestions/details siblings below .chat keep their default
   flex:0 1 auto (content-sized), matching .chat-kit-page's trailing-panel
   pattern (kits-appended.css). min-height:0 is load-bearing too: without
   it this column (an .app-main flex item via the topbar.css allow-list)
   sizes to its own content instead of shrinking to .app-main's leftover
   space. The two-class selector is required, not stylistic: .ds-app-surface
   alone sets display:flow-root (kits-appended.css) at equal specificity to
   a bare .aicat-focus-col rule and loads later in the sheet, so a
   single-class override here would lose the cascade. */
.ds-247420 .ds-app-surface.aicat-focus-col {
  display: flex;
  flex-direction: column;
  min-height: 0;
}
/* True empty-state canvas: matches the reference's near-empty page (a
   short lead question directly above a bare composer, nothing else visible
   until the first message lands) rather than centering the SAME bundled
   block that also carries the identity strip / suggestions / docs fold --
   margin:auto on that fuller block left too little spare height to move,
   which is why an earlier attempt at this still read top-heavy. This
   variant renders as its own minimal tree (see aicat/app.js), so centering
   it here actually has room to work: .app-main's flex-column parent gives
   this child top/bottom auto margins, and with only two short children the
   block is small enough to visibly float in the middle of the viewport. */
.ds-247420 .aicat-empty-canvas {
  margin-top: auto;
  margin-bottom: auto;
  display: flex;
  flex-direction: column;
  /* align-items:center previously left the composer at its shrink-to-fit
     intrinsic width instead of filling the 720px focus column -- .chat-composer
     is display:flex with no explicit width, so centering the flex item
     collapsed it to content width, reading as a narrow input hugging the
     left/center of the page instead of spanning the composer's normal width.
     stretch (the default, restated explicitly) fills the column like every
     other composer instance; align-items:center on .aicat-empty-lead below
     centers the text child directly, so the column still doesn't read
     full-bleed for the lede line. */
  align-items: stretch;
  gap: var(--space-4);
}
.ds-247420 .aicat-empty-lead {
  font-family: var(--ff-display);
  font-size: var(--fs-xl);
  font-weight: var(--fw-medium, 500);
  color: var(--fg);
  text-align: center;
  margin: 0;
}
/* Reference's Projects/General sidebar switcher: a two-pill row sitting
   above the tree, spaced off the tree below it like a real section header,
   not flush against it. Tighter side padding than --pad-x (round 4 widened
   that to 48px) -- two pills at natural width need more of the 256px rail
   than a 48px-each-side gutter leaves, so this row alone uses --space-3 to
   stay a single non-wrapping row like the reference, without touching the
   shared --pad-x token every other sidebar section still uses. */
.ds-247420 .aicat-side-tabs {
  padding: var(--space-3) var(--space-3) var(--space-2);
  flex-wrap: nowrap;
}
.ds-247420 .ds-side-empty-note {
  padding: var(--space-2) var(--pad-x);
  color: var(--fg-3);
  font-size: var(--fs-sm);
  margin: 0;
}

/* ============================================================
   Composer attachment-detected badge, context line, hint, toolbar,
   note strip, elapsed counter, dragover state — all referenced from
   composer.js / composer-affordances.js but previously had no CSS at
   all (unstyled DOM nodes / plain text falling through to block
   defaults).
   ============================================================ */
.ds-247420 .chat-composer-context {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-1);
  width: 100%; order: -1;
  padding: var(--space-1) var(--space-1) 0;
  font-family: var(--ff-mono); font-size: var(--fs-tiny); color: var(--fg-3);
  letter-spacing: 0.01em;
}
.ds-247420 button.chat-composer-context {
  background: transparent; border: 0; cursor: pointer; text-align: left;
  border-radius: var(--r-0);
}
.ds-247420 button.chat-composer-context:hover { color: var(--fg-2); }
.ds-247420 button.chat-composer-context:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
.ds-247420 .chat-composer-context-sep { color: var(--fg-3); opacity: 0.6; }
.ds-247420 .chat-composer-context-bit {
  background: transparent; border: 0; padding: 0; margin: 0; cursor: pointer;
  font: inherit; color: var(--fg-3); border-radius: var(--r-0);
  transition: color var(--dur-snap, .15s) var(--ease, ease);
}
.ds-247420 .chat-composer-context-bit:hover { color: var(--accent-ink); text-decoration: underline; text-underline-offset: 2px; }
.ds-247420 .chat-composer-context-bit:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
.ds-247420 .chat-composer-context-text { color: var(--fg-3); }

.ds-247420 .chat-composer-detected-badge {
  display: flex; align-items: center; gap: var(--space-2);
  width: 100%; order: -1;
  padding: var(--space-1) var(--space-2-5);
  margin: 0 0 var(--space-1);
  background: color-mix(in oklab, var(--accent) 12%, var(--bg-2));
  border: 1px solid color-mix(in oklab, var(--accent) 28%, transparent);
  border-radius: var(--r-1);
  font-size: var(--fs-xs); color: var(--fg);
}
.ds-247420 .chat-composer-detected-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ds-247420 .chat-composer-detected-dismiss {
  flex-shrink: 0; width: 20px; height: 20px;
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent; border: 0; cursor: pointer; border-radius: 50%;
  color: var(--fg-3); font-family: var(--ff-mono); font-size: var(--fs-tiny); line-height: 1;
  transition: background var(--dur-snap, .15s) var(--ease, ease), color var(--dur-snap, .15s) var(--ease, ease);
}
.ds-247420 .chat-composer-detected-dismiss:hover { background: color-mix(in oklab, var(--fg) 10%, transparent); color: var(--fg); }
.ds-247420 .chat-composer-detected-dismiss:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }

.ds-247420 .chat-composer-hint {
  width: 100%; order: 2;
  font-family: var(--ff-mono); font-size: var(--fs-tiny); color: var(--fg-3);
  padding: 0 var(--space-1);
}

.ds-247420 .chat-composer-toolbar {
  display: flex; align-items: center; gap: var(--space-1);
  flex-shrink: 0;
}
.ds-247420 .chat-composer-toolbar .composer-btn {
  width: 32px; height: 32px; border-radius: var(--r-1);
}
@media (pointer: coarse) {
  .ds-247420 .chat-composer-toolbar .composer-btn { width: 44px; height: 44px; }
}

.ds-247420 .chat-composer-note {
  width: 100%; order: 3;
  font-family: var(--ff-mono); font-size: var(--fs-tiny); color: var(--fg-3);
  padding: var(--space-1) var(--space-1) 0;
}

.ds-247420 .chat-composer-elapsed {
  font-family: var(--ff-mono); font-size: var(--fs-tiny); color: var(--fg-3);
  flex-shrink: 0; padding: 0 var(--space-1);
}

.ds-247420 .chat-composer.dragover {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px color-mix(in oklab, var(--accent) 18%, transparent);
  background: color-mix(in oklab, var(--accent) 4%, var(--bg));
}

/* ============================================================
   EmojiPicker (ov-emoji-*) — an anchored popover with search input,
   category tabs, and an emoji grid; previously entirely unstyled.
   Search input mirrors .ds-search-input's tonal-fill treatment so it
   reads consistently with the rest of the system's search fields.
   ============================================================ */
.ds-247420 .ov-emoji-root {
  position: fixed;
  display: flex; flex-direction: column; gap: var(--space-2);
  width: 280px; max-height: 320px;
  padding: var(--space-2-5);
  background: var(--bg);
  border: 1px solid color-mix(in oklab, var(--fg) 12%, transparent);
  border-radius: var(--r-2);
  box-shadow: var(--shadow-overlay, var(--shadow-3));
  z-index: var(--z-dropdown, var(--z-raised));
}
.ds-247420 .ov-emoji-search {
  width: 100%; box-sizing: border-box;
  font: inherit; font-size: var(--fs-sm); color: var(--fg);
  background: var(--bg-2); border: 0;
  box-shadow: inset 0 0 0 var(--bw-hair, 1px) var(--rule);
  border-radius: var(--r-1);
  height: var(--ctl-sm, 32px);
  padding: 0 var(--space-2-5);
  transition: box-shadow var(--dur-snap, .15s) var(--ease, ease);
}
.ds-247420 .ov-emoji-search::placeholder { color: var(--fg-3); }
.ds-247420 .ov-emoji-search:focus-visible,
.ds-247420 .ov-emoji-search:focus {
  outline: none;
  box-shadow: inset 0 0 0 var(--bw-hair, 1px) var(--accent), 0 0 0 2px color-mix(in oklab, var(--accent) 18%, transparent);
}
.ds-247420 .ov-emoji-tabs {
  display: flex; gap: var(--space-1);
  border-bottom: 1px solid color-mix(in oklab, var(--fg) 10%, transparent);
  padding-bottom: var(--space-1);
}
.ds-247420 .ov-emoji-tab {
  flex: 1;
  display: inline-flex; align-items: center; justify-content: center;
  height: 30px; font-size: var(--fs-sm);
  background: transparent; border: 0; border-radius: var(--r-1);
  cursor: pointer;
  transition: background var(--dur-snap, .15s) var(--ease, ease);
}
.ds-247420 .ov-emoji-tab:hover { background: color-mix(in oklab, var(--fg) 8%, transparent); }
.ds-247420 .ov-emoji-tab[aria-selected="true"] { background: color-mix(in oklab, var(--accent) 16%, transparent); }
.ds-247420 .ov-emoji-tab:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
.ds-247420 .ov-emoji-grid { overflow-y: auto; flex: 1; min-height: 0; scrollbar-width: thin; scrollbar-color: var(--bg-3) transparent; }
.ds-247420 .ov-emoji-grid-inner {
  display: grid; grid-template-columns: repeat(6, 1fr); gap: var(--space-1);
}
.ds-247420 .ov-emoji-cell {
  display: inline-flex; align-items: center; justify-content: center;
  aspect-ratio: 1; font-size: var(--fs-lg); line-height: 1;
  background: transparent; border: 0; border-radius: var(--r-1);
  cursor: pointer;
  transition: background var(--dur-snap, .15s) var(--ease, ease), transform 0.12s ease;
}
.ds-247420 .ov-emoji-cell:hover { background: color-mix(in oklab, var(--fg) 10%, transparent); transform: scale(1.12); }
.ds-247420 .ov-emoji-cell:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: 1px; background: color-mix(in oklab, var(--fg) 10%, transparent); }
.ds-247420 .ov-emoji-empty {
  grid-column: 1 / -1;
  text-align: center; color: var(--fg-3); font-size: var(--fs-xs);
  padding: var(--space-3) 0;
}
@media (prefers-reduced-motion: reduce) {
  .ds-247420 .ov-emoji-cell { transition: background var(--dur-snap, .15s) var(--ease, ease); }
  .ds-247420 .ov-emoji-cell:hover { transform: none; }
}

/* ============================================================
   Sidebar polish: hide zero-count badges; tonal active state;
   monospace count chips.
   ============================================================ */
/* Rail row rhythm: the sidebar rows (bins/labels) previously packed at
   --space-hair (2px) margin-bottom while the main content column reads at
   the --space-5/--space-3 outer/inner rhythm (AGENTS.md "panel rhythm").
   Bumping the inter-row gap to --space-1-5 (5px) — still a tight, dense-app
   value, but enough that adjacent rows no longer read as glued together —
   and making it density-aware via [data-density] mirrors the existing
   compact-density override pattern in files.css (.ds-file-grid[data-density]).
   [data-density="compact"] keeps the old tighter feel for long rail lists;
   [data-density="spacious"] opens it further. */
.ds-247420 .app-side a { margin-bottom: var(--space-1-5); }
.ds-247420 .app-side[data-density="compact"] a,
.ds-247420 .app-side-shell[data-density="compact"] .app-side a { margin-bottom: var(--space-hair); }
.ds-247420 .app-side[data-density="spacious"] a,
.ds-247420 .app-side-shell[data-density="spacious"] .app-side a { margin-bottom: var(--space-2); }

.ds-247420 .app-side .count {
  padding: 1px 8px; font-size: var(--fs-micro); font-weight: 700;
  font-family: var(--ff-body); letter-spacing: 0.04em;
  min-width: 18px; text-align: center;
}
.ds-247420 .app-side a:has(> .count:empty) > .count,
.ds-247420 .app-side .count:empty { display: none; }
.ds-247420 .app-side a .glyph { font-family: var(--ff-body); font-size: 13px; font-weight: 600; color: var(--fg-3); text-align: center; }
.ds-247420 .app-side a.active .glyph { color: var(--accent-ink); }

/* .ds-side-tree — two-level project-tree group (parent <details> + nested
   child rows), the sidebar's hierarchy primitive. Sits alongside Side()'s
   own flat .app-side-group rows but carries genuine nesting: a summary row
   (parent) disclosing child conversation rows, closer to a project-tree
   sidebar than a single flat list. */
.ds-247420 .ds-side-tree { border: 0; }
.ds-247420 .ds-side-tree-summary {
  display: flex; align-items: center; gap: var(--space-2);
  padding: var(--space-1-5) var(--space-2);
  border-radius: var(--r-1);
  font-family: var(--ff-mono); font-size: var(--fs-sm); color: var(--fg-2);
  cursor: pointer; list-style: none;
}
.ds-247420 .ds-side-tree-summary::-webkit-details-marker { display: none; }
.ds-247420 .ds-side-tree-summary:hover { background: var(--bg-2); color: var(--fg); }
.ds-247420 .ds-side-tree-summary:focus-visible {
  outline: var(--focus-w) solid var(--focus-color);
  outline-offset: var(--focus-offset);
}
.ds-247420 .ds-side-tree-summary .glyph { font-family: var(--ff-body); font-size: 13px; font-weight: 600; color: var(--fg-3); }
.ds-247420 .ds-side-tree-summary .count {
  margin-left: auto; padding: 1px 8px; font-size: var(--fs-micro); font-weight: 700;
  font-family: var(--ff-body); letter-spacing: 0.04em; min-width: 18px; text-align: center;
  color: var(--fg-3); background: var(--bg-2); border-radius: var(--r-pill, 999px);
}
.ds-247420 .ds-side-tree-children {
  display: flex; flex-direction: column;
  padding-left: var(--space-4);
  gap: var(--space-hair);
}
.ds-247420 .ds-side-tree-child {
  display: flex; align-items: center; gap: var(--space-2);
  padding: var(--space-1) var(--space-2);
  border-radius: var(--r-1);
  font-family: var(--ff-mono); font-size: var(--fs-xs); color: var(--fg-3);
  text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ds-247420 .ds-side-tree-child:hover, .ds-247420 .ds-side-tree-child:focus-visible { background: var(--bg-2); color: var(--fg); }
.ds-247420 .ds-side-tree-child:focus-visible {
  outline: var(--focus-w) solid var(--focus-color);
  outline-offset: var(--focus-offset);
}
.ds-247420 .ds-side-tree-child .glyph { font-family: var(--ff-body); color: var(--fg-3); }

/* -- select primitive --------------------------------------------------- */
.ds-247420 .ds-select {
    width: 100%;
    min-width: 0;
    min-height: 32px; /* shared control floor with .btn/.btn-primary/.btn-ghost */
    padding: 10px 30px 10px 14px; /* right gap clears the chevron */
    background: var(--bg-2);
    border: 0;
    border-radius: var(--r-2);
    font-family: inherit;
    font-size: var(--fs-sm);
    color: var(--fg);
    /* Clip long option text gracefully - a select that cuts 'Claude Sonnet
       (latest' mid-parenthesis reads as broken; ellipsis reads as truncated. */
    text-overflow: ellipsis;
    white-space: nowrap;
    overflow: hidden;
    appearance: none;
    background-image: linear-gradient(45deg, transparent 50%, var(--fg-3) 50%),
        linear-gradient(135deg, var(--fg-3) 50%, transparent 50%);
    background-position: calc(100% - 18px) 50%, calc(100% - 13px) 50%;
    background-size: 5px 5px, 5px 5px;
    background-repeat: no-repeat;
    cursor: pointer;
}
.ds-247420 .ds-select:focus-visible { box-shadow: var(--focus-ring-inset); }
.ds-247420 .ds-select:hover { background-color: var(--bg-3); }

/* -- chat composer autogrow polish (sizing lives in the primary block) -- */
.ds-247420 .chat-composer textarea {
    transition: height var(--dur-snap) var(--ease);
}
.ds-247420 .chat-composer textarea:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

/* -- mobile responsive: chat -------------------------------------------- */
@media (max-width: 768px) {
    .ds-247420 .chat-stack { max-width: 100%; }
    .ds-247420 .chat-msg { padding: 4px 0; }
    .ds-247420 .chat-msg:hover { padding: 4px 0; margin: 0; border-radius: 0; background: transparent; }
    .ds-247420 .chat-bubble { font-size: var(--fs-sm); padding: 10px 14px; }
    .ds-247420 .chat-avatar { width: 28px; height: 28px; font-size: 12px; }
    .ds-247420 .chat-composer { padding: 8px; }
    .ds-247420 .chat-composer textarea { padding: 10px 12px; }
}

/* --- landscape orientation: reduce vertical space for chrome + composer --- */
@media (max-height: 500px) and (orientation: landscape) {
    /* Keep horizontal padding: 0 put the textarea/focus ring flush on the edge. */
    .ds-247420 .chat-composer { padding: 6px var(--space-2); }
    .ds-247420 .chat-composer textarea { min-height: 36px; max-height: 96px; }
    .ds-247420 .chat-composer .chat-composer-hint,
    .ds-247420 .chat-composer .chat-composer-context { display: none; }
    /* Compact the shell chrome: ~114px of topbar+crumb+status is a third of a
       360px-tall landscape fold. */
    .ds-247420 { --app-topbar-h: 40px; --app-crumb-h: 24px; }
    .ds-247420 .app-chrome { padding: 4px var(--pad-x); }
    .ds-247420 .app-status { display: none; }
    .ds-247420 .ws-crumb { min-height: 32px; }
    .ds-247420 .ds-hero { padding: var(--space-4) 0 var(--space-3); }
    .ds-247420 .ds-hero-title { font-size: clamp(28px, 6cqi, 56px); }
}

/* ============================================================
   Enhanced UX: Component States, Drag-and-Drop, Context Menus
   Comprehensive improvements for perfect UX across all surfaces
   ============================================================ */

/* ------------------------------------------------------------
   Table — clickable row focus ring
   Table()'s onRowClick path (content.js) sets class="clickable"
   role="button" tabindex="0" on the <tr>; the browser's native
   :focus-visible outline on a <tr> renders inconsistently across
   engines (often clipped by table border-collapse), so it needs
   the same explicit outline treatment every other interactive
   primitive in this file gets, via the shared --focus-* tokens.
   -------------------------------------------------------------- */
.ds-247420 tr.clickable:focus-visible {
  outline: var(--focus-w) solid var(--focus-color);
  outline-offset: calc(-1 * var(--focus-offset));
}

/* ------------------------------------------------------------
   Component States — Disabled, Loading, Error, Success
   -------------------------------------------------------------- */

/* Disabled state for all interactive elements. The `.is-disabled` class
   variants cover Btn()'s anchor-rendered case (shell.js) — an <a> has no
   :disabled pseudo-class, so a disabled link-styled button relies entirely
   on this class selector for its visual treatment (aria-disabled + tabindex
   are set in JS; this is the paired CSS half). */
/* opacity:0.6 alone barely separated from an active .btn-primary against a
   busy background -- close enough in dark mode that users clicked disabled
   buttons expecting them to work. grayscale flattens any color signal (no
   more "still looks clickable-green") and the steeper opacity drop reads as
   unmistakably inert at a glance, not just slightly dimmed. */
.ds-247420 .btn:disabled, .ds-247420 .btn-primary:disabled, .ds-247420 .btn-ghost:disabled, .ds-247420 .btn.is-disabled, .ds-247420 .btn-primary.is-disabled, .ds-247420 .btn-ghost.is-disabled, .ds-247420 button:disabled, .ds-247420[disabled] {
  opacity: 0.5;
  filter: grayscale(60%);
  cursor: not-allowed;
  pointer-events: none;
}

/* Systemic hover fallback — button-like elements with no design-system class
   (a bare <button>, [role="button"], or a generic .row/.chip usage) get a
   `transition` declared above (line ~106) but nothing to transition to, so
   they look inert until clicked. This is a low-specificity (0,0,1)/(0,1,1)
   catch-all: any element that also carries `.btn`/`.btn-primary`/`.btn-ghost`/
   `.ds-icon-btn`/etc. keeps its own component `:hover` (those selectors are
   equal-or-higher specificity and, for the shared ones, declared later in
   the cascade), so this never overrides a deliberate per-component hover —
   it only fills the gap for elements that don't have one. */
.ds-247420 button:not(:disabled):hover,
.ds-247420[role="button"]:not([aria-disabled="true"]):hover {
  background-color: var(--panel-hover, var(--bg-3));
}

/* Loading state — spinning indicator */
@keyframes spinner-rotate {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}
/* Reusable spin utility: rotates the element (e.g. a refresh Icon used as a
   busy indicator) via the shared spinner-rotate keyframe. Respects reduced
   motion. Use instead of a decorative rotation glyph.
   Ring spinner — use for larger blocking/full-region loading states. */
.ds-247420 .ds-spin { animation: spinner-rotate 0.8s linear infinite; transform-origin: center; }
@media (prefers-reduced-motion: reduce) { .ds-247420 .ds-spin { animation: none; } }

/* Ring spinner — use for larger blocking/full-region loading states. */
.ds-247420 .loading { position: relative; }
.ds-247420 .loading::after {
  content: '';
  position: absolute; top: 50%; right: 12px;
  width: 16px; height: 16px;
  border: 2px solid var(--bg-3);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spinner-rotate 0.8s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .ds-247420 .loading::after { animation: none; }
}

/* Error state.
   No !important: the only rule in the kit that also resets a matching element's
   border-color is the bare-control base below (`textarea:where(:not(.input))`
   et al, which use the `border: 0` shorthand). Those selectors are (0,0,1) —
   the :where() guard adds nothing — so every selector in this list out-specifies
   them on merit, and specificity beats the later source position. */
.ds-247420 .error, .ds-247420[data-state="error"], .ds-247420 .row.error, .ds-247420 input.error, .ds-247420 textarea.error {
  border-color: var(--warn);
  background-color: color-mix(in oklab, var(--warn) 6%, var(--bg));
}
.ds-247420 .error::placeholder { color: color-mix(in oklab, var(--warn) 60%, var(--fg-3)); }

/* Success state */
.ds-247420 .success, .ds-247420[data-state="success"], .ds-247420 .row.success { background-color: color-mix(in oklab, var(--green-2) 6%, var(--bg)); }

/* Readonly state */
.ds-247420[readonly], .ds-247420 .readonly {
  background: var(--bg-2);
  cursor: default;
  opacity: 0.8;
}

/* ------------------------------------------------------------
   Enhanced Button Micro-Interactions
   -------------------------------------------------------------- */

/* Button scale feedback on active */
.ds-247420 .btn:active:not(:disabled),
.ds-247420 .btn-primary:active:not(:disabled),
.ds-247420 .btn-ghost:active:not(:disabled),
.ds-247420 button:active:not(:disabled) {
  transform: scale(0.98);
}

/* Enhanced hover transitions — transition only the properties that actually
   change (not `all`, which animates layout and is a perf trap), and drop the
   blanket `will-change` (it forces a permanent compositor layer per element). */
.ds-247420 .btn, .ds-247420 .btn-primary, .ds-247420 .btn-ghost, .ds-247420 button, .ds-247420 a.row, .ds-247420 .row, .ds-247420 .chip, .ds-247420[role="button"], .ds-247420 input[type="checkbox"], .ds-247420 input[type="radio"] {
  transition: background-color var(--dur-snap) var(--ease),
              color var(--dur-snap) var(--ease),
              box-shadow var(--dur-snap) var(--ease),
              transform var(--dur-snap) var(--ease);
}

/* Native checkbox/radio tint — browser default is UA blue, which fights the
   single lime lead accent wherever a raw control lands outside a bespoke
   toggle (e.g. signin's "remember me"). accent-color re-tints the native
   control box without hand-drawing a replacement. */
.ds-247420 input[type="checkbox"], .ds-247420 input[type="radio"] {
  accent-color: var(--accent-ink);
}

/* Prevent double-tap zoom on buttons (iOS) */
.ds-247420 .btn, .ds-247420 .btn-primary, .ds-247420 .btn-ghost, .ds-247420 button {
  -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
  -webkit-touch-callout: none;
}

/* ------------------------------------------------------------
   Drag-and-Drop Styles
   -------------------------------------------------------------- */

/* Draggable element */
.ds-247420[draggable="true"], .ds-247420 .draggable {
  cursor: grab;
  user-select: none;
}
.ds-247420[draggable="true"]:active { cursor: grabbing; }

/* Drag-over state */
.ds-247420 .drag-over {
  border: 2px dashed var(--accent);
}

/* Drag-ghost (semi-transparent dragging element) */
.ds-247420 .drag-ghost {
  opacity: 0.5;
  transform: rotate(2deg) scale(0.95);
}

/* Drop-zone indicator */
.ds-247420 .drop-zone {
  border: 2px dashed var(--rule-strong);
  border-radius: var(--r-2);
  padding: var(--space-4);
  background: color-mix(in oklab, var(--accent) 4%, var(--bg));
  min-height: 120px;
  display: flex; align-items: center; justify-content: center;
  text-align: center; color: var(--fg-3);
  /* Explicit list, never `all` — the only things `.drop-zone.active` changes
     are the dashed border tone, the tinted fill and the label colour. `all`
     also animated padding/min-height/border-width, so a drop target visibly
     resized mid-drag instead of snapping. */
  transition: background var(--dur-snap) var(--ease), border-color var(--dur-snap) var(--ease),
    color var(--dur-snap) var(--ease);
}
.ds-247420 .drop-zone.active {
  border-color: var(--accent);
  background: color-mix(in oklab, var(--accent) 10%, var(--bg));
  color: var(--accent-ink);
}

/* List item reorder indicator */
.ds-247420 .list-item.dragging { opacity: 0.5; }
.ds-247420 .list-item.drag-before::before,
.ds-247420 .list-item.drag-after::after {
  content: '';
  position: absolute;
  left: 0; right: 0; height: 2px;
  background: var(--accent);
}
.ds-247420 .list-item.drag-before::before { top: -2px; }
.ds-247420 .list-item.drag-after::after { bottom: -2px; }

@keyframes pulse-line {
  0%, 100% { opacity: 0.3; }
  50% { opacity: 1; }
}
@media (prefers-reduced-motion: no-preference) {
  .ds-247420 .list-item.drag-before::before,
  .ds-247420 .list-item.drag-after::after { animation: pulse-line 0.6s ease-in-out infinite; }
}

/* ------------------------------------------------------------
   Focus-visible for controls with no other component sheet
   -------------------------------------------------------------- */

.ds-247420 .ds-session-new:focus-visible,
.ds-247420 .agentchat-cwd-btn:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring-inset);
}

/* ------------------------------------------------------------
   Context Menu Styles
   -------------------------------------------------------------- */

.ds-247420 .ds-context-menu {
  position: absolute;
  background: var(--bg-2);
  border: 1px solid var(--rule);
  border-radius: var(--r-2);
  box-shadow: var(--shadow-overlay);
  min-width: 160px;
  z-index: var(--z-dropdown);
  padding: 4px 0;
}

@keyframes context-menu-in {
  from {
    opacity: 0;
    transform: scale(0.95) translateY(-4px);
  }
  to {
    opacity: 1;
    transform: scale(1) translateY(0);
  }
}
@media (prefers-reduced-motion: no-preference) {
  .ds-247420 .ds-context-menu { animation: context-menu-in 0.15s ease-out; }
}

.ds-247420 .ds-context-menu-item {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 14px;
  cursor: pointer;
  color: var(--fg);
  font-size: var(--fs-sm);
  transition: background var(--dur-snap) var(--ease);
}
.ds-247420 .ds-context-menu-item:hover {
  background: var(--bg-3);
  color: var(--accent-ink);
}
.ds-247420 .ds-context-menu-item:active {
  background: color-mix(in oklab, var(--accent) 20%, var(--bg-3));
}
.ds-247420 .ds-context-menu-item.danger {
  color: var(--warn);
  box-shadow: inset 2px 0 0 var(--warn);
}
.ds-247420 .ds-context-menu-item.danger:hover {
  background: color-mix(in oklab, var(--warn) 12%, var(--bg));
}
.ds-247420 .ds-context-menu-divider {
  height: 1px;
  background: var(--rule);
  margin: 4px 0;
}
.ds-247420 .ds-context-menu-icon {
  width: 16px; height: 16px;
  flex-shrink: 0;
  opacity: 0.7;
}

/* Context menu trigger indicator */
.ds-247420[data-has-context-menu] {
  cursor: context-menu;
}

/* ------------------------------------------------------------
   Enhanced Form Inputs
   -------------------------------------------------------------- */

/* Base chrome for any BARE native control (outside .ds-field/.row-form/.input):
   the same editorial tonal-fill + printed-baseline-rule as .ds-field, so a raw
   <input> is never a browser-default white box in dark mode. The guards are
   wrapped in :where() so they add ZERO specificity — the whole selector stays
   at (0,1,1), exactly like `.ds-field input`, so the later .ds-field / .ds-field--sm
   / .row-form descendant rules win by source order instead of being out-specified
   (a bare :not(.input) counts as a class and would beat every .ds-field rule,
   which clobbered the size variants). */
.ds-247420 input[type="text"]:where(:not(.input):not(.ds-search-input)),
.ds-247420 input[type="email"]:where(:not(.input):not(.ds-search-input)),
.ds-247420 input[type="password"]:where(:not(.input):not(.ds-search-input)),
.ds-247420 input[type="number"]:where(:not(.input):not(.ds-search-input)),
.ds-247420 input[type="search"]:where(:not(.input):not(.ds-search-input)),
.ds-247420 input[type="url"]:where(:not(.input):not(.ds-search-input)),
.ds-247420 input[type="tel"]:where(:not(.input):not(.ds-search-input)),
.ds-247420 textarea:where(:not(.input)),
.ds-247420 select:where(:not(.ds-select)) {
  font: inherit; color: var(--fg);
  background: var(--bg-2);
  border: 0;
  box-shadow: inset 0 0 0 var(--bw-hair, 1px) var(--rule),
              inset 0 calc(-1 * var(--bw-rule)) 0 var(--rule-strong);
  border-radius: var(--r-1);
  padding: 0 var(--space-3);
  transition: background var(--dur-snap) var(--ease),
              border-color var(--dur-snap) var(--ease),
              box-shadow var(--dur-snap) var(--ease);
}
/* Single-line bare controls snap to the field height; textarea keeps auto
   height with symmetric padding (its content decides the box). */
.ds-247420 input[type="text"]:where(:not(.input):not(.ds-search-input)),
.ds-247420 input[type="email"]:where(:not(.input):not(.ds-search-input)),
.ds-247420 input[type="password"]:where(:not(.input):not(.ds-search-input)),
.ds-247420 input[type="number"]:where(:not(.input):not(.ds-search-input)),
.ds-247420 input[type="search"]:where(:not(.input):not(.ds-search-input)),
.ds-247420 input[type="url"]:where(:not(.input):not(.ds-search-input)),
.ds-247420 input[type="tel"]:where(:not(.input):not(.ds-search-input)),
.ds-247420 select:where(:not(.ds-select)) {
  height: var(--field-height);
}
.ds-247420 textarea:where(:not(.input)) { padding: var(--space-2) var(--space-3); min-height: calc(4 * 1.5em); }
.ds-247420 input[type="text"]:where(:not(.input):not(.ds-search-input)):focus-visible,
.ds-247420 input[type="email"]:where(:not(.input):not(.ds-search-input)):focus-visible,
.ds-247420 input[type="password"]:where(:not(.input):not(.ds-search-input)):focus-visible,
.ds-247420 input[type="number"]:where(:not(.input):not(.ds-search-input)):focus-visible,
.ds-247420 input[type="search"]:where(:not(.input):not(.ds-search-input)):focus-visible,
.ds-247420 input[type="url"]:where(:not(.input):not(.ds-search-input)):focus-visible,
.ds-247420 input[type="tel"]:where(:not(.input):not(.ds-search-input)):focus-visible,
.ds-247420 textarea:where(:not(.input)):focus-visible,
.ds-247420 select:where(:not(.ds-select)):focus-visible {
  outline: none;
  background: var(--bg);
  box-shadow: inset 0 0 0 var(--bw-hair, 1px) var(--rule),
              inset 0 calc(-1 * var(--bw-chunk)) 0 var(--accent);
}

.ds-247420 input[type="text"]::placeholder,
.ds-247420 input[type="email"]::placeholder,
.ds-247420 input[type="password"]::placeholder,
.ds-247420 input[type="number"]::placeholder,
.ds-247420 input[type="search"]::placeholder,
.ds-247420 input[type="url"]::placeholder,
.ds-247420 textarea::placeholder {
  color: var(--fg-3);
}

/* Long-form reading voice — quiet sans, generous line-height, capped to a
   readable measure so a wide viewport never stretches body copy edge to
   edge. Links use --accent-ink (never the bare fill, which is unreadable
   as text). */
.ds-247420 .prose {
  max-width: var(--measure);
  font-family: var(--ff-body); font-size: var(--fs-body); line-height: 1.6;
  color: var(--fg);
}
.ds-247420 .prose p { margin: 0 0 var(--space-4) 0; }
.ds-247420 .prose p:last-child { margin-bottom: 0; }
.ds-247420 .prose a { color: var(--accent-ink); text-decoration-color: var(--rule-strong); text-underline-offset: 0.15em; }
.ds-247420 .prose a:visited { color: color-mix(in oklab, var(--accent-ink) 70%, var(--purple-2, var(--fg-3))); }
.ds-247420 .prose a:hover { text-decoration-color: currentColor; }

/* Rubber-stamp motif — a bordered, uppercase, letter-spaced badge with a
   slight built-in rotation (variants may override via inline transform for
   a stronger tilt). Max 1 per page per the component's own caption. */
.ds-247420 .stamp {
  display: inline-block;
  font-family: var(--ff-mono); font-size: var(--fs-xs); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.08em;
  padding: var(--space-1-5, var(--space-2)) var(--space-3);
  border: 2px solid var(--fg);
  border-radius: var(--r-hair);
  color: var(--fg);
  transform: rotate(-2deg);
  white-space: nowrap;
}
/* border-color previously matched background exactly (var(--ink) on both) --
   same class of bug as the colors-core "paper" swatch: a border drawn in the
   identical color as its own fill is invisible, so the ink-toned stamp was
   the one variant on the page missing its box outline. --paper reads as a
   visible ring against the ink fill (paper-on-ink is this token pair's
   whole reason to exist), matching every other .stamp variant's contrast. */
.ds-247420 .stamp.ink { background: var(--ink); border-color: var(--paper); color: var(--paper); }
.ds-247420 .stamp.acid { background: var(--accent); border-color: var(--accent-ink); color: var(--accent-ink); }
.ds-247420 .stamp.purple { border-color: var(--purple-2); color: var(--purple-2); }
.ds-247420 .stamp.mascot { border-color: var(--mascot); color: var(--mascot-deep); }

/* Same rubber-stamp face, but a real interactive <button> — resets UA button
   chrome so it isn't blue link text in a browser default box. */
.ds-247420 .btn-stamp {
  display: inline-block; cursor: pointer;
  font-family: var(--ff-mono); font-size: var(--fs-xs); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.08em;
  padding: var(--space-1-5, var(--space-2)) var(--space-3);
  border: 2px solid var(--fg);
  border-radius: var(--r-hair);
  background: transparent; color: var(--fg);
  /* .stamp's own caption claims the rubber-stamp motif is "always rotated
     slightly" -- .btn-stamp is the same visual face as an interactive
     <button> (preview/stamps-lore.html's second row) and had no rotation at
     all, breaking that claim for every stamp in that row. */
  transform: rotate(-2deg);
  transition: background var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease);
}
.ds-247420 .btn-stamp:hover { background: var(--panel-hover); }
.ds-247420 .btn-stamp.acid { border-color: var(--accent-ink); color: var(--accent-ink); }
.ds-247420 .btn-stamp.acid:hover { background: var(--accent-tint); }
.ds-247420 .btn-stamp.purple { border-color: var(--purple-2); color: var(--purple-2); }
.ds-247420 .btn-stamp.mascot { border-color: var(--mascot); color: var(--mascot-deep); }

/* Structural hairlines — "no boxes, no cards, structure comes from rules."
   Three weights: .rule (default separator), .rule-double (major section
   break), .rule-dotted (optional/skippable). Applied to a bare <hr>, which
   otherwise falls back to the browser's default inset groove line. */
.ds-247420 hr.rule, .ds-247420 hr.rule-double, .ds-247420 hr.rule-dotted {
  width: 100%; height: 0; border: 0;
}
.ds-247420 hr.rule { border-top: 1px solid var(--rule); }
.ds-247420 hr.rule-double {
  height: 3px;
  border-top: 1px solid var(--rule-strong);
  border-bottom: 1px solid var(--rule-strong);
}
/* 1px was indistinguishable from .rule -- border-style:dotted needs enough
   width to actually draw a gap between dots; below ~2px (esp. at DPRs where
   1px resolves to a sub-pixel value like 0.8px) Chrome collapses it to a
   solid line, which is exactly what preview/rules.html's report caught live. */
.ds-247420 hr.rule-dotted { border-top: 2px dotted var(--rule-strong); }

/* Hairline-only text input — no full box, no permanent fill. A single bottom
   rule that thickens to 2px on focus, matching the row/rule zero-border
   aesthetic instead of a browser-default boxed field. A completely flat,
   static hairline reads as a static label rather than an editable control
   at a glance (no cue at rest, nothing on hover) -- the very faint permanent
   wash plus a stronger hover tint below signal "this is a field" without
   reverting to the boxed/filled chrome the hairline design deliberately
   moved away from. */
.ds-247420 .input {
  display: block; width: 100%; box-sizing: border-box;
  /* appearance:none alone is not enough: with color-scheme:dark set on the
     page (colors_and_type.css), Chrome still paints its native dark
     form-control ring (an inset box-shadow) on <input> regardless of
     appearance -- confirmed live via getComputedStyle showing a real
     border-radius/box-shadow despite appearance computing to "none". Pin
     color-scheme:normal on the input itself to opt it out of that native
     dark styling, and zero box-shadow explicitly so nothing survives. */
  appearance: none; -webkit-appearance: none;
  color-scheme: normal;
  border: 0; border-bottom: 1px solid var(--rule-strong);
  border-radius: var(--r-0);
  background: color-mix(in oklab, var(--fg) 2.5%, transparent);
  box-shadow: none;
  padding: var(--space-2) var(--space-1);
  font: inherit; color: var(--fg);
  transition: background var(--dur-snap) var(--ease), border-color var(--dur-snap) var(--ease);
}
.ds-247420 .input:hover { background: color-mix(in oklab, var(--fg) 5%, transparent); }
/* Touch floor for the BASE input. Several specific inputs already carried this
   (ds-file-filter-input, ds-modal-input, ds-term-input) but the primitive every
   form actually uses did not, so a plain field measured 34px under a coarse
   pointer -- measured live on the settings and gm_inspector kits. The 16px
   font-size is not cosmetic: below 16px iOS auto-zooms the viewport on focus,
   and max() keeps it from ever shrinking under a reduced typescale. */
@media (pointer: coarse) {
  .ds-247420 .input { min-height: 44px; font-size: max(16px, var(--fs-body)); }
  .ds-247420 textarea.input { min-height: 88px; }
}
.ds-247420 .input::placeholder { color: var(--fg-3); }
.ds-247420 .input:focus-visible {
  outline: none;
  border-bottom-width: 2px;
  border-bottom-color: var(--accent-ink);
}

/* Switch primitive (form-primitives.js Toggle) -- track + sliding knob.
   Canonical definition lives in editor-primitives.css alongside the rest of
   the .ds-check/.ds-radio/.ds-toggle form-primitive family (that file loads
   AFTER this one in the build.mjs concat order, so a second .ds-toggle block
   here was dead weight that silently lost the cascade while still disagreeing
   on size/colors/knob-offset with the surviving rule — a duplicate-source-of-
   -truth foot-gun, not an intentional override). Removed; see
   editor-primitives.css's ".ds-toggle" block for track/knob/disabled/focus. */

/* -- Field controls: themed base for TextField / Select / SearchInput --
   Root fix: previously only `transition` was set, so themed apps got
   browser-default white boxes in dark mode and labels collided with inputs
   because `.ds-field` had no layout. */
.ds-247420 .ds-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-1, 6px);
  align-items: stretch;
  width: 100%;
}
.ds-247420 .ds-field-label {
  font-size: var(--fs-sm, 14px);
  font-weight: 500;
  color: var(--fg-2);
  line-height: 1.3;
}
.ds-247420 .ds-field-required {
  color: var(--danger);
  font-weight: 700;
  margin-left: 2px;
}
.ds-247420 .ds-field-hint {
  font-size: var(--fs-tiny, 13px);
  color: var(--fg-3);
  line-height: 1.35;
}
.ds-247420 .ds-field-error {
  color: var(--danger);
  font-size: var(--fs-tiny, 13px);
  font-weight: 500;
}
.ds-247420 .ds-field-count {
  font-size: var(--fs-tiny, 13px);
  color: var(--fg-3);
  align-self: flex-end;
}

/* :not(.input) guards, matching the bare-native-control pattern above: without
   them this descendant selector's (0,1,1) specificity out-ranks the hairline
   .input rule's (0,1,0), so any <input class="input"> inside .ds-field (every
   settings-kit field) got this boxed tonal-fill chrome instead -- confirmed
   live via getComputedStyle showing border-radius:8px + inset box-shadow on
   settings kit inputs despite .input declaring border-radius:var(--r-0) and
   box-shadow:none. */
.ds-247420 .ds-field input:not(.input),
.ds-247420 .ds-field textarea:not(.input),
.ds-247420 .ds-field .ds-select,
.ds-247420 .ds-search-input {
  width: 100%;
  box-sizing: border-box;
  font: inherit;
  color: var(--fg);
  background: var(--bg-2);
  border: 0;
  /* Editorial field — tonal fill with a printed baseline rule that thickens to
     the electric lead on focus, instead of a uniform hairline box. */
  box-shadow: inset 0 0 0 var(--bw-hair, 1px) var(--rule),
              inset 0 calc(-1 * var(--bw-rule)) 0 var(--rule-strong);
  /* Tighter corner than the old --r-2 (14px): a dense pro control, not a soft
     consumer box. Single-line controls snap to --field-height with h-only
     padding so they align pixel-exact with buttons on the same --ctl-md rung. */
  border-radius: var(--r-1);
  height: var(--field-height);
  padding: 0 var(--space-3);
  transition: box-shadow var(--dur-snap) var(--ease), background var(--dur-snap) var(--ease);
}
/* textarea is multi-line: height is auto (min-height instead), keep symmetric
   vertical padding so text doesn't sit flush to the top edge. */
.ds-247420 .ds-field textarea:not(.input) { height: auto; min-height: calc(4 * 1.5em); padding: var(--space-2) var(--space-3); resize: vertical; }

/* Field size ladder (size prop on TextField/Select/SearchInput). md == base.
   sm/lg retune height + h-padding + font off the --ctl-* ladder, matching the
   button ladder so a form of mixed controls stays on one rhythm. */
.ds-247420 .ds-field--sm input:not(.input), .ds-247420 .ds-field--sm .ds-select, .ds-247420 .ds-field--sm .ds-search-input {
  height: var(--ctl-sm); padding: 0 var(--space-2-5); font-size: var(--fs-tiny);
}
.ds-247420 .ds-field--sm textarea:not(.input) { height: auto; padding: var(--space-1-75) var(--space-2-5); font-size: var(--fs-tiny); }
.ds-247420 .ds-field--lg input:not(.input), .ds-247420 .ds-field--lg .ds-select, .ds-247420 .ds-field--lg .ds-search-input {
  height: var(--ctl-lg); padding: 0 var(--space-4); font-size: var(--fs-body);
}
.ds-247420 .ds-field--lg textarea:not(.input) { height: auto; padding: var(--space-2-5) var(--space-4); font-size: var(--fs-body); }
.ds-247420 .ds-search-input::placeholder { color: var(--fg-3); }
/* Only present when SearchInput is passed resultCount or has a value (clear
   button) - transparent to any flex/grid layout the bare input previously
   sat in directly. */
.ds-247420 .ds-search-input-wrap { display: contents; }
/* Leading-icon slot, matching .app-search's icon placement in Topbar so the
   two search surfaces read as one control family instead of icon-vs-no-icon. */
.ds-247420 .ds-search-field { position: relative; display: inline-flex; align-items: center; min-width: 0; }
.ds-247420 .ds-search-field .ds-search-icon {
  position: absolute; left: var(--space-3);
  display: inline-flex; pointer-events: none;
  color: var(--fg-3); opacity: 0.6;
}
.ds-247420 .ds-search-field .ds-search-input { padding-left: calc(var(--space-3) + 15px + var(--space-2)); }

/* Visible clear (X) button for SearchInput - mirrors .ds-alert-dismiss's
   ghost-icon treatment but sized to the kit's 44px touch-target floor
   (app-shell.css already establishes this convention for .ds-file-check /
   .ds-density-btn / .ds-upload-act etc). Positioned by the consumer's own
   flex/grid context (ds-search-input-wrap is display:contents), so it simply
   sits after the input in DOM order. */
.ds-247420 .ds-search-clear {
  display: inline-flex;
  align-items: center; justify-content: center;
  min-width: 44px; min-height: 44px;
  flex-shrink: 0;
  cursor: pointer;
  color: var(--fg-3);
  background: transparent;
  border: 0;
  padding: 0;
  border-radius: var(--r-1);
  transition: background var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease);
}
.ds-247420 .ds-search-clear:hover { background: var(--bg-2); color: var(--fg); }
.ds-247420 .ds-search-clear:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }

.ds-247420 .ds-field input:not(.input):focus-visible,
.ds-247420 .ds-field textarea:not(.input):focus-visible,
.ds-247420 .ds-field .ds-select:focus-visible,
.ds-247420 .ds-search-input:focus-visible {
  outline: none;
  background: var(--bg);
  /* Focus thickens the baseline to the lead accent — the printed-rule tell. */
  box-shadow: inset 0 0 0 var(--bw-hair, 1px) var(--rule),
              inset 0 calc(-1 * var(--bw-chunk)) 0 var(--accent);
}
.ds-247420 .ds-field input:not(.input):disabled,
.ds-247420 .ds-field textarea:not(.input):disabled,
.ds-247420 .ds-field .ds-select:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
.ds-247420 .ds-field input:not(.input)[aria-invalid="true"],
.ds-247420 .ds-field textarea:not(.input)[aria-invalid="true"] {
  box-shadow: inset 0 0 0 var(--bw-hair, 1px) var(--danger);
}

/* Clear button for text inputs */
.ds-247420 input[type="text"]:not(:placeholder-shown) + .input-clear,
.ds-247420 input[type="email"]:not(:placeholder-shown) + .input-clear,
.ds-247420 input[type="password"]:not(:placeholder-shown) + .input-clear {
  display: inline-flex;
}

.ds-247420 .input-clear {
  display: none;
  align-items: center; justify-content: center;
  width: 32px; height: 32px;
  cursor: pointer;
  color: var(--fg-3);
  background: transparent;
  border: 0;
  padding: 0;
  margin-right: 8px;
  border-radius: var(--r-1);
  transition: background var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease);
}
.ds-247420 .input-clear:hover {
  background: var(--bg-2);
  color: var(--fg);
}

/* ------------------------------------------------------------
   Accessibility Enhancements
   -------------------------------------------------------------- */

/* Base focus-visible: 2px ring + 2px offset clears dark bg-3 surfaces (WCAG 1.4.11) */
.ds-247420 :focus-visible {
  outline: var(--focus-w) solid var(--focus-color);
  outline-offset: var(--focus-offset);
}

/* Enhanced focus-visible for all interactive elements */
.ds-247420[role="button"]:focus-visible,
.ds-247420[role="link"]:focus-visible,
.ds-247420[role="tab"]:focus-visible,
.ds-247420 .row:focus-visible,
.ds-247420 .row[tabindex]:focus-visible {
  outline: var(--focus-w) solid var(--focus-color);
  outline-offset: var(--focus-offset);
}

/* Reduced motion preferences — scoped to the DS surface so it doesn't reach
   into and neutralize the host document's own motion.
   The !important flags here are LOAD-BEARING and must not be "cleaned up":
   `.ds-247420 *` / `.app *` is only (0,1,0) — the universal selector adds
   nothing — yet it has to defeat every animation/transition declaration in the
   kit, many of which are strictly more specific. Concrete rules it would
   otherwise lose to: `.ds-dropzone.dragover` (0,2,0, files.css),
   `.ds-upload-bar.indeterminate .ds-upload-fill` (0,3,0, files.css),
   `.list-item.drag-after::after` (0,2,1, this file). Raising this selector to
   out-specify an open-ended set of future component rules is not possible, so
   the flag is the only correct mechanism — this is a WCAG 2.3.3 guarantee,
   not a stylistic preference. */
@media (prefers-reduced-motion: reduce) {
  .ds-247420 *, .ds-247420 .app * {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  /* Kill the residual paint frame of looping skeleton/shimmer placeholders and
     give reduced-motion users a steady placeholder fill instead.
     Also load-bearing: `.skeleton` is (0,1,0) and must override
     `.ds-session-row-skeleton .ds-skel` (0,2,0), which sets its own shimmer. */
  .ds-247420 .skeleton, .ds-247420 .ds-skeleton, .ds-247420 .ds-session-row-skeleton .ds-skel {
    animation: none !important;
    background: var(--bg-2) !important;
  }
}

/* ------------------------------------------------------------
   Performance
   -------------------------------------------------------------- */

/* Limit layout/style containment to self-contained components. `paint` is
   intentionally omitted — it clips focus rings, tooltips and dropdowns that
   emanate from a contained .row/.btn. */
.ds-247420 .panel, .ds-247420 .row {
  contain: layout style;
}

/* ------------------------------------------------------------
   Mobile Touch Optimizations
   -------------------------------------------------------------- */

@media (hover: none) and (pointer: coarse) {
  /* Mobile devices: larger touch targets, faster responses */
  .ds-247420 .btn, .ds-247420 .btn-primary, .ds-247420 .btn-ghost, .ds-247420 button, .ds-247420 a.row, .ds-247420[role="button"], .ds-247420 .ds-icon-btn, .ds-247420 .composer-btn {
    min-height: 48px;
    min-width: 48px;
    padding: 12px 20px;
  }
  /* Icon buttons stay square — the padding bump above would distort them. */
  .ds-247420 .ds-icon-btn, .ds-247420 .composer-btn { padding: 0; }

  /* Remove hover effects on touch devices (use active instead) */
  .ds-247420 .btn:hover, .ds-247420 .btn-primary:hover, .ds-247420 .btn-ghost:hover {
    background-color: inherit;
  }

  /* Momentum scrolling for iOS */
  .ds-247420 .app-side, .ds-247420 .panel, .ds-247420 .row {
    -webkit-overflow-scrolling: touch;
  }

  /* Long-press feedback for drag-enabled items */
  .ds-247420[draggable="true"], .ds-247420 .draggable {
    -webkit-user-select: none;
    -webkit-touch-callout: none;
  }
}

/* Touch floor also for coarse-pointer devices that report hover (hybrid
   laptops/tablets with mice) — the block above requires hover:none too. */
@media (pointer: coarse) {
  .ds-247420 .ds-icon-btn, .ds-247420 .ds-icon-btn-xs, .ds-247420 .ds-icon-btn-sm, .ds-247420 .ds-icon-btn-base {
    min-width: 44px; min-height: 44px;
  }
  .ds-247420 .btn, .ds-247420 .btn-primary, .ds-247420 .btn-ghost, .ds-247420 .app-topbar nav a, .ds-247420 .app-side a { min-height: 44px; }
}

/* ------------------------------------------------------------
   Theme Transition Smoothness
   -------------------------------------------------------------- */

/* Scoped to the DS wrapper so the design system never reaches out and
   animates the host document's html/body. */
.ds-247420, .ds-247420 .app {
  transition: background-color var(--dur-base) var(--ease),
              color var(--dur-base) var(--ease);
}

/* ------------------------------------------------------------
   Empty State & Loading States
   -------------------------------------------------------------- */

/* Skeleton loading state */
.ds-247420 .skeleton {
  background: linear-gradient(
    90deg,
    var(--bg-2) 0%,
    var(--bg-3) 50%,
    var(--bg-2) 100%
  );
  background-size: 200% 100%;
  animation: skeleton-load 1.5s infinite;
  border-radius: var(--r-2);
}
@media (prefers-reduced-motion: reduce) {
  .ds-247420 .skeleton { animation: none; }
}

@keyframes skeleton-load {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

.ds-247420 .skeleton-line { height: 12px; margin-bottom: 12px; }
.ds-247420 .skeleton-title { height: 24px; margin-bottom: 16px; width: 60%; }

/* ------------------------------------------------------------
   Toast Notification Styles
   -------------------------------------------------------------- */

.ds-247420 .toast {
  position: fixed;
  bottom: 24px; right: 24px;
  background: var(--bg-2);
  border: 1px solid var(--rule);
  border-radius: var(--r-2);
  padding: var(--space-3) var(--space-4);
  max-width: 320px;
  box-shadow: var(--shadow-overlay);
  z-index: var(--z-toast);
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
}

.ds-247420 .toast-action {
  flex: none;
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: var(--r-1);
  color: var(--fg-0);
  padding: var(--space-1) var(--space-2);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}
.ds-247420 .toast-action:hover { background: var(--bg-3); }
.ds-247420 .toast-action:disabled { opacity: 0.6; cursor: default; }

.ds-247420 .toast-dismiss {
  flex: none;
  margin-left: auto;
  background: transparent;
  border: none;
  color: var(--fg-2);
  font-size: 1.1em;
  line-height: 1;
  padding: var(--space-1);
  cursor: pointer;
}
.ds-247420 .toast-dismiss:hover { color: var(--fg-0); }

@keyframes toast-slide-in {
  from {
    opacity: 0;
    transform: translateX(400px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}
@media (prefers-reduced-motion: no-preference) {
  .ds-247420 .toast { animation: toast-slide-in 0.3s ease-out; }
}

.ds-247420 .toast.error { border-color: var(--warn); }
.ds-247420 .toast.success { border-color: var(--green-2); }
/* --sun measures well under 3:1 against every app background (1.1-1.5:1) -
   a border effectively invisible in light theme; --amber is the tuned sibling
   warning tone already used for this exact severity elsewhere. */
.ds-247420 .toast.warning { border-color: var(--amber); }

@media (max-width: 480px) {
  .ds-247420 .toast {
    bottom: 16px; right: 16px; left: 16px;
    max-width: none;
  }
}

/* Stacking host for the imperative toast()/<Toast> component
   (src/components/editor-primitives/toast.js). One fixed-position column;
   individual toasts are appended/removed as siblings inside it rather than
   each toast fixed-positioning itself, so more than one toast in flight
   stacks instead of overlapping at the same bottom-right point. */
.ds-247420 .ds-ep-toast-host {
  position: fixed; z-index: var(--z-toast);
  bottom: 24px; right: 24px;
  display: flex; flex-direction: column-reverse; gap: var(--space-2);
  pointer-events: none;
}
.ds-247420 .ds-ep-toast {
  pointer-events: auto;
  display: flex; align-items: center; gap: var(--space-3);
  background: var(--bg-2); color: var(--fg);
  border: 1px solid var(--rule);
  border-radius: var(--r-2);
  padding: var(--space-3) var(--space-4);
  max-width: 320px;
  box-shadow: var(--shadow-overlay);
  font-size: var(--fs-sm);
}
@media (prefers-reduced-motion: no-preference) {
  .ds-247420 .ds-ep-toast { animation: toast-slide-in 0.3s ease-out; }
  .ds-247420 .ds-ep-toast.leaving { animation: toast-slide-in 0.2s ease-in reverse; }
}
.ds-247420 .ds-ep-toast.kind-error   { border-color: var(--danger, var(--warn)); }
.ds-247420 .ds-ep-toast.kind-success { border-color: var(--green-2); }
.ds-247420 .ds-ep-toast.kind-warning { border-color: var(--amber); }
.ds-247420 .ds-ep-toast-msg { flex: 1; }
.ds-247420 .ds-ep-toast-action {
  flex-shrink: 0;
  background: transparent; border: 0; color: var(--accent-ink);
  font: inherit; font-weight: 600; cursor: pointer; padding: 0;
}
.ds-247420 .ds-ep-toast-action:hover { text-decoration: underline; }

@media (max-width: 480px) {
  .ds-247420 .ds-ep-toast-host { bottom: 16px; right: 16px; left: 16px; }
  .ds-247420 .ds-ep-toast { max-width: none; }
}

/* ============================================================
   Spinner — animated loading indicator
   ============================================================ */
/* Dots spinner — use for small inline/compact loading states. */
.ds-247420 .ds-spinner {
  display: inline-flex;
  gap: var(--space-1);
  align-items: center;
  height: 16px;
}
.ds-247420 .ds-spinner span {
  width: var(--space-1);
  height: var(--space-1);
  border-radius: 50%;
  background: var(--accent);
}
@media (prefers-reduced-motion: no-preference) {
  .ds-247420 .ds-spinner span { animation: ds-bounce 1.4s infinite ease-in-out; }
  .ds-247420 .ds-spinner span:nth-child(1) { animation-delay: 0s; }
  .ds-247420 .ds-spinner span:nth-child(2) { animation-delay: 0.18s; }
  .ds-247420 .ds-spinner span:nth-child(3) { animation-delay: 0.36s; }
}

.ds-247420 .ds-spinner-lg { height: 24px; gap: var(--space-1-75); }
.ds-247420 .ds-spinner-lg span { width: var(--space-1-75); height: var(--space-1-75); }

.ds-247420 .ds-spinner-sm { height: var(--space-2-5); gap: var(--space-hair); }
.ds-247420 .ds-spinner-sm span { width: var(--space-hair); height: var(--space-hair); }

.ds-247420 .ds-spinner-xs { height: var(--space-2); gap: 1px; }
.ds-247420 .ds-spinner-xs span { width: 1.5px; height: 1.5px; }

.ds-247420 .ds-spinner-xl { height: var(--space-5); gap: var(--space-2); }
.ds-247420 .ds-spinner-xl span { width: var(--space-2); height: var(--space-2); }

@keyframes ds-bounce {
  0%, 80%, 100% { transform: translateY(0); }
  40% { transform: translateY(-8px); }
}

/* Two spinner visual languages coexist intentionally: dots (.ds-spinner*)
   are for small inline/compact loading states (buttons, chips, list rows),
   the ring (.loading::after in states-interactions.css) is for larger
   blocking/full-region loading states (panels, full views). */

/* ============================================================
   Skeleton — animated loading placeholder
   ============================================================ */
.ds-247420 .ds-skeleton-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.ds-247420 .ds-skeleton {
  background: linear-gradient(90deg, var(--bg-2) 25%, var(--bg-3) 50%, var(--bg-2) 75%);
  background-size: 200% 100%;
  border-radius: var(--r-1);
}
@media (prefers-reduced-motion: no-preference) {
  .ds-247420 .ds-skeleton { animation: ds-shimmer 1.5s infinite; }
}

@keyframes ds-shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

/* ============================================================
   Alert — contextual message banner
   ============================================================ */
.ds-247420 .ds-alert {
  display: flex;
  gap: var(--space-3);
  align-items: flex-start;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--r-1);
  background: var(--panel-1);
  border: 1px solid var(--rule-strong);
  color: var(--fg);
}

.ds-247420 .ds-alert-info { --alert-tone: var(--sky); }
.ds-247420 .ds-alert-success { --alert-tone: var(--green); }
.ds-247420 .ds-alert-warn { --alert-tone: var(--amber); }
.ds-247420 .ds-alert-error { --alert-tone: var(--warn); }

.ds-247420 .ds-alert-icon {
  flex-shrink: 0;
  color: var(--alert-tone);
  font-weight: 600;
  min-width: var(--icon-md);
  text-align: center;
}

.ds-247420 .ds-alert-content {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.ds-247420 .ds-alert-title {
  font-weight: 500;
  font-size: var(--fs-sm);
}

.ds-247420 .ds-alert-message {
  font-size: var(--fs-sm);
  color: var(--fg-2);
}
.ds-247420 .ds-alert-retry { margin-top: var(--space-2); }

.ds-247420 .ds-alert-dismiss {
  flex-shrink: 0;
  background: none;
  border: none;
  cursor: pointer;
  color: var(--fg-3);
  padding: 0;
  font-size: var(--fs-xs);
  line-height: 1;
  min-width: var(--icon-md);
  text-align: center;
  transition: color var(--dur-snap) var(--ease);
}
.ds-247420 .ds-alert-dismiss:hover { color: var(--fg); }
.ds-247420 .ds-alert-dismiss:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }

/* ============================================================
   Responsive Perfection — Drawer rail, ultrawide cap, mobile compact
   ============================================================ */

/* Tablet + Mobile: side rail becomes off-canvas drawer (≤900px) */
/* Sidebar drawer collapse keys on the SHELL width (@container on .app, which is
   container-type:inline-size) — NOT the viewport. Full-page (freddie) the shell
   == viewport so this is identical to the old @media(max-width:900px); embedded
   in a thebird WM window the drawer + hamburger appear when the WINDOW is narrow
   even on a wide desktop. Drawer/scrim/toggle are absolute (anchored to .app), so
   they overlay the window, not the page. */
@container (max-width: 900px) {
  /* `.app`-scoped for specificity instead of an !important flag. The bare
     `.app-body` half of this list is (0,1,0) and was out-specified by
     `.app-body.no-side` (0,2,0) in topbar.css; scoping lifts both halves to
     (0,2,0)/(0,3,0), which also lands later in source than topbar.css, so the
     drawer collapse wins on merit. `.app` is guaranteed: `.app-body` is
     emitted only as a direct child of `.app` (single call site, shell.js
     AppShell), and this @container query already requires `.app` as its
     container ancestor. */
  .ds-247420 .app .app-body,
  .ds-247420 .app .app-body.no-side { grid-template-columns: 1fr; }
  .ds-247420 .app-side-shell {
    position: absolute;
    /* Full-height overlay drawer: the merged .app-chrome wraps and its real
       height drifts from --app-topbar-h, so anchoring below it misplaces the
       drawer. Overlay the chrome instead (z above --z-header), like ws drawers. */
    left: 0; top: 0;
    height: 100%;
    width: 280px; max-width: 80%;
    transform: translateX(-100%);
    transition: transform var(--dur-base, 240ms) var(--ease, ease);
    /* Drawer rung, above the sticky chrome it slides over. +1 puts the panel
       itself over its own scrim (--z-drawer), which is the layer below it. */
    z-index: calc(var(--z-drawer) + 1);
    background: var(--bg);
    border-right: 1px solid color-mix(in oklab, var(--fg) 8%, transparent);
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  /* Lock the content scroller while the drawer is open so touch pans don't
     scroll the page behind the scrim. */
  .ds-247420 .app-body.side-open .app-main { overflow: hidden; }
  .ds-247420 .app-side-shell.open,
  .ds-247420 .app-side.open,
  .ds-247420 .app-body.side-open .app-side-shell { transform: translateX(0); }
  /* No flags needed: the ONLY other rule that touches these properties is the
     `.app-side` base in topbar.css, which already sets `flex-direction: column`
     and never sets `flex-wrap` (nowrap is the initial value anyway). This rule
     is equal-specificity and later in source, so it wins unaided. */
  .ds-247420 .app-side {
    flex-direction: column;
    flex-wrap: nowrap;
    gap: var(--space-2);
    padding: var(--space-3);
    font-size: var(--fs-sm);
  }
  .ds-247420 .app-side a {
    flex: 0 0 auto; min-width: 0;
    grid-template-columns: 18px 1fr auto;
    padding: 10px 12px;
  }
  .ds-247420 .app-side .group { width: 100%; }
  /* Keep the crumb on mobile — apps mount contextual controls (model picker,
     actions, live status) in crumb-right, so hiding it strands them. Collapse
     the breadcrumb trail/leaf instead and let the right cluster wrap. */
  .ds-247420 .app-crumb { display: flex; flex-wrap: wrap; gap: var(--space-2); padding: 8px 12px; }
  .ds-247420 .app-crumb > .sep, .ds-247420 .app-crumb > span:not(.crumb-right):not(.leaf) { display: none; }
  .ds-247420 .app-crumb .crumb-right { margin-left: 0; }
  .ds-247420 .chat-head { padding: var(--space-2) var(--space-3) var(--space-2); margin-bottom: var(--space-2); }
  .ds-247420 .app-main { padding-left: var(--space-3); padding-right: var(--space-3); }
}

/* Drawer scrim — absolute so it covers THIS shell, shown when drawer open and
   the shell is narrow (@container on .app). */
.ds-247420 .app-side-scrim {
  display: none;
  position: absolute; inset: 0;
  background: color-mix(in oklab, var(--bg) 60%, transparent);
  /* Above the sticky chrome so nav behind an open drawer is not clickable.
     Base of the drawer rung — the panel itself sits one above this. */
  z-index: var(--z-drawer);
}
@container (max-width: 900px) {
  .ds-247420 .app-body.side-open .app-side-scrim { display: block; }
}

/* Nav toggle (hamburger) — hidden when the shell is wide, shown when the shell
   itself is narrow (@container on .app). Absolute so it sits in THIS shell's
   topbar, not pinned to the viewport corner. */
.ds-247420 .app-side-toggle {
  display: none;
  position: absolute; top: calc((var(--app-topbar-h) - 44px) / 2); left: 10px;
  /* Above the sticky topbar/chrome and the drawer/scrim so the translucent
     blurred chrome never paints over or intercepts the toggle. Top of the
     drawer rung: scrim < panel < toggle. */
  z-index: calc(var(--z-drawer) + 2);
  /* 44x44 minimum hit area (WCAG 2.5.5 / Apple HIG) - 36px is below the
     reliable-tap threshold on touch. */
  width: 44px; height: 44px;
  align-items: center; justify-content: center;
  font-size: 18px; line-height: 1;
  background: var(--bg-2); color: var(--fg);
  border: 1px solid var(--rule, color-mix(in oklab, var(--fg) 12%, transparent));
  border-radius: var(--r-1, 6px);
  cursor: pointer;
}
.ds-247420 .app-side-toggle:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
@container (max-width: 900px) {
  .ds-247420 .app-side-toggle { display: inline-flex; }
  .ds-247420 .app-topbar .brand { margin-left: 44px; }
  /* Merged-chrome layout (.app-chrome > .app-crumb) puts the breadcrumb, not
     the topbar brand, at the left edge where the absolute-positioned toggle
     sits — without this the crumb text renders directly under the button. */
  .ds-247420 .app-chrome > .app-crumb { margin-left: 44px; }
}

/* Desktop: the app shell is exactly viewport-height and contains its own
   scroll, so only .app-main (and the side rail) scroll — never the page too.
   Pinning .app to 100dvh + overflow:hidden is what prevents the double
   scrollbar that a min-height:100vh shell + inner overflow:auto produces.
   .app-body fills the gap between topbar/crumb and status via flex:1 rather
   than a hand-computed calc() that drifts from the real chrome heights. */
@media (min-width: 901px) {
  .ds-247420 .app { height: 100dvh; min-height: 0; overflow: hidden; }
  /* Embedded shell (freddie WM window) fills its container, not the viewport. */
  .ds-247420 .fd-root .app { height: 100%; overflow: visible; }
  /* Document-scroll opt-out (see topbar.css's .app-doc-scroll for the full
     rationale) -- this desktop media query is the ACTUAL enforcement point
     for the split-scroll clamp (it reapplies height:100dvh+overflow:hidden
     unconditionally, after topbar.css's own .app rule), so the opt-out must
     win here too, not just in topbar.css, or a long-form page stays clipped
     at every desktop width. */
  .ds-247420 .app.app-doc-scroll { height: auto; min-height: 100dvh; overflow: visible; }
  .ds-247420 .app-body { min-height: 0; flex: 1 1 auto; }
  /* scrollbar-gutter mirrors .app-main's own (topbar.css) — without it the
     thumb draws flush against the sidebar's nav text with zero breathing
     room, reading as a mark stuck to the content rather than a scrollbar
     anchored to the shell's own edge. */
  .ds-247420 .app-side-shell { overflow-y: auto; overflow-x: hidden; min-height: 0; scrollbar-gutter: stable; }
  .ds-247420 .app-main { overflow-y: auto; min-height: 0; }
  .ds-247420 .app-doc-scroll .app-side-shell,
  .ds-247420 .app-doc-scroll .app-main { overflow-y: visible; min-height: auto; }
}

/* Mobile (≤480) status bar compact; hide tail item */
@media (max-width: 480px) {
  .ds-247420 .app-status {
    font-size: var(--fs-micro);
    padding: 4px var(--space-2);
    gap: var(--space-2);
  }
  .ds-247420 .app-status > :last-child:not(:only-child) { display: none; }
}

/* Tiny phone (≤360) topbar compact */
@media (max-width: 360px) {
  .ds-247420 .app-topbar {
    padding: 8px var(--space-2);
    font-size: var(--fs-xs);
    gap: var(--space-2);
  }
  .ds-247420 .app-chrome {
    padding: 8px var(--space-2);
    font-size: var(--fs-xs);
    gap: var(--space-2);
  }
  .ds-247420 .app-topbar nav,
  .ds-247420 .app-topbar > :last-child { display: none; }
  .ds-247420 .brand { font-size: var(--fs-tiny); }
  /* Slightly narrower drawer on tiny phones; max-width:80% remains the guard. */
  .ds-247420 .app-side-shell { width: 260px; }
}

/* ----------------------------------------------------------------------------
   WorkspaceShell — Claude-Desktop / cowork three-column app shell.
   Columns: [ws-rail] [ws-sessions?] [ws-content] [ws-pane?]
   Collapse via .ws-rail-collapsed / .ws-pane-collapsed classes on .ws-shell.
   All sizing/colour from kit tokens; no hardcoded palette.
---------------------------------------------------------------------------- */
.ds-247420 .ws-shell {
  /* Fluid chrome columns: shrink on small laptops, breathe on ultrawide, so the
     ~848px of fixed chrome no longer eats a constant slab. An inline --ws-*-w
     written by a resize drag overrides these (inline wins), pinning the width. */
  --ws-rail-w: clamp(200px, 16vw, 260px);
  --ws-rail-w-collapsed: 60px;
  --ws-sessions-w: clamp(248px, 22vw, 360px);
  --ws-pane-w: clamp(288px, 24vw, 420px);
  display: grid;
  grid-template-columns: var(--ws-rail-w) var(--ws-sessions-w) 1fr var(--ws-pane-w);
  grid-template-areas: "rail sessions content pane";
  height: 100%;
  min-height: 0;
  background: var(--bg);
  color: var(--fg);
}
.ds-247420 .ws-shell.ws-no-sessions { grid-template-columns: var(--ws-rail-w) 1fr var(--ws-pane-w); grid-template-areas: "rail content pane"; }
.ds-247420 .ws-shell.ws-no-pane.ws-no-sessions { grid-template-columns: var(--ws-rail-w) 1fr; grid-template-areas: "rail content"; }
.ds-247420 .ws-shell.ws-no-pane:not(.ws-no-sessions) { grid-template-columns: var(--ws-rail-w) var(--ws-sessions-w) 1fr; grid-template-areas: "rail sessions content"; }
/* G1 stable frame: when a tab has no pane but asks to keep the column geometry
   (ws-no-pane + ws-pane-collapsed), KEEP the 4-track grid with the pane track
   at width 0 instead of collapsing to a 3-track layout - so the shell does not
   re-flow its column count on tab switch. The pane track stays present (0px),
   so chat/history/files/live/settings share one geometry. */
.ds-247420 .ws-shell.ws-no-pane.ws-pane-collapsed:not(.ws-no-sessions) { grid-template-columns: var(--ws-rail-w) var(--ws-sessions-w) 1fr var(--ws-pane-w); grid-template-areas: "rail sessions content pane"; }
.ds-247420 .ws-shell.ws-no-pane.ws-pane-collapsed.ws-no-sessions { grid-template-columns: var(--ws-rail-w) 1fr var(--ws-pane-w); grid-template-areas: "rail content pane"; }
.ds-247420 .ws-shell.ws-rail-collapsed { --ws-rail-w: var(--ws-rail-w-collapsed); }
.ds-247420 .ws-shell.ws-pane-collapsed { --ws-pane-w: 0px; }
/* Sessions column desktop collapse (parity with the pane collapse), reclaiming
   its width for a full-width thread/grid. */
.ds-247420 .ws-shell.ws-sessions-collapsed { --ws-sessions-w: 0px; }
.ds-247420 .ws-sessions-collapsed .ws-sessions { overflow: hidden; border-right: none; }
.ds-247420 .ws-sessions-collapsed .ws-sessions > * { display: none; }
/* Column resize handles: a thin keyboard-accessible separator pinned to the
   right edge of each chrome track; drag (or ArrowLeft/Right) writes a clamped
   inline --ws-*-w. */
.ds-247420 .ws-resizer { position: relative; grid-row: 1 / -1; align-self: stretch; justify-self: end; width: 8px; margin-right: -4px; z-index: var(--z-raised); cursor: col-resize; background: transparent; border: none; padding: 0; touch-action: none; display: flex; align-items: center; justify-content: center; }
.ds-247420 .ws-resizer::after { content: ""; display: block; width: 2px; height: 100%; margin: 0 auto; background: var(--rule); opacity: 0.4; transition: background var(--dur-snap) var(--ease), opacity var(--dur-snap) var(--ease); }
.ds-247420 .ws-resizer:hover::after, .ds-247420 .ws-resizer:focus-visible::after { background: var(--accent); opacity: 1; }
/* Grip: a small pill of dots centered on the handle, visible at rest (low
   contrast) so the resizer reads as a draggable affordance rather than an
   invisible hit zone, then brightens to accent on hover/drag like the line. */
.ds-247420 .ws-resizer::before {
  content: ""; position: absolute; width: 4px; height: 20px; border-radius: var(--r-hair);
  background-image: radial-gradient(circle, var(--fg-3) 1px, transparent 1px);
  background-size: 4px 5px; background-repeat: repeat-y; background-position: center;
  opacity: 0.5; transition: opacity var(--dur-snap) var(--ease), background-image var(--dur-snap) var(--ease);
  pointer-events: none;
}
.ds-247420 .ws-resizer:hover::before, .ds-247420 .ws-resizer:focus-visible::before {
  opacity: 1; background-image: radial-gradient(circle, var(--accent) 1px, transparent 1px);
}
.ds-247420 .ws-resizer:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: -2px; }
/* Active drag state: wider + fully opaque accent grip so the handle a
   fast-moving pointer has left :hover on still visibly reads as "gripped". */
.ds-247420 .ws-resizer.ws-resizer-active::after { background: var(--accent); width: 3px; }
.ds-247420 .ws-resizer.ws-resizer-rail { grid-column: 1; }
.ds-247420 .ws-resizer.ws-resizer-sessions { grid-column: 2; }
.ds-247420 .ws-resizer.ws-resizer-pane { grid-column: 3; justify-self: end; }
/* A resizer must vanish once its TRACK is removed by the staging media blocks,
   else dragging it writes a --ws-*-w var that no longer affects the now-fixed
   drawer (a dead handle). Mirror the 1480 / 1100 / 900 staging exactly. */
@media (max-width: 1480px) { .ds-247420 .ws-resizer.ws-resizer-pane { display: none; } }
@media (max-width: 1100px) { .ds-247420 .ws-resizer.ws-resizer-sessions { display: none; } }
@media (max-width: 900px)  { .ds-247420 .ws-resizer { display: none; } }
/* A resizer must also vanish once its TRACK is desktop-collapsed: a 0px (pane/
   sessions) or 60px icon-only (rail) track has no width to drag, and a drag would
   write an INLINE --ws-*-w that overrides the collapse var (inline wins), fighting
   the collapse state. Mirror the breakpoint guards for the collapse classes. */
.ds-247420 .ws-rail-collapsed .ws-resizer-rail,
.ds-247420 .ws-sessions-collapsed .ws-resizer-sessions,
.ds-247420 .ws-pane-collapsed .ws-resizer-pane { display: none; }
/* Coarse-pointer hit target for hybrid touch laptops above the drawer breakpoint
   (the 8px sliver is un-grabbable by touch). */
@media (pointer: coarse) { .ds-247420 .ws-resizer { width: 16px; margin-right: -8px; } }
/* Ease the rail/pane collapse. Track COUNT stays stable on collapse (only a
   width var flips), so grid-template-columns is animatable; reduced-motion
   drops it to an instant swap. */
@media (prefers-reduced-motion: no-preference) {
  .ds-247420 .ws-shell { transition: grid-template-columns var(--dur-base) var(--ease); }
}
/* Ultrawide: .ws-shell itself stays full-width (fills the real screen edges
   at any viewport, matching .app's own unbounded-width rule in topbar.css) --
   only the 1fr content track's OWN inner measure is capped, so rail/sessions/
   pane keep their fixed/clamped widths and only the middle content column's
   text gets a readability ceiling, instead of the whole grid (and its side
   columns) shrinking together and leaving dead margin on either side of the
   shell. */
.ds-247420 .ws-content { max-width: var(--measure-wide, 1200px); margin-inline: auto; width: 100%; }

/* Rail (left nav) */
.ds-247420 .ws-rail {
  grid-area: rail;
  position: relative;
  display: flex;
  flex-direction: column;
  min-height: 0;
  background: var(--bg-2);
  border-right: var(--bw-hair) solid var(--bg-3);
  overflow: hidden;
  transition: width var(--dur-base) var(--ease);
}
.ds-247420 .ws-rail-toggle {
  position: absolute; top: var(--space-2); right: var(--space-2);
  width: 36px; height: 36px;
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent; border: none; color: var(--fg-2);
  border-radius: var(--r-1); cursor: pointer;
}
.ds-247420 .ws-rail-toggle:hover { background: var(--bg-3); color: var(--fg); }
.ds-247420 .ws-rail-toggle:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
.ds-247420 .ws-rail-inner { display: flex; flex-direction: column; min-height: 0; flex: 1; padding: var(--space-3) var(--space-2); gap: var(--space-2); }
.ds-247420 .ws-rail-head { display: flex; align-items: center; min-height: 36px; padding: 0 var(--space-2); }
/* Wordmark treatment matches .brand (topbar.css) — same display family/weight/
   tracking — so the rail brand reads as the SAME logotype as every other kit's
   topbar wordmark instead of a lighter, differently-set imitation. */
.ds-247420 .ws-rail-brand { font-family: var(--ff-display); font-weight: 700; font-size: var(--fs-body); letter-spacing: var(--tr-tight); color: var(--fg); white-space: nowrap; overflow: hidden; }
.ds-247420 .ws-rail-action {
  display: flex; align-items: center; gap: var(--space-2);
  padding: var(--space-2) var(--space-3); margin: var(--space-1) 0;
  background: var(--accent); color: var(--accent-fg, var(--bg));
  border: none; border-radius: var(--r-1); cursor: pointer;
  font-family: var(--ff-body); font-size: var(--fs-sm); font-weight: 600;
  min-height: 40px; white-space: nowrap; overflow: hidden;
}
.ds-247420 .ws-rail-action:hover { filter: brightness(0.92); }
.ds-247420 .ws-rail-action:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
.ds-247420 .ws-rail-nav { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
.ds-247420 .ws-rail-item {
  display: flex; align-items: center; gap: var(--space-2);
  width: 100%; padding: var(--space-2); min-height: 44px;
  background: transparent; border: none; color: var(--fg-2);
  border-radius: var(--r-1); cursor: pointer; text-align: left;
  font-family: var(--ff-body); font-size: var(--fs-sm); white-space: nowrap; overflow: hidden;
}
.ds-247420 .ws-rail-item:hover { background: var(--bg-3); color: var(--fg); }
.ds-247420 .ws-rail-item:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: -2px; }
.ds-247420 .ws-rail-item.active { background: var(--accent-tint); color: var(--fg); }
.ds-247420 .ws-rail-item-label { flex: 1; overflow: hidden; text-overflow: ellipsis; }
/* Shape/color come from the shared .ds-badge component (see app-shell/primitives.css);
   this class is now just the rail-item layout hook (spacing, collapsed-state targeting). */
.ds-247420 .ws-rail-item-count { flex: none; margin-inline-start: var(--space-1); }
.ds-247420 .ws-rail-item-flag { width: 6px; height: 6px; border-radius: 50%; flex: none; }
.ds-247420 .ws-rail-item-flag.tone-flame { background: var(--flame); box-shadow: 0 0 0 2px color-mix(in oklab, var(--flame) 38%, transparent); }
.ds-247420 .ws-rail-foot { margin-top: auto; padding-top: var(--space-2); border-top: var(--bw-hair) solid var(--bg-3); }

/* Collapsed rail: icon-only — hide text labels, center icons, drop toggle's far-right pin */
.ds-247420 .ws-rail-collapsed .ws-rail-brand,
.ds-247420 .ws-rail-collapsed .ws-rail-action-label,
.ds-247420 .ws-rail-collapsed .ws-rail-item-label,
.ds-247420 .ws-rail-collapsed .ws-rail-item-count { display: none; }
.ds-247420 .ws-rail-collapsed .ws-rail-item,
.ds-247420 .ws-rail-collapsed .ws-rail-action { justify-content: center; gap: 0; }
.ds-247420 .ws-rail-collapsed .ws-rail-head { justify-content: center; }

/* Sessions column */
.ds-247420 .ws-sessions {
  grid-area: sessions;
  min-height: 0; display: flex; flex-direction: column;
  /* Rail and pane both sit on --bg-2 (a recessed plane framing the elevated
     --bg content column); sessions used flat --bg instead, so the
     conversation list and the chat thread read as the SAME surface with only
     a 1px hairline between them - the flattest, most "one big beige box"
     tell in the shell. --bg-2 restores the three-plane depth (rail/sessions
     recessed either side, content elevated in the middle) the design
     already establishes everywhere else. */
  background: var(--bg-2); border-right: var(--bw-hair) solid var(--bg-3);
  overflow: hidden;
}
.ds-247420 .ws-no-sessions .ws-sessions { display: none; }

/* Content column */
.ds-247420 .ws-content { grid-area: content; min-width: 0; min-height: 0; display: flex; flex-direction: column; }
/* Crumb band is the top chrome of the content column: a stable height so the
   top edge aligns with the rail head, and a left gutter matching .ws-main so
   the trail text is not flush against the rail border. */
.ds-247420 .ws-crumb { flex: 0 0 auto; min-height: calc(var(--app-crumb-h) + var(--space-3)); padding: 0 var(--space-5); display: flex; align-items: center; gap: var(--space-1); border-bottom: var(--bw-hair) solid var(--bg-3); }
.ds-247420 .ws-crumb-main { flex: 1 1 auto; min-width: 0; }
/* Content column gutter. Claude-Code-class calm: file grid / dashboard / event
   list / history get a generous, consistent inner gutter instead of butting
   against the 1px column borders. The chat tab opts out via .ws-main--flush
   because the chat thread carries its own --measure gutter. */
.ds-247420 .ws-main { flex: 1; min-height: 0; overflow: auto; display: flex; flex-direction: column; padding: var(--space-4) var(--space-5); }
.ds-247420 .ws-main.ws-main--flush { padding: 0; }
/* Skip-link target (workspace-shell.js's "#ws-main" skip-link, tabindex="-1")
   — it does receive real keyboard-driven focus when the skip link is
   activated, so it needs a visible ring rather than a bare outline:none. */
.ds-247420 .ws-main:focus { outline: none; }
.ds-247420 .ws-main:focus-visible {
  outline: var(--focus-w) solid var(--focus-color);
  outline-offset: calc(-1 * var(--focus-offset));
}

/* Right context pane */
.ds-247420 .ws-pane {
  grid-area: pane; position: relative;
  min-height: 0; display: flex; flex-direction: column;
  background: var(--bg-2); border-left: var(--bw-hair) solid var(--bg-3);
  overflow: hidden; transition: width var(--dur-base) var(--ease);
  overscroll-behavior: contain;
}
.ds-247420 .ws-pane-collapsed .ws-pane { width: 0; border-left: none; }
.ds-247420 .ws-pane-collapsed .ws-pane > * { display: none; }
@media (pointer: coarse) {
  .ds-247420 .ws-rail-toggle { width: 44px; height: 44px; }
  .ds-247420 .ws-drawer-toggle { width: 44px; height: 44px; }
  .ds-247420 .ws-desktop-toggle { width: 44px; height: 44px; }
}

/* Drawer toggles and the scrim are hidden by default and revealed by the staged
   media blocks BELOW - these base rules must precede those blocks in source
   order: at equal specificity the later rule wins regardless of media, so a
   trailing display:none silently kills every drawer affordance (shipped bug:
   the mobile drawers were dead because these lines used to sit last). */
.ds-247420 .ws-drawer-toggle { display: none; align-items: center; justify-content: center; width: 40px; height: 40px; border: none; background: none; color: var(--fg-2); cursor: pointer; border-radius: var(--r-1); }
/* Desktop-only chrome toggle (sessions collapse): inverse of the mobile drawer-toggle. */
.ds-247420 .ws-desktop-toggle { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border: none; background: none; color: var(--fg-2); cursor: pointer; border-radius: var(--r-1); }
.ds-247420 .ws-desktop-toggle:hover { background: var(--bg-2); color: var(--fg); }
.ds-247420 .ws-desktop-toggle:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
@media (max-width: 900px) { .ds-247420 .ws-desktop-toggle { display: none; } }
/* The pane collapse toggle is meaningless once the pane TRACK is dropped (the
   pane becomes a mobile overlay drawer at <=1480px, reached via its own
   drawer-toggle), so hide this crumb control past that breakpoint. */
@media (max-width: 1480px) { .ds-247420 .ws-pane-toggle { display: none; } }
@media (max-width: 480px) { .ds-247420 .ws-crumb { padding-left: var(--space-2); padding-right: var(--space-2); } }
.ds-247420 .ws-scrim { display: none; }

/* Responsive: the columns yield to the CONTENT in stages - the main column is
   the product, so the right pane gives way first, the sessions list second,
   and only below 900px does the rail itself drop to a fixed icon strip. Yielded
   columns stay reachable as overlay drawers (.ws-pane-open / .ws-sessions-open,
   toggled from the crumb bar) over a shared fade scrim. Before this staging the
   4-track grid survived down to 901px and crushed the chat thread to ~245px
   while the 320px context pane kept its full width. */

/* Stage 1 (<=1480px): drop the pane TRACK; the context pane becomes a right
   overlay drawer. */
@media (max-width: 1480px) {
  .ds-247420 .ws-shell,
  .ds-247420 .ws-shell.ws-no-pane,
  .ds-247420 .ws-shell.ws-pane-collapsed,
  .ds-247420 .ws-shell.ws-no-pane.ws-pane-collapsed:not(.ws-no-sessions) {
    grid-template-columns: var(--ws-rail-w) var(--ws-sessions-w) minmax(0, 1fr);
    grid-template-areas: "rail sessions content";
  }
  .ds-247420 .ws-shell.ws-no-sessions,
  .ds-247420 .ws-shell.ws-no-pane.ws-no-sessions,
  .ds-247420 .ws-shell.ws-no-pane.ws-pane-collapsed.ws-no-sessions {
    grid-template-columns: var(--ws-rail-w) minmax(0, 1fr);
    grid-template-areas: "rail content";
  }
  /* Drawer width is a literal: --ws-pane-w is 0px while pane-collapsed, and a
     desktop-collapsed pane must still open as a full drawer. */
  .ds-247420 .ws-pane {
    position: fixed; inset: 0 0 0 auto;
    width: min(340px, 85vw);
    z-index: calc(var(--z-drawer) + 1); transform: translateX(110%);
    transition: transform var(--dur-base) var(--ease);
  }
  .ds-247420 .ws-shell.ws-pane-open .ws-pane { transform: translateX(0); width: min(340px, 85vw); }
  .ds-247420 .ws-shell.ws-pane-open.ws-pane-collapsed .ws-pane { border-left: var(--bw-hair) solid var(--bg-3); }
  .ds-247420 .ws-shell.ws-pane-open.ws-pane-collapsed .ws-pane > * { display: revert; }
  .ds-247420 .ws-pane-drawer-toggle { display: inline-flex; }
  /* Scrim is always present from this stage down so it FADES with the drawer
     instead of hard-appearing; pointer-events gate keeps it inert while closed. */
  .ds-247420 .ws-scrim { display: block; opacity: 0; pointer-events: none; position: fixed; inset: 0; z-index: var(--z-drawer); background: color-mix(in srgb, var(--fg) 38%, transparent); transition: opacity var(--dur-base) var(--ease); }
  .ds-247420 .ws-shell.ws-sessions-open .ws-scrim, .ds-247420 .ws-shell.ws-pane-open .ws-scrim { opacity: 1; pointer-events: auto; }
}

/* Stage 2 (<=1100px): drop the sessions TRACK; the conversation list becomes a
   left overlay drawer. */
@media (max-width: 1100px) {
  .ds-247420 .ws-shell,
  .ds-247420 .ws-shell.ws-no-pane,
  .ds-247420 .ws-shell.ws-pane-collapsed,
  .ds-247420 .ws-shell.ws-no-sessions,
  .ds-247420 .ws-shell.ws-no-pane.ws-no-sessions,
  .ds-247420 .ws-shell.ws-no-pane.ws-pane-collapsed:not(.ws-no-sessions),
  .ds-247420 .ws-shell.ws-no-pane.ws-pane-collapsed.ws-no-sessions {
    grid-template-columns: var(--ws-rail-w) minmax(0, 1fr);
    grid-template-areas: "rail content";
  }
  .ds-247420 .ws-sessions {
    position: fixed; inset: 0 auto 0 0;
    /* Immune to the desktop collapse var (--ws-sessions-w:0px persisted from
       a desktop session would open an invisible 0-width drawer). */
    width: min(max(var(--ws-sessions-w), 280px), 80vw);
    z-index: calc(var(--z-drawer) + 1); transform: translateX(-110%);
    transition: transform var(--dur-base) var(--ease);
    border-right: var(--bw-hair) solid var(--bg-3);
    overscroll-behavior: contain;
  }
  .ds-247420 .ws-shell.ws-sessions-open .ws-sessions { transform: translateX(0); }
  /* Drawer mode ignores the desktop collapsed state: children stay rendered
     and the border returns, so a persisted collapse can't blank the drawer. */
  .ds-247420 .ws-shell.ws-sessions-collapsed .ws-sessions > * { display: revert; }
  .ds-247420 .ws-shell.ws-sessions-collapsed .ws-sessions { border-right: var(--bw-hair) solid var(--bg-3); }
  .ds-247420 .ws-sessions-drawer-toggle { display: inline-flex; }
}

/* Stage 3 (<=900px): single content column; the rail drops to a fixed icon-only
   strip. Labels hide UNCONDITIONALLY here - a 60px rail showing clipped label
   text ('age', 'Ne') is the worst of both worlds. */
@media (max-width: 900px) {
  .ds-247420 .ws-shell,
  .ds-247420 .ws-shell.ws-no-sessions,
  .ds-247420 .ws-shell.ws-no-pane,
  .ds-247420 .ws-shell.ws-pane-collapsed,
  .ds-247420 .ws-shell.ws-no-pane.ws-no-sessions,
  .ds-247420 .ws-shell.ws-no-pane.ws-pane-collapsed:not(.ws-no-sessions),
  .ds-247420 .ws-shell.ws-no-pane.ws-pane-collapsed.ws-no-sessions {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "content";
  }
  /* Persistent chrome, not a drawer: it stays put while the session/pane
     drawers slide over it, so it sits on the header rung below them. */
  .ds-247420 .ws-rail { position: fixed; inset: 0 auto 0 0; width: var(--ws-rail-w-collapsed); z-index: var(--z-header); }
  .ds-247420 .ws-rail .ws-rail-brand,
  .ds-247420 .ws-rail .ws-rail-action-label,
  .ds-247420 .ws-rail .ws-rail-item-label,
  .ds-247420 .ws-rail .ws-rail-item-count,
  .ds-247420 .ws-rail .ds-theme-toggle-label { display: none; }
  .ds-247420 .ws-rail .ws-rail-item, .ds-247420 .ws-rail .ws-rail-action { justify-content: center; gap: 0; }
  .ds-247420 .ws-rail .ws-rail-head { justify-content: center; }
  /* Expand/collapse is meaningless on the icon strip. */
  .ds-247420 .ws-rail-toggle { display: none; }
  .ds-247420 .ws-rail-foot { display: flex; justify-content: center; }
  /* The 1100px rule's closed-state translateX(-110%) was sized for left:0;
     once left shifts to the collapsed rail width, -110% no longer clears the
     viewport and leaves a sliver of the "closed" drawer visible on screen. */
  .ds-247420 .ws-sessions { inset: 0 auto 0 var(--ws-rail-w-collapsed); transform: translateX(calc(-100% - var(--ws-rail-w-collapsed))); }
  .ds-247420 .ws-shell.ws-sessions-open .ws-sessions { transform: translateX(0); }
  .ds-247420 .ws-content { grid-area: content; padding-left: var(--ws-rail-w-collapsed); }
  /* The Crumb's narrow collapse elsewhere keys on the .app @container, which
     does not exist inside WorkspaceShell - mirror it here so the crumb bar
     cannot overflow a narrow viewport (trail hides, leaf ellipsizes, the
     right cluster wraps under instead of pushing past the edge). */
  .ds-247420 .ws-crumb { padding-top: 6px; padding-bottom: 6px; }
  .ds-247420 .ws-crumb .app-crumb { flex-wrap: wrap; min-width: 0; }
  .ds-247420 .ws-crumb .app-crumb > .sep, .ds-247420 .ws-crumb .app-crumb > span:not(.crumb-right):not(.leaf) { display: none; }
  .ds-247420 .ws-crumb .app-crumb .leaf { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
  .ds-247420 .ws-crumb .app-crumb .crumb-right { margin-left: 0; flex-wrap: wrap; min-width: 0; display: inline-flex; }
}
.ds-247420 .ws-drawer-toggle:hover { background: var(--bg-2); color: var(--fg); }
.ds-247420 .ws-drawer-toggle:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }

@media (prefers-reduced-motion: reduce) {
  .ds-247420 .ws-shell, .ds-247420 .ws-rail, .ds-247420 .ws-pane, .ds-247420 .ws-sessions, .ds-247420 .ws-scrim { transition: none; }
  .ds-247420 .ws-resizer::after, .ds-247420 .ws-resizer::before { transition: none; }
}

/* ============================================================
   Row title highlight, expanded-row actions, filter pills.
   ============================================================ */

/* Highlighted match inside a Row title (Row `highlight` prop). */
.ds-247420 .ds-hl { background: color-mix(in oklab, var(--accent) 25%, transparent); color: inherit; border-radius: var(--r-hair); }

/* Action strip rendered inside an EXPANDED Row (Row `actions` prop). The
   buttons stop propagation so they never fire the row onClick. */
.ds-247420 .row-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); width: 100%; padding-top: var(--space-2); }
.ds-247420 .row-act {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 4px 10px; min-height: 28px;
  background: var(--bg-2); border: var(--bw-hair) solid var(--bg-3); border-radius: var(--r-1);
  color: var(--fg-2); font-family: var(--ff-body); font-size: var(--fs-tiny); cursor: pointer;
  transition: background var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease);
}
.ds-247420 .row-act:hover { background: var(--bg-3); color: var(--fg); }
.ds-247420 .row-act:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }

/* FilterPills - a group of pill toggle buttons (aria-pressed). */
.ds-247420 .ds-filter-pills { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.ds-247420 .ds-filter-pill {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 4px 12px; min-height: 28px;
  background: var(--bg-2); border: var(--bw-hair) solid var(--bg-3); border-radius: var(--r-pill);
  color: var(--fg-2); font-family: var(--ff-body); font-size: var(--fs-tiny); cursor: pointer;
  transition: background var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease);
}
.ds-247420 .ds-filter-pill:hover { background: var(--bg-3); color: var(--fg); }
.ds-247420 .ds-filter-pill.active { background: var(--accent-tint); color: var(--fg); border-color: var(--accent); }
.ds-247420 .ds-filter-pill:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }

.ds-247420 .ds-filter-pill[class*="tone-"]::before {
  content: '';
  display: inline-block; width: 7px; height: 7px; border-radius: 50%;
  margin-right: var(--space-1);
  background: currentColor;
}
.ds-247420 .ds-filter-pill.tone-accent  { color: var(--accent-ink); }
.ds-247420 .ds-filter-pill.tone-warn    { color: var(--amber); }
.ds-247420 .ds-filter-pill.tone-live    { color: var(--sky); }
.ds-247420 .ds-filter-pill.tone-sun     { color: var(--sun); }
.ds-247420 .ds-filter-pill.tone-success { color: var(--green-deep); }
.ds-247420 .ds-filter-pill.tone-dim     { color: var(--fg-3); }

/* Touch floor for the new small controls. */
@media (pointer: coarse) {
  .ds-247420 .row-act, .ds-247420 .ds-filter-pill { min-height: 44px; }
}

/* Disabled file actions (read-only row / in-flight mutation) stay visible but inert. */
.ds-247420 .ds-file-act:disabled { opacity: .5; cursor: default; }
.ds-247420 .ds-file-act:disabled:hover { background: transparent; color: var(--fg-3); }
.ds-247420 .ds-file-act:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }

/* ============================================================
   Print — linearize WorkspaceShell so main content prints in full.
   The multi-column CSS grid with overflow:auto regions clips at
   the on-screen height in print/PDF; drop all chrome and let
   .ws-main expand to its natural document height.
   ============================================================ */
@media print {
  .ds-247420 .ws-shell { display: block; }
  .ds-247420 .ws-rail,
  .ds-247420 .ws-sessions,
  .ds-247420 .ws-pane,
  .ds-247420 .ws-crumb,
  .ds-247420 .app-status { display: none; }
  .ds-247420 .ws-main { overflow: visible; height: auto; }
  /* AppShell: unpin the viewport-height frame so content flows across pages,
     and drop all chrome (it prints as page furniture otherwise). */
  .ds-247420 .app { height: auto; min-height: 0; overflow: visible; display: block; }
  .ds-247420 .app-body { display: block; }
  .ds-247420 .app-main { overflow: visible; height: auto; display: block; max-width: 100%; }
  .ds-247420 .app-topbar, .ds-247420 .app-chrome, .ds-247420 .app-crumb, .ds-247420 .app-side-shell, .ds-247420 .app-side-scrim, .ds-247420 .app-side-toggle, .ds-247420 .btn-fab, .ds-247420 .skip-link, .ds-247420 .ds-marquee { display: none; }
  /* Modals/preview: print the content, not a blurred overlay clipped to one page. */
  .ds-247420 .ds-modal-backdrop { position: static; background: none; backdrop-filter: none; padding: 0; }
  .ds-247420 .ds-modal, .ds-247420 .ds-modal-preview { max-width: 100%; max-height: none; box-shadow: none; }
  .ds-247420 .ds-modal-body, .ds-247420 .ds-preview-body, .ds-247420 .ds-preview-code, .ds-247420 .ds-preview-text { overflow: visible; }
  .ds-247420 .ds-modal-actions, .ds-247420 .ds-preview-actions { display: none; }
  /* Data-density: fixed-height log clips; bg-only tiles lose their boundary. */
  .ds-247420 .ds-live-log { height: auto; max-height: none; overflow: visible; }
  .ds-247420 .ds-stat, .ds-247420 .kpi-card { border: 1px solid var(--rule); }
  .ds-247420 .ds-session-row { break-inside: avoid; }

  /* Receipt table (Receipt() in src/components/content/panel.js, `.kv`) — the
     project_page kit's "receipt" section, and any other kv-table consumer.
     On screen it inherits panel/token colors that can render as near-white
     text on a printer's assumed-white page background if the browser's
     "background graphics" print option is off (the common default); force
     the LIGHT-theme ink/paper tokens (real print-page colors, not a raw
     literal — --ink/--paper are the same "true black text on true white
     page" values the light theme already defines) and visible cell borders
     so it stays legible without relying on background-graphics printing at
     all. break-inside:avoid keeps a receipt from splitting a row across a
     page boundary. */
  .ds-247420 .kv { border-collapse: collapse; width: 100%; color: var(--ink); background: var(--paper); }
  .ds-247420 .kv tr { break-inside: avoid; }
  .ds-247420 .kv td { border: 1px solid var(--paper-3); padding: 4px 8px; color: var(--ink); }

  /* Any button/interactive-only affordance still present inside otherwise-
     printable content (copy buttons, expand/collapse toggles, filter pills,
     the receipt/doc section's own action row) is chrome, not content. */
  .ds-247420 .btn, .ds-247420 .btn-primary, .ds-247420 .btn-ghost, .ds-247420 .row-act, .ds-247420 .ds-filter-pills, .ds-247420 .app-search, .ds-247420 input[type="search"] { display: none; }

  /* Force real ink-on-paper for body copy: panel/fg tokens are tuned for
     screen surfaces (including dark theme, where --panel-text can resolve
     near-white) and a printer defaults to a white page regardless of
     data-theme, so an unconverted dark-theme page would print near-invisible
     text without this override. Same --ink/--paper light-theme pair as the
     receipt table above, not a second raw literal. */
  /* No !important: source order already wins here (this rule is the last
     word inside its own @media print block, same as every other override in
     this file), and an unconditional !important on `body` would beat even a
     future print-specific consumer override with higher specificity. */
  .ds-247420 body, .ds-247420 .app-main, .ds-247420 .page-body, .ds-247420 .ds-lede { color: var(--ink); background: var(--paper); }
  .ds-247420 a { color: var(--ink); text-decoration: underline; }
}

/* ============================================================
   Data density — dense observability/dashboard primitives (ported from the
   gmsniff GUI). Components: PhaseWalk, TreeNode, BarRow, StatTile/StatsGrid,
   SubGrid, SessionRow, DevRow, LiveLog/LiveLogEntry. src/components/data-density.js.
   All colors derive from colors_and_type.css tokens — see lint-tokens.mjs.
   ============================================================ */

/* --danger-surface: semantic surface for a deviation/error callout background
   (gmsniff's #3a1a1a dark-red-tinted panel). Derived from --warn so it always
   reads as "danger" against the active theme instead of a baked literal. */
.ds-247420 {
  --danger-surface: color-mix(in oklab, var(--warn) 14%, var(--bg));
}

/* PhaseWalk */
.ds-247420 .ds-phasewalk { display: inline-flex; gap: var(--space-hair); vertical-align: middle; }
.ds-247420 .ds-phasewalk-seg {
  width: 18px; height: 10px; border-radius: var(--r-hair);
  background: var(--bg-3); position: relative;
}
.ds-247420 .ds-phasewalk-seg.is-reached { background: var(--success); }
.ds-247420 .ds-phasewalk-seg.is-gap { background: var(--warn); }
/* Standalone PhaseWalk reserves headroom for the labels instead of letting
   them escape upward and overlap the element above. */
/* 15px is load-bearing, not off-scale drift: it must equal the absolute
   `top: -15px` the label below is lifted by, so the label clears the segments
   without a gap. Snapping the pad to --space-3 (16px) alone would desync the
   pair. */
.ds-247420 .ds-phasewalk { padding-top: 15px; }
.ds-247420 .ds-phasewalk-lbl {
  position: absolute; top: -15px; left: 0;
  font-size: var(--fs-micro); color: var(--fg-3); white-space: nowrap;
}
/* Inline in a single-line SessionRow the labels would overlap the row above —
   segments already carry title tooltips. */
.ds-247420 .ds-session-row .ds-phasewalk { padding-top: 0; }
.ds-247420 .ds-session-row .ds-phasewalk-lbl { display: none; }

/* TreeNode */
.ds-247420 .ds-tree-node {
  padding: var(--space-1-5) var(--space-2-5); border-left: var(--bw-rule) solid var(--bg-3);
  margin: var(--space-hair) 0 var(--space-hair) var(--space-3); font-size: var(--fs-tiny);
  min-width: 0; overflow-wrap: anywhere;
}
@media (max-width: 400px) {
  .ds-247420 .ds-tree-node { margin-left: var(--space-2); padding-left: var(--space-2); }
}
.ds-247420 .ds-tree-node.is-phase { border-color: var(--accent); background: var(--bg-3); font-weight: 600; }
.ds-247420 .ds-tree-node.is-deviation { border-color: var(--warn); background: var(--danger-surface); color: var(--warn); }
.ds-247420 .ds-tree-node.is-mutable-resolve { border-color: var(--success); }
.ds-247420 .ds-tree-node.is-prd-add { border-color: var(--amber); }
.ds-247420 .ds-tree-node-ts { margin-right: var(--space-2); color: var(--fg-3); }
.ds-247420 .ds-tree-node-pills { margin-left: var(--space-1-75); }
.ds-247420 .ds-tree-node-reason { margin-top: var(--space-hair); color: var(--fg-2); }
.ds-247420 .ds-tree-node-deviation { margin-top: var(--space-hair); }
.ds-247420 .ds-tree-node-residuals { margin-top: var(--space-hair); color: var(--fg-3); }

/* BarRow */
.ds-247420 .ds-bar-row { display: flex; align-items: center; gap: var(--space-2); margin: var(--space-half) 0; font-size: var(--fs-tiny); }
.ds-247420 .ds-bar-row-label { width: 100px; flex-shrink: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ds-247420 .ds-bar-bg { flex: 1; min-width: 60px; background: var(--bg-3); border-radius: var(--r-hair); height: 6px; overflow: hidden; }
.ds-247420 .ds-bar-fill { background: var(--fg-3); height: 100%; border-radius: var(--r-hair); }
.ds-247420 .ds-bar-fill.is-active { background: var(--accent); }
.ds-247420 .ds-bar-row-value { flex-shrink: 0; color: var(--fg-2); max-width: 40%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@media (max-width: 400px) {
  .ds-247420 .ds-bar-row-label { width: 72px; }
}

/* StatTile / StatsGrid */
/* min(140px, 100%) keeps the grid from collapsing to 1 column early inside
   padded panels; the wrapper div stretches its tile to equal row height. */
.ds-247420 .ds-stats-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(140px, 100%), 1fr)); gap: var(--space-2); }
.ds-247420 .ds-stats-grid > div { display: grid; }
@media (max-width: 360px) {
  .ds-247420 .ds-stats-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-1); }
}
.ds-247420 .ds-stats-grid-empty { padding: var(--space-3); color: var(--fg-3); font-size: var(--fs-tiny); }
.ds-247420 .ds-stat { min-width: 0; background: var(--bg-3); border-radius: var(--r-0); padding: var(--space-2-5) var(--space-2-75); }
.ds-247420 .ds-stat-val { font-size: clamp(18px, 5cqi, 22px); font-weight: 700; color: var(--accent-ink); overflow-wrap: anywhere; }
.ds-247420 .ds-stat-val.rate-big { font-size: clamp(24px, 7cqi, 32px); color: var(--success); }
.ds-247420 .ds-stat-val.err-rate { font-size: clamp(24px, 7cqi, 32px); color: var(--warn); }
.ds-247420 .ds-stat-lbl { font-size: var(--fs-micro); color: var(--fg-3); margin-top: var(--space-hair); }

/* RateCell — tone-colored numeric table cell (success-rate / percentile-latency
   columns), ported from docstudio's endpointsView() success-rate coloring.
   Neutral at rest; color carries meaning only, matching Table's sortable-header
   restraint pattern. */
.ds-247420 .ds-rate-cell { font-variant-numeric: tabular-nums; font-weight: 600; }
.ds-247420 .ds-rate-cell-neutral { color: var(--fg-2); font-weight: 400; }
.ds-247420 .ds-rate-cell-good { color: var(--success); }
.ds-247420 .ds-rate-cell-warn { color: var(--warn); }
.ds-247420 .ds-rate-cell-bad { color: var(--danger); }

/* Inline row — flex row with centered items and a small gap, for demo/kit
   markup that needs to lay siblings out horizontally without an inline style. */
.ds-247420 .ds-inline-row { display: flex; align-items: center; gap: var(--space-2, 8px); }

/* SubGrid */
.ds-247420 .ds-sub-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: var(--space-1-75); margin-top: var(--space-2); }
.ds-247420 .ds-sub-grid-empty { padding: var(--space-3); color: var(--fg-3); font-size: var(--fs-tiny); }
.ds-247420 .ds-sub-btn {
  padding: var(--space-1-75) var(--space-2-5); background: var(--bg-3); border: var(--bw-hair) solid var(--bg-3);
  border-radius: var(--r-0); font-size: var(--fs-tiny); cursor: pointer; color: var(--fg);
  text-align: left; font-family: var(--ff-body);
}
.ds-247420 .ds-sub-btn:hover { border-color: var(--accent); color: var(--accent-ink); }
.ds-247420 .ds-sub-btn span { display: block; font-size: 18px; font-weight: 700; color: var(--accent-ink); }

/* SessionRow — the RICH treatment (hover/focus/active, rail tones, nesting,
   inline rename/delete) lives in chat.css with the rest of the chat session
   list. But SessionRow is part of this data-density family and is used by kits
   that never link chat.css (gm_inspector links only colors_and_type +
   app-shell + editor-primitives), where the component rendered with NO layout
   at all: gap resolved to `normal`, so id/counts/devcnt/phasewalk/span all
   butted together at 0px. The layout floor therefore belongs here, next to the
   sub-parts it positions, so the component is never shipped shapeless.
   chat.css still layers its interactive states on top; these declarations are
   identical to the ones there, so a kit loading both gets the same result. */
.ds-247420 .ds-session-row {
  position: relative; display: flex; align-items: center; gap: var(--space-2);
  width: 100%; padding: var(--space-2); min-height: 52px; margin-bottom: var(--space-hair);
  background: transparent; border: none; border-radius: var(--r-1);
  text-align: left; color: var(--fg);
}
/* Only an interactive row gets the pointer affordance (chat.css sets cursor
   unconditionally because every row there is clickable; here rows are often
   static readouts). */
.ds-247420 .ds-session-row[role="button"], .ds-247420 a.ds-session-row { cursor: pointer; }

/* ConversationList's title/meta layout had no dedicated rule anywhere (only a
   stray cross-reference comment in panel-row.css), so the title sat at flex's
   default 0-basis and lost the space race to its meta siblings (agent name,
   status dot) - titles truncated to a character or two ("d…") well before the
   row ran out of real width. .ds-session-main grows to fill the row; its
   title/sub children get the min-width:0 an ellipsis needs inside a flex
   item; the meta/actions siblings take only what their content needs. */
.ds-247420 .ds-session-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 0; }
.ds-247420 .ds-session-title {
  display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--fs-sm); font-weight: 500; color: var(--fg);
}
.ds-247420 .ds-session-sub {
  display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--fs-micro); color: var(--fg-3);
}
.ds-247420 .ds-session-meta, .ds-247420 .ds-session-row-actions { flex: 0 0 auto; display: flex; align-items: center; gap: var(--space-1); }

/* Sub-parts that decorate the row above. */
.ds-247420 .ds-session-row-id {
  font-family: var(--ff-mono); font-size: var(--fs-micro); color: var(--accent-ink);
  flex: 0 1 160px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ds-247420 .ds-session-row-counts {
  font-size: var(--fs-micro); color: var(--fg-3);
  flex: 1 1 200px; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
@media (max-width: 640px) {
  .ds-247420 .ds-session-row-span { flex-basis: 100%; }
}
.ds-247420 .ds-session-row-devcnt { color: var(--warn); font-weight: var(--fw-medium, 500); }
.ds-247420 .ds-session-row-span { font-size: var(--fs-micro); color: var(--fg-3); }

/* DevRow */
.ds-247420 .ds-dev-row {
  background: var(--danger-surface); border-left: var(--bw-chunk) solid var(--warn);
  padding: var(--space-2) var(--space-2-75); margin: var(--space-1) 0; border-radius: var(--r-hair); font-size: var(--fs-tiny);
}
.ds-247420 .ds-dev-row strong { color: var(--warn); }

/* LiveLog — natural height with a viewport-tracking cap (dvh follows mobile
   URL-bar chrome; min-height keeps short-landscape from going near-zero). */
.ds-247420 .ds-live-log {
  height: auto;
  max-height: calc(100vh - 200px);
  max-height: calc(100dvh - 200px);
  min-height: 160px;
  overflow-y: auto; overflow-x: hidden;
  font-family: var(--ff-mono); font-size: var(--fs-micro); line-height: 1.5;
}
/* Embedded in a WM window the viewport calc is meaningless — fill the container. */
.ds-247420 .fd-root .ds-live-log { height: 100%; max-height: none; min-height: 0; flex: 1 1 auto; }
.ds-247420 .ds-live-log-empty { height: auto; min-height: 0; padding: var(--space-3); color: var(--fg-3); }
.ds-247420 .ds-live-log-entry { padding: var(--space-hair) var(--space-2); border-bottom: var(--bw-hair) solid var(--bg-3); overflow-wrap: anywhere; }
.ds-247420 .ds-live-log-entry:hover { background: var(--bg-3); }
.ds-247420 .ds-live-log-ts { margin-right: var(--space-1-75); color: var(--fg-3); }
.ds-247420 .ds-live-log-subtag { margin-right: var(--space-1-75); padding: 0 var(--space-1-5); border-radius: var(--r-hair); font-size: var(--fs-micro); }
.ds-247420 .ds-live-log-preview { margin-left: var(--space-1-75); color: var(--fg-3); word-break: break-word; }

/* Touch floors for the dense controls (not covered by the mobile width blocks). */
@media (pointer: coarse) {
  .ds-247420 .ds-sub-btn { min-height: 44px; }
  .ds-247420 .ds-session-row { min-height: 44px; }
  .ds-247420 .ds-live-log-entry { padding-block: var(--space-1-75); }
}


/* == appended: popovers-files == */
/* FilePreviewMedia controls row — spacing + coarse-pointer tap floor */
.ds-247420 .ds-preview-media-controls {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}
@media (hover: none), (pointer: coarse) {
  .ds-247420 .ds-preview-media-controls .chat-code-copy,
  .ds-247420 .chat-code-copy-head {
    min-height: 44px;
    min-width: 44px;
  }
}

/* == appended: ui-kits == */
/* ============================================================
   ui_kit responsive helpers (kits-grids sweep) */
/* App-surface root. .ds-section carries a --space-8 (96px) editorial rhythm
   that is correct between landing-page sections and wrong as the ROOT wrapper
   of an Operate surface, where .app-main has already padded the region — used
   there it just pushes the page heading a screen-inch down and adds dead space
   below the last panel. This is the app-mode counterpart: no outer margin, and
   it owns the heading -> lede -> first-panel cadence so each kit stops
   re-deriving it. Pair with .ds-section-pad for the shadow inset. */
.ds-247420 .ds-app-surface { margin: 0; display: flow-root; }
/* Operate surfaces run the compact heading ceiling. --fs-h1 tops out at 64px,
   a display size that makes the page TITLE outweigh the data it labels; the
   app scale (--fs-h1-app, 22-30px) is what these tokens exist for. Applied
   here rather than as a data-attribute on each kit shell so any surface using
   this root gets the app voice without a per-kit opt-in that kits forget. */
/* Descendant, not direct-child: a kit that wraps its title in a head row (an
   h1 beside a theme toggle, say) still gets the app voice. The direct-child
   form silently missed exactly that shape and left those titles at the 64px
   display ceiling -- the surface opted in but the rule did not reach the
   heading. Nested .ds-section content is unaffected because nothing re-raises
   the size below this point. */
.ds-247420 .ds-app-surface h1, .ds-247420 .ds-app-surface .t-h1 { font-size: var(--fs-h1-app); line-height: 1.15; }
.ds-247420 .ds-app-surface h2, .ds-247420 .ds-app-surface .t-h2 { font-size: var(--fs-h2-app); }
.ds-247420 .ds-app-surface h3, .ds-247420 .ds-app-surface .t-h3 { font-size: var(--fs-h3-app); }
/* The lede is supporting chrome under an app title, not a marketing stand-first:
   it drops to body size and keeps a readable measure instead of --fs-xl. */
.ds-247420 .ds-app-surface > .lede, .ds-247420 .ds-app-surface > .t-lede { font-size: var(--fs-body); max-width: 60ch; }
.ds-247420 .ds-app-surface > h1,
.ds-247420 .ds-app-surface > h2 { margin-bottom: var(--space-2); }
/* The lede belongs to the heading above it, so it sits tight to that heading
   and separates generously from the content it introduces. */
.ds-247420 .ds-app-surface > .lede,
.ds-247420 .ds-app-surface > .t-lede { margin-bottom: calc(var(--space-6) * var(--density)); }
/* No leading margin on the first block or trailing margin on the last — the
   shell owns the region's outer edges. */
.ds-247420 .ds-app-surface > :first-child { margin-top: 0; }
.ds-247420 .ds-app-surface > :last-child { margin-bottom: 0; }

/* Section padding is the shell's job (.app-main owns --pad-x). This only adds
   the small inset a kit root needs so panel shadows are not clipped by the
   scroll container, and it scales with density like every other app metric —
   the old clamp(6px,1.2vw,16px) sized against the VIEWPORT inside a
   container-query layout that measures everything else in cqi. */
.ds-247420 .ds-section-pad { padding: calc(var(--space-1) * var(--density)); }

/* Separation between sibling panels. This is the OUTER rhythm: it must exceed
   .panel-body's 16px inner sibling gap, otherwise proximity says two unrelated
   panels belong together more tightly than two rows inside one panel. The old
   value was --space-2 (8px), which inverted exactly that relationship and made
   the class named "gap" the only rule in the system that removed separation. */
.ds-247420 .ds-panel-gap { margin: calc(var(--space-5) * var(--density)) 0; }
/* Density-scaled sibling rhythm for any run of stacked panels, so the cadence
   holds without each kit re-declaring a margin per panel. */
.ds-247420 .ds-panel-gap + .ds-panel-gap { margin-top: calc(var(--space-5) * var(--density)); }

/* A panel inside a gap-owning parent (grid/flex row). .panel ships a 24px
   bottom margin for the stacked case; inside a row that margin fights the
   parent's gap and makes the last row of a wrapping grid sit on a different
   rhythm than the columns. The container owns separation, so the panel drops
   its own. Also stretches to the row's height so a short panel next to a tall
   one does not leave a ragged bottom edge. */
/* Drops only the panel's own bottom margin — the row container owns separation.
   Height is deliberately NOT forced here: whether panels in a row should match
   heights depends on whether they are a SET (trio: equal peers, matching edge
   reads as one band) or a PAIR of different things (duo: a 4-bar chart beside a
   5-row table, where forcing equality just relocates the dead space inside the
   shorter panel). Each container decides via align-items below. */
.ds-247420 .ds-panel-flush { margin-bottom: 0; }

/* Paired panels. Container-relative (the shell is the CQ container), so the
   split collapses when the PANELS are cramped rather than when the viewport
   crosses an arbitrary width — with a sidebar open those are far apart.
   minmax(0,1fr) lets a scrolling table shrink instead of forcing overflow. */
.ds-247420 .ds-panel-duo {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: calc(var(--space-3) * var(--density));
  margin: calc(var(--space-5) * var(--density)) 0;
  /* A duo pairs two DIFFERENT things (a 4-bar chart beside a 5-row table).
     Stretching them to a common height does not create equality, it just moves
     the shorter panel's dead space inside its own body. Each takes its natural
     height and they align at the top, where the eye actually starts. */
  align-items: start;
}
@container (max-width: 900px) {
  .ds-247420 .ds-panel-duo { grid-template-columns: minmax(0, 1fr); }
}
/* Fallback for kit roots that are not themselves a container (older kit shells
   mount outside .app/.ds-stage); harmless where the CQ rule already applies. */
@media (max-width: 760px) {
  .ds-247420 .ds-panel-duo { grid-template-columns: minmax(0, 1fr); }
}

/* Three equal reference panels. Real grid tracks, NOT percentage flex-basis:
   three 33.33% bases plus two gaps exceed 100% and wrap the last panel onto
   its own row. Steps 3 -> 2 -> 1 on container width so the panels reflow when
   they are actually cramped rather than at a viewport number that ignores how
   much chrome the shell is holding. */
.ds-247420 .ds-panel-trio {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: calc(var(--space-3) * var(--density));
  margin: calc(var(--space-5) * var(--density)) 0;
  /* A trio IS a set of equal peers, so a shared bottom edge reads as one band
     rather than three ragged cards — the opposite call from .ds-panel-duo
     above, and the reason these are two classes and not one. */
  align-items: stretch;
}
.ds-247420 .ds-panel-trio > .panel { height: 100%; }
@container (max-width: 1100px) {
  .ds-247420 .ds-panel-trio { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@container (max-width: 720px) {
  .ds-247420 .ds-panel-trio { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 760px) {
  .ds-247420 .ds-panel-trio { grid-template-columns: minmax(0, 1fr); }
}
/* wide dense rows/tables scroll inside their panel, never the page */
.ds-247420 .ds-scroll-x { overflow-x: auto; -webkit-overflow-scrolling: touch; max-width: 100%; }

/* --- slide deck ------------------------------------------- */
.ds-247420 .ds-deck-stage {
  aspect-ratio: 16 / 9;
  width: 100%;
  max-width: min(1100px, 100%);
  margin: var(--space-3) auto;
  background: var(--bg-2);
  border-radius: var(--r-2);
  padding: clamp(var(--space-3), 5vw, var(--space-7));
  box-sizing: border-box;
  display: flex;
  overflow: hidden;
  container-type: inline-size; /* cqw units track the stage, not the viewport */
}
@media (max-height: 500px) and (orientation: landscape) {
  .ds-247420 .ds-deck-stage {
    max-height: calc(100vh - 140px);
    max-height: calc(100dvh - 140px);
    aspect-ratio: auto;
  }
}
.ds-247420 .ds-deck-slide {
  flex: 1; display: flex; min-width: 0;
  animation: ds-deck-slide-in var(--dur-base) var(--ease);
}
@media (prefers-reduced-motion: reduce) {
  .ds-247420 .ds-deck-slide { animation: none; }
}
@keyframes ds-deck-slide-in {
  from { opacity: 0; transform: translateY(var(--space-1-75)); }
  to { opacity: 1; transform: none; }
}
.ds-247420 .ds-deck-controls {
  /* 14px sits between --space-2-75 (12) and --space-3 (16) with no tier; it is
     the deck control spacing tuned against the mono glyph run. */
  display: flex; align-items: center; justify-content: center; gap: 14px;
  padding: var(--space-2); font-family: var(--ff-mono); font-size: var(--fs-sm);
}
.ds-247420 .ds-deck-count { color: var(--fg-3); }
@media (pointer: coarse) {
  .ds-247420 .ds-deck-controls .btn { min-height: 44px; min-width: 44px; }
}
/* Dot progress indicator: one dot per slide, current slide filled with
   --accent (same fill token .ds-dot-on uses elsewhere in the system), rest
   dim at --fg-3 so the row reads as a track, not a second counter. */
.ds-247420 .ds-deck-progress {
  display: flex; align-items: center; justify-content: center;
  gap: var(--space-1-5); padding-bottom: var(--space-2);
}
.ds-247420 .ds-deck-dot {
  width: var(--space-1-5); height: var(--space-1-5); border-radius: 50%;
  background: var(--fg-3); flex: 0 0 auto;
  transition: background var(--dur-base) var(--ease), transform var(--dur-base) var(--ease);
}
.ds-247420 .ds-deck-dot--active { background: var(--accent); transform: scale(1.3); }
/* Kit small-label voice — one local composition of the house `.t-label` recipe
   (base.css) for every eyebrow/label in the appended kit blocks below. These
   previously each restated the four declarations with drifted tracking
   (0.1em / 0.06em / --tr-label) and drifted size (11px / --fs-micro); they now
   share --fs-nano + --tr-label. Members declare only their real differences.
   --fs-nano because these are dense kit chrome, never body or UI text. */
.ds-247420 .ds-slide-eyebrow,
.ds-247420 .ds-field-eyebrow,
.ds-247420 .ds-auth-field-label,
.ds-247420 .ds-lightbox-tag {
  font-family: var(--ff-mono);
  font-size: var(--fs-nano);
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--panel-text-3);
}

/* Deliberate overrides: the deck slide eyebrow sits on the app --fg ramp (it is
   full-bleed slide chrome, not panel chrome) and owns its own bottom gutter. */
.ds-247420 .ds-slide-eyebrow {
  color: var(--fg-3);
  margin-bottom: clamp(var(--space-2-75), 2.5cqw, var(--space-4));
}
.ds-247420 .ds-slide-col { display: flex; flex-direction: column; justify-content: center; gap: clamp(var(--space-2-75), 2.5cqw, var(--space-4)); min-width: 0; }
.ds-247420 .ds-slide-col--start { align-items: flex-start; }
.ds-247420 .ds-slide-col--narrow { max-width: 38ch; }
.ds-247420 .ds-slide-col--quote { max-width: 42ch; }
.ds-247420 .ds-slide-hero {
  font-size: clamp(var(--fs-h2), 6cqw, var(--fs-hero));
  line-height: var(--lh-tight); letter-spacing: var(--tr-tight); font-weight: 600;
  color: var(--slide-accent, var(--accent-ink));
  overflow-wrap: anywhere;
}
.ds-247420 .ds-slide-h1 {
  font-size: clamp(var(--fs-h3), 4.5cqw, var(--fs-h1));
  line-height: var(--lh-snug); color: var(--fg); font-weight: 500; min-width: 0;
}
.ds-247420 .ds-slide-h1--lead { margin-bottom: var(--space-2-75); }
.ds-247420 .ds-slide-quote-body {
  font-size: clamp(var(--fs-lg), 3.5cqw, var(--fs-h2));
  line-height: var(--lh-snug); color: var(--fg); font-weight: 400; font-style: italic;
}
.ds-247420 .ds-slide-cite { font-family: var(--ff-mono); font-size: var(--fs-sm); color: var(--fg-3); }
.ds-247420 .ds-slide-bullet {
  /* 18px ceiling has no tier (--space-3 is 16, --space-3-5 is 20); it is the
     bullet key/value split tuned against the clamp(56px,18%,100px) key column. */
  display: flex; gap: clamp(var(--space-2), 2vw, 18px); align-items: baseline;
  border-bottom: 1px solid var(--rule); padding: var(--space-2-75) 0; flex-wrap: wrap;
}
.ds-247420 .ds-slide-bullet-key {
  flex: 0 1 clamp(56px, 18%, 100px); min-width: 0;
  font-family: var(--ff-mono); font-size: var(--fs-sm);
  color: var(--slide-accent, var(--accent-ink));
}
.ds-247420 .ds-slide-bullet-val { color: var(--fg-2); font-size: var(--fs-lg); min-width: 0; }
.ds-247420 .ds-slide-split { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(var(--space-2-75), 3vw, var(--space-5)); margin-top: var(--space-2-75); }
@media (max-width: 760px) {
  .ds-247420 .ds-slide-split { grid-template-columns: 1fr; }
}
.ds-247420 .ds-slide-split-cell {
  padding: var(--space-3-5); background: var(--bg-2); border-radius: var(--r-2);
  color: var(--fg-2); font-size: var(--fs-lg); line-height: var(--lh-base); min-width: 0;
}
/* DELIBERATE accent spine — this modifier's only job is to mark the emphasized
   half of a split slide (see ui_kits/slide_deck/app.js), so the rule is the
   whole component, not decoration on a card. Routed through the house
   .panel-spine idiom (::before rail at --bw-chunk, squared leading corners)
   instead of an off-scale 3px border-left. */
.ds-247420 .ds-slide-split-cell--accent {
  position: relative;
  padding-left: calc(var(--space-3-5) + var(--bw-chunk));
  border-radius: 0 var(--r-2) var(--r-2) 0;
}
.ds-247420 .ds-slide-split-cell--accent::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0;
  width: var(--bw-chunk); background: var(--slide-accent, var(--accent));
  border-radius: var(--bw-chunk) 0 0 var(--bw-chunk);
}

/* --- error 404 --------------------------------------------- */
.ds-247420 .ds-err-hero {
  /* 36px/22px clamp ceilings and the 14px gap are off-scale on purpose: this
     hero is centred around a clamp(56px,20vw,120px) numeral and the insets are
     tuned to it, not to the panel rhythm the --space-* scale encodes. */
  padding: clamp(var(--space-3), 5vw, 36px) clamp(var(--space-2-75), 4vw, 22px);
  text-align: center; display: flex; flex-direction: column; gap: 14px; align-items: center;
}
.ds-247420 .ds-err-numeral {
  font-family: var(--ff-mono); font-size: clamp(56px, 20vw, 120px);
  /* Shares the headline's weight (base.css h1: font-weight:600) so the two
     most important elements on the page read as one weight relationship
     instead of a thin numeral against a bold headline. */
  font-weight: 600;
  line-height: 1; color: var(--panel-text-3); letter-spacing: -0.03em;
}
.ds-247420 .ds-err-path {
  display: inline-flex; gap: var(--space-1-75); align-items: baseline; flex-wrap: wrap; max-width: 100%;
  font-family: var(--ff-mono); font-size: var(--fs-tiny); color: var(--panel-text-2);
  background: var(--panel-1); padding: var(--space-1-75) var(--space-2-75); border-radius: var(--r-0);
  overflow-wrap: anywhere; word-break: break-all;
}

/* --- gallery ------------------------------------------------ */
.ds-247420 .ds-tile-grid {
  padding: clamp(var(--space-2), 2.5vw, var(--space-3));
  display: grid; grid-template-columns: repeat(auto-fill, minmax(min(180px, 100%), 1fr)); gap: var(--space-2);
}
.ds-247420 .ds-tile-grid--tight { grid-template-columns: repeat(auto-fill, minmax(min(140px, 100%), 1fr)); }
.ds-247420 .ds-swatch-grid {
  padding: clamp(var(--space-2), 2.5vw, var(--space-3));
  display: grid; grid-template-columns: repeat(auto-fill, minmax(min(160px, 100%), 1fr)); gap: var(--space-2-75);
}
.ds-247420 .ds-gallery-tile {
  cursor: pointer; display: flex; flex-direction: column; gap: var(--space-1-75);
  padding: var(--space-2-75); border-radius: var(--r-1);
  /* Fixed aspect-ratio (not just a min-height floor) so every tile in the grid
     occupies the same shape regardless of caption length — the ascii specimen
     is centered inside via .ds-tile-cap's flex alignment, the equivalent of
     object-fit: cover for a text tile with no bitmap to crop. */
  aspect-ratio: 4 / 3;
  border: 0; text-align: left; font: inherit; color: inherit;
  background: var(--tile-tone, var(--panel-1));
  transition: transform var(--dur-base) var(--ease), box-shadow var(--dur-base) var(--ease);
}
/* Flat tonal fill only (house style: no bespoke tile chrome, no borders — see
   .ds-feature above), so the hover affordance is a lift, matching the same
   translateY + shadow idiom .chat-msg:hover .chat-bubble already uses. */
.ds-247420 .ds-gallery-tile:hover { transform: translateY(-2px); box-shadow: 0 2px 8px color-mix(in oklab, var(--fg) 10%, transparent); }
.ds-247420 .ds-gallery-tile:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
.ds-247420 .ds-gallery-tile--tight { aspect-ratio: 3 / 2; }
/* The lightbox scrim/card chrome itself is now Dialog's (.ds-ep-dialog /
   .ds-ep-dialog-backdrop in editor-primitives.css) — Dialog already ships
   role="dialog", aria-modal, Tab-trap and focus-restore-on-close, which the
   former hand-rolled .ds-lightbox/.ds-lightbox-card overlay had none of. Only
   the gallery-specific content rules (head row, tag, carousel sizing, tile
   preview) remain here. */
.ds-247420 .ds-lightbox-preview {
  /* 36px ceiling has no tier (--space-5 is 32, --space-6 is 48). */
  padding: clamp(var(--space-3), 6vw, 36px); border-radius: var(--r-1); text-align: center;
  font-family: var(--ff-mono); white-space: pre-line; font-size: clamp(16px, 5vw, 24px);
  background: var(--tile-tone, var(--panel-1));
}
/* Carousel adoption in the lightbox: each item takes the track's full inner
   width so exactly one tile is visible per page, matching the modal's own
   fixed 520px card width rather than sizing to caption content. */
.ds-247420 .ds-lightbox-card--carousel .ds-carousel-item { flex: 0 0 100%; width: 100%; }
.ds-247420 .ds-lightbox-card--carousel .ds-carousel-track { scroll-snap-type: x mandatory; }

/* --- search ------------------------------------------------- */
.ds-247420 .ds-topbar-search { width: min(280px, 100%); min-width: 0; flex: 1 1 clamp(140px, 30vw, 280px); }
@media (max-width: 480px) {
  .ds-247420 .ds-topbar-search { flex-basis: 100%; }
}
/* Namespaced .ds-kit-empty-state (not .ds-empty-state) -- .ds-empty-state is
   primitives.css's shared base (padding var(--space-6), color var(--fg-3));
   this sheet loads after it in app-shell.css's import order, so reusing the
   same class name silently overrode that base's padding/color for every
   consumer, defeating the "shared base, one place" contract primitives.css
   documents. This block is unused by any component today, but kept
   namespaced so a future consumer gets this sheet's fluid clamp() treatment
   deliberately rather than by cascade accident. */
.ds-247420 .ds-kit-empty-state { padding: clamp(var(--space-3), 5vw, var(--space-4)); text-align: center; color: var(--panel-text-3); }
/* Fluid ICON size (empty-state mark), not a type tier: it scales with the
   viewport between 24 and 32px. Every fluid --fs-* tier is a heading clamp
   tuned to cqi + heading weight, so none of them substitutes here. */
.ds-247420 .ds-kit-empty-state-glyph { font-size: clamp(24px, 6vw, 32px); }
.ds-247420 .ds-kit-empty-state-msg { margin: var(--space-1-75) 0; }
.ds-247420 .ds-kit-empty-state-hint { margin: 0; font-size: var(--fs-sm); }

/* --- system primer ------------------------------------------ */
/* The three primer panels below share one fluid inset,
   clamp(10px, 2.5vw, 14px) clamp(10px, 3vw, 18px), and a 14/18px gap. None of
   10/14/18 is a --space-* tier (the scale jumps 8 -> 10 -> 12 -> 16 -> 20) and
   the three values are tuned as a set against the 64px label columns, so the
   panels stay optically identical to each other. Snapping any one of them
   desyncs the family; they are deliberate literals, not un-migrated drift. */
.ds-247420 .ds-swatch-grid-sm {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(min(110px, 100%), 1fr));
  gap: var(--space-2-75); padding: clamp(10px, 2.5vw, 14px) clamp(10px, 3vw, 18px);
}
.ds-247420 .ds-swatch-grid-lg { grid-template-columns: repeat(auto-fill, minmax(min(150px, 100%), 1fr)); }
.ds-247420 .ds-swatch-col { display: flex; flex-direction: column; gap: var(--space-1-75); }
.ds-247420 .ds-swatch-chip { height: 48px; border-radius: var(--r-1); border: 1px solid var(--rule); background: var(--swatch, var(--panel-1)); }
.ds-247420 .ds-swatch-chip--big { height: 64px; }
.ds-247420 .ds-swatch-name { font-family: var(--ff-mono); font-size: var(--fs-nano); color: var(--fg-3); }
.ds-247420 .ds-type-panel { padding: clamp(10px, 2.5vw, 14px) clamp(10px, 3vw, 18px); display: flex; flex-direction: column; gap: 14px; }
.ds-247420 .ds-type-row {
  display: flex; align-items: baseline; gap: 14px;
  border-bottom: 1px solid var(--rule); padding-bottom: var(--space-2); flex-wrap: wrap;
}
.ds-247420 .ds-type-row-label { flex: 0 0 64px; font-family: var(--ff-mono); font-size: var(--fs-nano); color: var(--fg-3); }
.ds-247420 .ds-type-sample {
  min-width: 0; overflow-wrap: anywhere; line-height: var(--lh-tight); color: var(--fg);
  font-size: var(--sample-size, var(--fs-body));
}
.ds-247420 .ds-prim-panel { padding: clamp(10px, 2.5vw, 14px) clamp(10px, 3vw, 18px); display: flex; flex-direction: column; gap: 18px; }
.ds-247420 .ds-prim-row { display: flex; gap: var(--space-2-5); flex-wrap: wrap; align-items: center; }
/* collab overlay specimens (LiveCursorOverlay/RemoteSelectionRings/
   RecentEditHighlightFlash) are `position:absolute; inset:0`, meant to
   overlay their own demo row -- without a positioned ancestor closer than
   the ancestor `.panel` (panel-row.css), they escape to cover the whole
   panel including its title. Scope them to this row. */
.ds-247420 .ds-prim-row-collab { position: relative; min-height: 64px; }
.ds-247420 .ds-prim-label { flex: 0 0 64px; font-family: var(--ff-mono); font-size: var(--fs-nano); color: var(--fg-3); }
/* Trailing status readouts (e.g. "none pressed yet") share .ds-prim-label's
   type/color but are free-length prose, not the fixed-width leading row
   label -- the shared 64px flex-basis wrapped them into a cramped 3-line
   stack. Give status text room to sit on one line instead. */
.ds-247420 .ds-prim-label-status { flex: 0 1 auto; white-space: nowrap; }

/* == appended: content-site == */
/* Home landing wrappers (site/theme.mjs) — replace banned inline styles. */
.ds-247420 .ds-home-hero-wrap { padding: 0 var(--space-2, 8px); }
.ds-247420 .ds-home-panel { margin: var(--space-2, 8px); }
@media (max-width: 480px) {
  .ds-247420 .ds-home-panel { margin: var(--space-1, 4px) 0; }
}
/* Home landing section tablist (site/theme.mjs Tabs()) — bare .tabs had no
   rule anywhere, so the four links rendered as unstyled inline text jammed
   together with no separation. Mirror the topbar nav pill-tab treatment. */
.ds-247420 .tabs[role="tablist"] {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1, 4px);
  font-size: var(--fs-sm);
}
.ds-247420 .tabs[role="tablist"] a {
  display: flex;
  align-items: center;
  gap: var(--space-1, 4px);
  padding: var(--space-2, 8px) var(--space-3, 12px);
  border-radius: var(--r-pill);
  color: var(--fg-2);
  transition: background var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease),
    transform var(--dur-base) var(--ease-spring);
}
.ds-247420 .tabs[role="tablist"] a:hover { background: var(--bg-2); color: var(--fg); transform: translateY(-1px); }
.ds-247420 .tabs[role="tablist"] a.active {
  color: var(--accent-ink);
  background: color-mix(in oklab, var(--accent) 16%, transparent);
  font-weight: 600;
}
.ds-247420 .ds-quickstart {
  padding: var(--space-3, 16px) var(--space-4, 24px);
  display: flex;
  flex-direction: column;
  gap: var(--space-1-75, 6px);
}
/* Table() scroll containment — wide tables scroll inside their own box,
   never the page body. overflow-x:auto alone works but gives no visible cue
   that a table is wider than its box (macOS overlay scrollbars stay hidden
   until an active hover/scroll gesture) -- columns beyond the fold read as
   simply missing rather than reachable. An always-visible thin scrollbar
   track makes the overflow legible without a gesture first. */
.ds-247420 .ds-table-wrap {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  max-width: 100%;
  scrollbar-width: thin;
  scrollbar-color: var(--rule-strong) transparent;
}
.ds-247420 .ds-table-wrap::-webkit-scrollbar { height: 8px; }
.ds-247420 .ds-table-wrap::-webkit-scrollbar-thumb { background: var(--rule-strong); border-radius: var(--r-pill, 999px); }
.ds-247420 .ds-table-wrap::-webkit-scrollbar-track { background: transparent; }
.ds-247420 .ds-table-wrap > table { min-width: 100%; }
/* Sortable column headers (Table's opt-in sortable prop) — a docstudio-cue
   addition: color stays neutral at rest (this is a structural control, not a
   brand moment) and only the active sort direction gets the accent tint,
   matching the "color reserved for meaning, not decoration" pattern seen in
   docstudio's admin table. */
.ds-247420 .ds-table-sort-btn {
  display: inline-flex; align-items: center; gap: var(--space-1);
  background: none; border: none; padding: 0; margin: 0;
  font: inherit; font-weight: inherit; color: inherit; cursor: pointer;
}
.ds-247420 .ds-table-sort-btn:hover { color: var(--fg); }
.ds-247420 .ds-table-sort-btn:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: 2px; }
.ds-247420 .ds-table-sort-btn.is-active { color: var(--accent-ink, var(--accent)); }


/* == appended: settings kit cleanup == */
/* == appended: settings kit == */
/* .app-main already owns this region's padding; this only supplies the inset
   that keeps panel shadows off the scroll edge (see .ds-section-pad). */
.ds-247420 .ds-settings-main { padding: calc(var(--space-1) * var(--density)); }
/* .panel-body already pads the panel interior — this sat inside it and
   double-padded every field, on an off-scale 14/18 pair. The body is now a
   flow container that owns only the RHYTHM between fields, not a second inset. */
.ds-247420 .ds-settings-body { padding: 0; }
.ds-247420 .ds-settings-body-stack { display: flex; flex-direction: column; gap: calc(var(--space-2-5) * var(--density)); }
/* Field rhythm: the label/control/hint cluster is tight internally, so fields
   need a clearly larger interval between them than within them, or the whole
   form reads as one undifferentiated column. Margin-collapse-free (flow root
   siblings) — first/last are zeroed so the panel body sets the outer edge. */
.ds-247420 .ds-field-block, .ds-247420 .ds-field-wrap { display: flex; flex-direction: column; gap: var(--space-1-75); margin: calc(var(--space-4) * var(--density)) 0 0; }
.ds-247420 .ds-settings-body > .ds-field-block:first-child, .ds-247420 .ds-settings-body > .ds-field-wrap:first-child { margin-top: 0; }
/* Field width is a hint about expected input length. A name or handle stretched
   across a 1000px desktop column tells the user to type a paragraph and leaves
   the eye no left-to-right anchor; capping the control (not the row) keeps the
   label/hint full-width for wrapping while the input reads as the right size.
   Textareas keep a wider cap since prose genuinely uses the room. */
.ds-247420 .ds-field-block > .input { max-width: 44ch; width: 100%; }
.ds-247420 .ds-field-block > textarea.input { max-width: 72ch; }
/* .ds-field-eyebrow joins the kit small-label voice above with no overrides. */
.ds-247420 .ds-hint-sm { font-size: var(--fs-xs); color: var(--panel-text-2); }
.ds-247420 .ds-note-quiet { margin: 0; color: var(--panel-text-2); }
.ds-247420 .ds-btn-row { display: flex; gap: var(--space-2, 8px); flex-wrap: wrap; align-items: center; }
.ds-247420 .ds-btn-row-tight { gap: var(--space-1-75, 6px); }
/* Size-ladder demo group: each size's caption+button pair stacks and the
   group aligns at the BOTTOM so buttons of different heights share one
   visual baseline instead of centering on their own mismatched midpoints. */
.ds-247420 .ds-btn-size-row { display: flex; flex-wrap: wrap; gap: var(--space-4); align-items: flex-end; }
.ds-247420 .ds-btn-size-col { display: flex; flex-direction: column; gap: var(--space-1-75); align-items: flex-start; }
.ds-247420 .ds-key-input { flex: 1 1 160px; min-width: 0; font-family: var(--ff-mono); }
.ds-247420 .ds-toggle-btn { min-width: 78px; }
/* Side-by-side checked/unchecked reference pair (settings kit theme panel) --
   documents the visual difference between states in one static, non-live
   spot rather than requiring a viewer to toggle a real control back and
   forth to compare. */
.ds-247420 .ds-toggle-state-sample { display: flex; align-items: center; gap: var(--space-1-5, 6px); }
.ds-247420 .ds-toggle-label { margin-left: var(--space-2, 8px); color: inherit; opacity: 0.7; }
.ds-247420 .ds-btn-warn { color: var(--warn); }
/* Raw --mascot text measures 3.11:1 on paper (fails 4.5:1); --mascot-deep is
   the text-safe tone, same accent/accent-ink split used everywhere else. */
.ds-247420 .ds-btn-mascot { color: var(--mascot-deep); }
/* Sticky commit bar. It floats OVER the form it commits, so it needs real
   elevation and a larger top margin than the form's own field rhythm —
   otherwise it reads as one more field rather than as the action for all of
   them. Spacing is density-scaled like the rest of the surface. */
.ds-247420 .ds-savebar {
  position: sticky; bottom: var(--space-2); z-index: var(--z-sticky);
  display: flex; align-items: center;
  justify-content: flex-end; gap: var(--space-2); flex-wrap: wrap;
  padding: calc(var(--space-2-75) * var(--density)) calc(var(--space-3) * var(--density));
  background: var(--panel-2); border-radius: var(--r-1);
  /* --panel-1 (this bar's parent .panel surface) and --panel-2 are adjacent
     lightness steps that measure only ~1.1-1.5:1 apart (both themes) -- too
     close to read as a distinct nesting depth by background tone alone.
     A hairline border is the same edge-definition device .panel itself
     already uses (see the top of this file) rather than a token-lightness
     change, which would risk the fg-3-on-panel-2 text contrast that is
     already right at its AA floor in the dark theme. */
  border: var(--bw-hair) solid var(--rule);
  margin: calc(var(--space-5) * var(--density)) 0 var(--space-2);
  box-shadow: var(--shadow-2, var(--shadow-1));
}
.ds-247420 .ds-savebar-note { flex: 1 1 160px; min-width: 0; color: var(--panel-text-2); }
.ds-247420 .ds-modal-body-form { padding: var(--space-3); display: flex; flex-direction: column; gap: var(--space-2-75); }
.ds-247420 .ds-modal-note { margin: 0; color: var(--panel-text-2); font-size: var(--fs-xs); }
.ds-247420 .ds-draft-preview {
  background: var(--panel-1); padding: var(--space-2-75); border-radius: var(--r-0);
  font-size: var(--fs-micro); color: var(--panel-text-2); max-height: 120px;
  overflow-y: auto; font-family: var(--ff-mono); overflow-wrap: anywhere;
}
/* Destructive-choice actions get a clearly larger interval than the body's own
   gap — the decision must not read as the next line of the explanation. */
.ds-247420 .ds-modal-actions { display: flex; gap: var(--space-2); margin-top: var(--space-3); justify-content: space-between; flex-wrap: wrap; }

/* == appended: signin kit cleanup == */
/* -- signin kit (ui_kits/signin) -- */
/* Centres one short card in the pane. `display:flex` is repeated after
   .ds-app-surface's `flow-root` because this class is used TOGETHER with that
   root and must win the centring; `min-height:100%` lets justify-content
   actually have room to centre against. No block margin: an auth screen is one
   card, not a run of editorial sections, and inheriting .ds-section's 96px
   rhythm is what pushed a 430px card to a 946px scroll height inside a ~514px
   pane and gave .app-main its own scrollbar at every real window height. */
.ds-247420 .ds-auth-wrap { padding: var(--space-2, 8px); display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100%; margin-block: 0; }
/* `auto` block margin, not a fixed clamp: the wrap centres the column, and a
   fixed top/bottom margin both fights that centring and re-adds height the pane
   then has to scroll. `auto` collapses when the pane is short, centres when tall. */
.ds-247420 .ds-auth-col { width: 100%; max-width: 440px; display: flex; flex-direction: column; gap: var(--space-2); margin: auto 0; min-width: 0; }
/* 18px ceiling: no tier between --space-3 (16) and --space-3-5 (20). */
.ds-247420 .ds-auth-form { display: flex; flex-direction: column; gap: var(--space-2-5); }
.ds-247420 .ds-auth-sent { align-items: center; text-align: center; }
/* 32px is an ICON size, not a type size: this is the confirmation mark above
   the "check your mail" copy, sized to the 440px auth column, and the --fs-*
   scale tops out at --fs-xl (21px) before jumping to fluid clamp() heading
   tiers. Snapping it to either would visibly resize the mark. */
.ds-247420 .ds-auth-sent-glyph { font-size: 32px; color: var(--panel-accent); }
.ds-247420 .ds-auth-sent-title { margin: 0; font-weight: 600; }
.ds-247420 .ds-auth-sent-sub { margin: 0; color: var(--panel-text-2); overflow-wrap: anywhere; }
.ds-247420 .ds-auth-otp-wrap { display: flex; flex-direction: column; align-items: center; gap: var(--space-2); }
.ds-247420 .ds-auth-field { display: flex; flex-direction: column; gap: var(--space-1); }
/* .ds-auth-field-label joins the kit small-label voice above, no overrides. */
.ds-247420 .ds-auth-field-input-row { position: relative; display: flex; align-items: center; }
/* Reserves room for the absolutely-positioned toggle: its --space-1 inset plus
   the --ds-icon-btn-sm 26px box, so the input text never sits under it. This
   only holds at the pointer:fine 26px box size, though -- the coarse-pointer
   touch-floor rule (states-interactions.css) grows .ds-icon-btn-sm to a
   min-width/height of 44px without this padding following it, so on a
   touchscreen the enlarged hit target actually overlapped 14px into the
   typed password text (44 + 4 inset > 34 reserved). Reserve for the coarse
   floor too, gated behind the same media query, so clearance holds at both
   sizes instead of only the desktop one. */
.ds-247420 .ds-auth-field-input-row .input { flex: 1 1 auto; padding-right: calc(26px + var(--space-2)); }
@media (pointer: coarse) {
  .ds-247420 .ds-auth-field-input-row .input { padding-right: calc(44px + var(--space-2)); }
}
.ds-247420 .ds-auth-pw-toggle { position: absolute; right: var(--space-1); }
.ds-247420 .ds-auth-row-between { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); flex-wrap: wrap; }
.ds-247420 .ds-auth-remember { display: flex; align-items: center; gap: var(--space-1-75); cursor: pointer; }
/* The native checkbox is already re-tinted site-wide via `accent-color:
   var(--accent-ink)` in states-interactions.css (that rule's own comment
   names this exact control) -- reused here rather than hand-drawing a new
   checkbox component. What that global rule doesn't cover is size: the UA
   default box renders at ~13px, which is under the 24px comfortable-tap
   floor other controls in this row (the link text, the input fields) sit
   above. Sized up on the existing --space-2-5 token, no new primitive. */
.ds-247420 .ds-auth-remember input[type="checkbox"] { width: var(--space-2-5); height: var(--space-2-5); margin: 0; }
.ds-247420 .ds-auth-remember-text { color: var(--panel-text-2); font-size: var(--fs-tiny); }
/* --accent-ink, not --accent, per the design system's text-vs-fill split
   (AGENTS.md): --accent is a background/border-color lead only, --accent-ink
   is the tone actually verified for text contrast. The link was previously
   unstyled prose color at --fs-tiny, reading as inert grey next to the
   checkbox label it shares a row with. */
.ds-247420 .ds-auth-forgot { font-size: var(--fs-tiny); color: var(--accent-ink); font-weight: 500; }
.ds-247420 .ds-auth-forgot:hover { text-decoration: underline; }
/* Error and status share layout but not color: a color-only distinction
   between role="alert" (something is wrong) and role="status" (informational)
   fails WCAG 1.4.1, so each also carries a distinct ASCII marker via ::before —
   never rely on the reader noticing the role attribute alone. */
.ds-247420 .ds-auth-error, .ds-247420 .ds-auth-status { padding: var(--space-2) var(--space-2-5); background: var(--panel-1); border-radius: var(--r-0); font-size: var(--fs-tiny); overflow-wrap: anywhere; display: flex; gap: var(--space-1-75); align-items: flex-start; }
.ds-247420 .ds-auth-error { color: var(--danger); }
.ds-247420 .ds-auth-error::before { content: '[!]'; flex: none; }
.ds-247420 .ds-auth-status { color: var(--panel-text-2); }
.ds-247420 .ds-auth-status::before { content: '[i]'; flex: none; }
/* The shared Divider() label defaults to --fs-tiny/--panel-text-3 (editor-
   primitives.css), tuned for a dense editor toolbar rule -- inside the auth
   form it sat at the same weight as fine print two rows below it and read
   as barely-there rather than a section break between the password form and
   the OAuth row. Scoped to this form only (never edit the shared component
   sheet for one consumer): borrows the kit small-label voice above (mono,
   --fs-nano, --tr-label, uppercase) at --panel-text-2 instead of the
   dimmer -3 tier, plus the ::before/::after rule lines pushed to full
   --panel-text opacity so the break itself, not just the label, is visible. */
.ds-247420 .ds-auth-form .ds-ep-divider-labeled { margin: var(--space-1) 0; }
.ds-247420 .ds-auth-form .ds-ep-divider-labeled::before,
.ds-247420 .ds-auth-form .ds-ep-divider-labeled::after {
  border-top-color: var(--panel-text-3);
}
.ds-247420 .ds-auth-form .ds-ep-divider-label {
  font-family: var(--ff-mono);
  font-size: var(--fs-nano);
  font-weight: 600;
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--panel-text-2);
}
.ds-247420 .ds-auth-providers { display: flex; gap: var(--space-2); flex-wrap: wrap; }
.ds-247420 .ds-auth-provider-btn { flex: 1 1 100px; min-width: 0; display: flex; align-items: center; justify-content: center; gap: var(--space-2); padding: var(--space-2-5); }
.ds-247420 .ds-auth-provider-btn--loading { opacity: 0.7; pointer-events: none; }
.ds-247420 .ds-auth-provider-glyph { font-family: var(--ff-mono); color: var(--panel-text-3); display: inline-flex; }
.ds-247420 .ds-auth-modes { display: flex; justify-content: center; gap: 14px; /* off-scale, matches the auth form inset */ margin-top: var(--space-2); font-size: var(--fs-tiny); flex-wrap: wrap; }
.ds-247420 .ds-auth-mode-link { color: var(--panel-text-3); text-decoration: none; }
.ds-247420 .ds-auth-mode-link--active { color: var(--panel-text); text-decoration: underline; }
.ds-247420 .ds-auth-fineprint { text-align: center; font-size: var(--fs-micro); color: var(--panel-text-3); margin: var(--space-1-75) 0; }

/* == appended: gallery-misc kit cleanup == */
/* --- shared helpers (converted from inline styles) --- */
.ds-247420 .ds-m0 { margin: 0; }
.ds-247420 .ds-text-2 { color: var(--panel-text-2); }
.ds-247420 .ds-text-3 { color: var(--panel-text-3); }
.ds-247420 .ds-kit-head { display: flex; align-items: center; justify-content: space-between; gap: clamp(var(--space-2), 2vw, var(--space-3)); flex-wrap: wrap; }

/* --- gallery kit --- */
.ds-247420 .ds-tile-cap { flex: 1; display: flex; align-items: center; justify-content: center; font-family: var(--ff-mono); white-space: pre-line; color: var(--panel-text-2); font-size: var(--fs-lg); min-width: 0; text-align: center; }
.ds-247420 .ds-tile-meta { display: flex; align-items: center; justify-content: space-between; gap: var(--space-1-75); font-size: var(--fs-micro); flex-wrap: wrap; min-width: 0; }
.ds-247420 .ds-tile-glyph { font-family: var(--ff-mono); color: var(--panel-text-3); }
.ds-247420 .ds-tile-label { color: var(--panel-text); overflow-wrap: anywhere; }
.ds-247420 .ds-gal-swatch { height: 64px; border-radius: var(--r-0); background: var(--swatch, var(--panel-1)); }
.ds-247420 .ds-gal-swatch-meta { display: flex; justify-content: space-between; gap: var(--space-1-75); flex-wrap: wrap; font-family: var(--ff-mono); font-size: var(--fs-nano); min-width: 0; }
.ds-247420 .ds-gal-swatch-name { color: var(--panel-text); }
.ds-247420 .ds-gal-swatch-hint { color: var(--panel-text-3); overflow-wrap: anywhere; }
.ds-247420 .ds-lightbox-head { display: flex; justify-content: space-between; align-items: center; gap: var(--space-2); flex-wrap: wrap; margin-bottom: var(--space-2-75); }
/* .ds-lightbox-tag joins the kit small-label voice above, no overrides. */
/* Dialog's .ds-ep-dialog-body has no default child gap (unlike the former
   hand-rolled .ds-lightbox-card, which was `display:flex; gap:14px`), so the
   carousel block gets its own top gutter to keep head/carousel/hint spaced. */
.ds-247420 .ds-lightbox-card--carousel { margin-bottom: var(--space-2-75); }

/* --- error_404 kit --- */
.ds-247420 .ds-err-actions { display: flex; gap: var(--space-2); flex-wrap: wrap; justify-content: center; margin-top: var(--space-1-75); }
.ds-247420 .ds-err-chips { display: flex; gap: var(--space-1-75); flex-wrap: wrap; justify-content: center; margin-top: var(--space-1); }

/* == appended: terminal-capp kit cleanup == */
/* --- terminal kit ------------------------------------------ */
.ds-247420 .ds-section-pad-sm { padding: var(--space-2); }
.ds-247420 .ds-kit-head-row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); flex-wrap: wrap; }
/* `.cli` (hero-content.css) is an ALWAYS-INK card: it sets a var(--ink)
   background with var(--paper) text in every theme, by design. So its text
   tones must be ink-surface tones, NOT the theme-relative fg / accent-ink /
   mascot-deep ramps. Those resolve to the PAPER tier under a light theme and
   paint dark text on this permanently dark card: --fg-3 lands at 2.48:1,
   --fg-2 at 1.18:1, --mascot-deep at 3.13:1 — all failing AA, and all
   invisible on a machine whose browser defaults to a dark color-scheme.
   The tones below are theme-independent and measured on --ink:
   --paper-3-dark 9.35:1, --paper-2 13.26:1, --acid 15.88:1, and the dark
   mascot tone 6.67:1. Same FILL-vs-TEXT discipline --accent/--accent-ink
   draws, applied to a surface that never follows the theme. */
.ds-247420 .ds-cli-cmt .cmd { color: var(--paper-3-dark); }
.ds-247420 .ds-cli-out .cmd { color: var(--paper-2); }
.ds-247420 .ds-cli-ok .prompt, .ds-247420 .ds-cli-ok .cmd { color: var(--success-on-ink); } /* WCAG-verified 2026 restyle: --acid alone is now a dark neutral and fails on this always-dark .cli surface (npm run a11y) */
.ds-247420 .ds-cli-warn .prompt, .ds-247420 .ds-cli-warn .cmd { color: var(--mascot-on-ink); }
.ds-247420 .ds-cli-log .prompt { color: var(--paper-3-dark); }
.ds-247420 .ds-cli-log .cmd { color: var(--paper-2); font-family: var(--ff-mono); }
/* The terminal surface is theme-INDEPENDENT ink, like .cli itself: a shell
   that turns white in light mode does not read as a terminal, and the
   .ds-cli-* line tones above are all measured on --ink, so a light surface
   would also break their contrast. */
.ds-247420 .ds-term-body {
  padding: var(--space-3) var(--space-3-5);
  display: flex; flex-direction: column; gap: 0;
  background: var(--ink); border-radius: var(--r-1);
  font-family: var(--ff-mono); font-size: var(--fs-micro); line-height: 1.65;
  /* Responsive strategy for long logs: cap the scrollback height and scroll
     vertically rather than growing the page indefinitely; long command/output
     lines wrap (see .cli .cmd's overflow-wrap: anywhere below) instead of
     scrolling horizontally, so wrapping is the chosen default over
     horizontal scroll for this kit. */
  max-height: 60vh;
  overflow-y: auto;
  overflow-wrap: anywhere;
}
/* .cli is authored in hero-content.css as a STANDALONE hero code block --
   20px/24px padding, --fs-lg type, --r-3 radius and its own --ink background
   per row. Stacked as scrollback those become a column of fat rounded cards
   instead of terminal lines, which is why this kit did not read as a
   terminal. Inside the terminal body a .cli is a LINE: no box of its own,
   no radius, no padding, inheriting the body's mono scale. */
.ds-247420 .ds-term-body .cli {
  display: flex; align-items: flex-start; gap: var(--space-1-5);
  padding: 0; background: none; border-radius: 0;
  font-size: inherit; font-family: inherit; line-height: inherit;
  overflow-x: visible;
}
/* Prompt marks are a fixed gutter so every line's text starts on one column;
   ragged prompts are the tell of a fake terminal. ch units track the mono
   glyph width, so the gutter holds at any type size. */
.ds-247420 .ds-term-body .cli .prompt { flex: 0 0 auto; width: 1ch; text-align: center; opacity: 0.75; }
.ds-247420 .ds-term-body .cli .cmd { flex: 1; min-width: 0; white-space: pre-wrap; overflow-wrap: anywhere; overflow-x: visible; }
.ds-247420 .ds-term-body--tall { min-height: 280px; }
/* Per-row copy button on command lines only (kind==='cmd', see terminal
   app.js's Line()) -- hover/focus-reveal like the chat surface's
   .chat-code-copy so the scrollback stays a clean transcript at rest. */
.ds-247420 .ds-term-line-copyable { position: relative; padding-right: 40px; }
.ds-247420 .ds-term-line-copy {
  position: absolute; top: 0; right: 0;
  opacity: 0; pointer-events: none;
  background: none; border: 0; color: var(--paper-3-dark);
  font-family: var(--ff-mono); font-size: var(--fs-micro);
  cursor: pointer; padding: 0 var(--space-1);
  transition: opacity var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease);
}
.ds-247420 .ds-term-line-copyable:hover .ds-term-line-copy,
.ds-247420 .ds-term-line-copyable:focus-within .ds-term-line-copy {
  opacity: 1; pointer-events: auto;
}
.ds-247420 .ds-term-line-copy:hover { color: var(--paper); }
.ds-247420 .ds-term-line-copy.is-copied { color: var(--accent); opacity: 1; pointer-events: auto; }
@media (pointer: coarse) {
  /* No hover on touch -- always visible so the affordance is reachable. */
  .ds-247420 .ds-term-line-copy { opacity: 1; pointer-events: auto; }
}
.ds-247420 .ds-term-input-row { margin-top: var(--space-1-75); }
.ds-247420 .ds-term-input { flex: 1; min-width: 0; background: transparent; border: 0; outline: 0; font-family: var(--ff-mono); font-size: var(--fs-micro); color: var(--paper); caret-color: var(--acid); }
.ds-247420 .ds-term-input::placeholder { color: var(--paper-3-dark); }
/* Adding type="text" (for the a11y fix below) makes the global
   input[type="text"] rule in states-interactions.css match this element for
   the first time, at equal-or-higher specificity than this file's load
   order would otherwise win. Re-assert the borderless prompt-line chrome so
   the shared form-control skin (bg, border, radius, fixed height) can't leak
   onto what is a terminal prompt, not a form field. */
.ds-247420 .ds-term-input[type='text'] { background: transparent; border: 0; border-radius: 0; box-shadow: none; height: auto; }
/* The row-level `outline: 0` above kills the browser default, so the control
   needs its own :focus-visible ring back — otherwise a keyboard user gets no
   visual confirmation focus landed on the one control they type into. */
.ds-247420 .ds-term-input:focus-visible {
  outline: var(--focus-w) solid var(--focus-color);
  outline-offset: var(--focus-offset);
}
/* The input itself is a prompt line, not a form control, so it is NOT floored
   to 44px (that would break the terminal's line rhythm). Instead the whole
   prompt ROW becomes the tap target, and the font goes to 16px so focusing it
   does not trigger iOS zoom — a sub-16px input is what causes that. */
@media (pointer: coarse) {
  .ds-247420 .ds-term-input-row { min-height: 44px; align-items: center; }
  .ds-247420 .ds-term-input { font-size: max(16px, var(--fs-micro)); }
}
/* full-viewport render root (community-app) */
.ds-247420 .ds-root-viewport { height: 100vh; height: 100dvh; }

/* --- chat kit: widescreen detail pane -----------------------
   .chat itself stays single-column (shared component, untouched) and
   stretches full-width on ultrawide via the existing .app-main > .chat
   max-width:none rule above (~L380) — that's fine for a bare thread, but
   it leaves a lone thread column growing to 1400px+ with no other use of
   the extra width. This wrapper adds a persistent "thread info" rail once
   there is room to spare, instead of just widening the message column. */
/* min-height is a floor, not the intended size — .chat-kit-page's flex-column
   splits leftover height between this (flex:1 1 0%, "leftover") and the
   pattern-notes panel below it (flex:0 1 auto, i.e. sized to its own content
   first). A hard px floor here (this used to read max(30vh, 420px)) can
   exceed .chat-kit-page's own available height on a short/laptop viewport —
   420px plus the panel's natural content height overflows the column
   instead of yielding to it, so .chat-thread's internal overflow-y:auto
   never engages and the last couple of messages are stranded below the
   fold with no reachable scroll region (the thread is never empty, it is
   invisible). min-height:0 here — same as .chat/.chat-thread already use —
   lets flex:1 actually shrink this to whatever .chat-kit-page has left,
   so .chat-thread (flex:1, min-height:0, overflow-y:auto) is the thing that
   scrolls, not the page. It still grows past 0 via flex:1 whenever the
   column has room, and 30vh remains the width->grid breakpoint's intent via
   plain flex growth, not a floor that can outgrow its container. */
.ds-247420 .ds-chat-layout { display: flex; flex-direction: column; flex: 1; min-height: 0; gap: var(--space-3); }
.ds-247420 .ds-chat-layout > .chat { min-width: 0; }
.ds-247420 .ds-chat-detail { display: none; }
@media (min-width: 1100px) {
  /* Two compounding grid quirks, not one:
     1. align-items:start sizes the row to its tallest item's natural content
        height instead of stretching items to fill it — align-items must stay
        the grid default (stretch).
     2. Even with stretch, an implicit row with no explicit sizing (the
        default here) ALSO sizes itself to content first, then stretches
        items to that content-derived height — which can exceed the row's
        actual allotment from this grid's own flex parent (.chat-kit-page,
        flex:1/min-height:0). .chat then renders taller than .ds-chat-layout's
        own box, so it overflows past the grid container instead of being
        clipped by .chat-thread's internal scroll — visually burying the
        pattern-notes panel that follows this grid in the DOM. Explicit
        `grid-template-rows: minmax(0, 1fr)` makes the row take exactly the
        container's height (from the flex chain above) as the ceiling, so
        .chat's min-height:0 has a real, smaller-than-content value to clip
        against. .ds-chat-detail opts out via align-self so its own sticky
        positioning still works without forcing the row back to content-sized. */
  .ds-247420 .ds-chat-layout { display: grid; grid-template-columns: minmax(0, 1fr) 280px; grid-template-rows: minmax(0, 1fr); gap: var(--space-4); }
  .ds-247420 .ds-chat-detail { display: block; position: sticky; top: 0; align-self: start; }
}

/* Avatar — generic identity disc primitive (content.js Avatar). Sized
   variants mirror the existing chat-avatar/community-avatar footprints so
   any consumer gets a consistent disc without duplicating the CSS. */
.ds-247420 .ds-avatar {
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 50%; background: var(--bg-3); color: var(--fg);
  font-family: var(--ff-body); font-weight: 600; text-transform: lowercase;
  letter-spacing: 0.02em; user-select: none; flex-shrink: 0; object-fit: cover;
  box-shadow: inset 0 0 0 1px var(--rule);
}
.ds-247420 .ds-avatar-sm { width: 24px; height: 24px; font-size: var(--fs-pico); }
.ds-247420 .ds-avatar-md { width: 36px; height: 36px; font-size: var(--fs-micro); }
.ds-247420 .ds-avatar-lg { width: 48px; height: 48px; font-size: var(--fs-sm); }
.ds-247420 .ds-avatar-square { border-radius: var(--r-2, 6px); }

/* SpreadsheetPreview — tabbed inline spreadsheet/CSV viewer (docstudio
   xls-preview.js port). Tabs reuse the same neutral-at-rest pattern as
   Table's sortable headers; the truncation banner is persistent and never
   hides the table underneath. */
.ds-247420 .ds-sheet-preview { display: flex; flex-direction: column; gap: var(--space-2, 8px); min-width: 0; }
.ds-247420 .ds-sheet-preview-tabbar {
  display: flex; gap: var(--space-1, 4px); overflow-x: auto; -webkit-overflow-scrolling: touch;
  border-bottom: 1px solid var(--border, var(--bg-3));
}
.ds-247420 .ds-sheet-preview-tab {
  flex-shrink: 0; background: none; border: none; cursor: pointer;
  padding: var(--space-1-5, 5px) var(--space-2-5, 10px);
  font: inherit; color: var(--fg-3);
}
.ds-247420 .ds-sheet-preview-tab:hover { color: var(--fg); }
.ds-247420 .ds-sheet-preview-tab:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: -2px; }
.ds-247420 .ds-sheet-preview-tab.is-active { color: var(--fg); font-weight: 600; background: var(--bg-2); border-radius: var(--r-1, 6px); }
.ds-247420 .ds-sheet-preview-truncated {
  font-size: var(--fs-tiny, 12px); color: var(--fg-3);
  padding: var(--space-1, 4px) var(--space-2, 8px);
  background: var(--bg-2); border-radius: var(--r-1, 6px);
}
.ds-247420 .ds-sheet-preview-body { overflow: auto; -webkit-overflow-scrolling: touch; max-width: 100%; }
.ds-247420 .ds-sheet-preview-table { min-width: 100%; border-collapse: collapse; font-size: var(--fs-sm, 13px); }
.ds-247420 .ds-sheet-preview-table th, .ds-247420 .ds-sheet-preview-table td {
  padding: var(--space-1-5, 5px) var(--space-2, 8px);
  border-bottom: 1px solid var(--border, var(--bg-3));
  text-align: left; white-space: nowrap;
}
.ds-247420 .ds-sheet-preview-table thead th {
  position: sticky; top: 0; background: var(--bg-2); color: var(--fg-3);
  font-weight: 600; z-index: var(--z-sticky);
}
.ds-247420 .ds-sheet-preview-table td.num { text-align: right; font-variant-numeric: tabular-nums; }
.ds-247420 .ds-sheet-preview-empty { padding: var(--space-3, 16px); color: var(--fg-3); }
.ds-247420 .ds-sheet-preview-error-action {
  align-self: flex-start; background: none; border: 1px solid var(--border, var(--bg-3));
  border-radius: var(--r-1, 6px); padding: var(--space-1, 4px) var(--space-2-5, 10px);
  font: inherit; color: var(--fg); cursor: pointer;
}
.ds-247420 .ds-sheet-preview-error-action:hover { background: var(--bg-2); }
.ds-247420 .ds-sheet-preview-error-action:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: 2px; }

/* == appended: community kit message list == */
/* ui_kits/community mounts CommunityShell (the .cm-* family) but renders its
   OWN message list in ds-* markup, and its index.html links app-shell.css
   WITHOUT community.css. So these live here, not in community.css: the .cm-*
   sheet never reaches this page, and .ds-community-* is ds-family markup by
   the lint-classes taxonomy anyway. Every rule below deliberately mirrors the
   .chat-msg / .chat-avatar / .chat-stack primitives in chat-polish.css so the
   community thread and the chat kit read as one message language. */

/* Page root — owns the full viewport column so CommunityShell's own 100dvh
   flex shell has a height to resolve against, with the topbar/status chrome
   stacked around it. */
.ds-247420 .ds-community-page {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100dvh;
  min-width: 0;
}
/* Main column inside the shell — .cm-main's ds-family counterpart: header
   pinned, message list taking the remaining height and doing the scrolling. */
.ds-247420 .ds-community-main {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-width: 0;
  min-height: 0;
}

/* Thread scroller. Same rhythm as .chat-thread (column + --space-3 gap +
   own overflow), so messages breathe on the same interval as the chat kit. */
.ds-247420 .ds-community-messages {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: var(--space-3);
  scrollbar-width: thin;
  scrollbar-color: var(--bg-3) transparent;
}
.ds-247420 .ds-community-messages::-webkit-scrollbar { width: 8px; }
.ds-247420 .ds-community-messages::-webkit-scrollbar-thumb { background: var(--bg-3); border-radius: var(--r-0); }

/* One message — avatar rail + body stack, matching .chat-msg's flex/gap and
   its quiet tonal hover (no border, per the zero-border aesthetic). */
.ds-247420 .ds-community-msg {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2-75);
  padding: var(--space-1-75) var(--space-2);
  margin: 0;
  border-radius: var(--r-1);
  min-width: 0;
  transition: background var(--dur-base) var(--ease);
}
.ds-247420 .ds-community-msg:hover { background: color-mix(in oklab, var(--fg) 4%, transparent); }

/* Identity disc. The inline `background:` from app.js supplies the per-author
   tone; this owns the SHAPE — a circle, which is the whole reason the avatar
   was rendering as a coloured bar with no rule. Footprint + type match
   .chat-avatar exactly. */
.ds-247420 .ds-community-avatar {
  flex: 0 0 36px;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  color: var(--ink);
  font-family: var(--ff-mono);
  font-size: var(--fs-micro);
  font-weight: 600;
  line-height: 1;
  user-select: none;
  box-shadow: inset 0 0 0 1px var(--rule);
}

/* Body stack — name + time on one baseline, text beneath. .chat-stack's
   column/gap idiom, without the bubble max-width (this is a flat list row,
   not a two-sided bubble thread). */
.ds-247420 .ds-community-msg-body {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-1) var(--space-2);
  flex: 1;
  min-width: 0;
}
/* Author. The inline `color:` from app.js carries the per-author tone; weight
   and size are what make it read as the row's title. */
.ds-247420 .ds-community-msg-name {
  font-weight: 600;
  font-size: var(--fs-sm);
  line-height: 1.3;
}
/* Timestamp — the quiet mono meta voice used by .cm-user-tag / row .meta. */
.ds-247420 .ds-community-msg-time {
  font-family: var(--ff-mono);
  font-size: var(--fs-micro);
  color: var(--fg-3);
  line-height: 1.3;
}
/* Message body claims the full second line of the wrapping body row. Capped
   to a readable measure -- unlike .chat-bubble this is a flat list row, not
   a two-sided bubble, but an uncapped prose line stretches the full pane
   width (~970px), which reads as dead paper beside every short message
   rather than an intentionally spacious flat list. */
.ds-247420 .ds-community-msg-text {
  flex: 1 0 100%;
  max-width: 70ch;
  margin: 0;
  min-width: 0;
  color: var(--fg-2);
  font-size: var(--fs-sm);
  line-height: var(--lh-base);
  overflow-wrap: anywhere;
}

@media (prefers-reduced-motion: reduce) {
  .ds-247420 .ds-community-msg { transition: none; }
}

/* == appended: cross-kit note / swatch / chat-page / caret == */

/* "about this kit" explanatory block — 8 kits render a run of <p> lines here
   (often carrying inline Chip()/<code>) inside a .panel body. This is
   SECONDARY copy annotating the demo above it, so it takes the quiet voice:
   one step down from body, on the --fg-2 ramp, with a comfortable reading
   line-height. It owns only the RHYTHM between its lines; the panel body still
   owns the inset, so this must not re-pad. */
.ds-247420 .ds-pattern-notes {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  color: var(--fg-2);
  font-size: var(--fs-sm);
  line-height: var(--lh-base);
  max-width: var(--measure);
  min-width: 0;
}
.ds-247420 .ds-pattern-notes p { margin: 0; overflow-wrap: anywhere; }
/* Inline literals inside a note read as quoted machinery, not as body copy —
   same tonal chip treatment .chat-bubble code uses. */
.ds-247420 .ds-pattern-notes code {
  font-family: var(--ff-mono);
  /* em-relative on purpose: inline code must track whatever size the
     surrounding prose is set at (the mono face runs optically large next to
     the system stack, hence 0.92). An absolute --fs-* tier would decouple it
     from its own paragraph and break at every containing type size. */
  font-size: 0.92em;
  background: color-mix(in oklab, var(--fg) 8%, transparent);
  padding: 0 var(--space-1);
  border-radius: var(--r-hair);
}
.ds-247420 .ds-pattern-notes strong { color: var(--fg); font-weight: 600; }

/* .panel.panel-docs — documentation/annotation chrome, visibly different from
   the plain .panel the real interface renders in. aicat stacks its "about
   this kit" caption directly under the live chat surface; identical card
   chrome on both made the meta-explanation read as part of the product
   instead of a caption about it. Dashed border reuses the .ds-dropzone
   affordance vocabulary (a dashed edge already reads as "not solid content"
   in this system) and the tint is a quiet, low-saturation wash rather than
   an accent color, so it never competes with real accent-toned UI state. */
.ds-247420 .panel.panel-docs {
  border-style: dashed;
  border-color: var(--rule-strong);
  background: color-mix(in oklab, var(--fg) 4%, var(--bg-2));
  box-shadow: none;
}
.ds-247420 .panel.panel-docs .panel-title {
  color: var(--fg-3);
  font-family: var(--ff-mono);
  font-size: var(--fs-xs);
  letter-spacing: 0;
}
.ds-247420 .panel.panel-docs .panel-title::before {
  content: 'docs · ';
  color: var(--fg-3);
}

/* .ds-kit-controls — collapsed-by-default dev/demo toggle drawer. Reference
   state-toggle pills (ready/loading/empty/error and similar) are scaffolding
   for exercising a kit's other states, not live content; sitting exposed in
   the flow next to real content made them read as leftover dev debris. A
   native <details>/<summary> disclosure needs no bespoke open/close state or
   JS and is keyboard/AT-operable for free — closed by default, opens on
   click or Enter/Space on the summary. */
.ds-247420 .ds-kit-controls {
  border: var(--bw-hair) dashed var(--rule);
  border-radius: var(--r-2);
  margin: 0 0 var(--space-4);
  background: var(--bg);
}
.ds-247420 .ds-kit-controls summary {
  cursor: pointer;
  padding: var(--space-2) var(--space-3);
  font-family: var(--ff-mono);
  font-size: var(--fs-xs);
  color: var(--fg-3);
  list-style: none;
}
.ds-247420 .ds-kit-controls summary::-webkit-details-marker { display: none; }
.ds-247420 .ds-kit-controls summary::before { content: '[+] '; }
.ds-247420 .ds-kit-controls[open] summary::before { content: '[-] '; }
.ds-247420 .ds-kit-controls summary:focus-visible {
  outline: var(--focus-w) solid var(--focus-color);
  outline-offset: var(--focus-offset);
}
.ds-247420 .ds-kit-controls-body {
  padding: 0 var(--space-3) var(--space-3);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* Swatch cell wrapper (system_primer pairs it with .ds-swatch-col, which owns
   the column/gap). This adds only what the wrapper itself needs: a minimum-zero
   track so long token names ellipsize inside the grid instead of widening it. */
.ds-247420 .ds-swatch { min-width: 0; }
.ds-247420 .ds-swatch .ds-swatch-name { overflow-wrap: anywhere; }

/* Chat kit page root — the thread must own the leftover column height rather
   than growing the page, so the composer stays on screen. Pairs with
   .ds-chat-layout (flex column, flex:1) which is already defined above. */
.ds-247420 .chat-kit-page {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
  min-width: 0;
}
/* .chat-kit-page's trailing "pattern notes" panel has no flex-basis of its
   own, so on a content-basis vs. .ds-chat-layout's flex:1 1 0% it can win
   the height contest either way depending on which sibling has more natural
   content -- flex-shrink:0 alone (an earlier version of this rule) fixed the
   composer-overlaps-panel regression but overcorrected: it let the panel
   ALWAYS claim its full natural height regardless of how tall it is, which
   on a viewport where the panel's content runs long (several paragraphs of
   pattern notes) starves the actual chat surface down to a sliver -- live-
   measured at 229px total for header+thread+composer, with .chat-thread
   clipped to ~55px, meaning real messages render but are pushed almost
   entirely out of the visible scroll window. The chat surface is the primary
   content; the notes panel is reference material and must never outrank it.
   Give .ds-chat-layout a real height floor so it always gets a usable pane
   even when the panel below it is long, and let the panel itself scroll its
   own overflow past that floor instead of pushing the chat surface down. */
.ds-247420 .chat-kit-page > .ds-chat-layout { flex: 1 1 auto; min-height: min(420px, 55vh); }
/* max-height/overflow sit on .panel-body, not the outer .panel, so the
   panel-head title stays pinned above the scroll rather than scrolling out
   of view with the notes. .panel-body itself needs tabindex+role for the
   same reason Table's scroll wrapper does (WCAG 2.1.1 / axe's
   scrollable-region-focusable) -- see ui_kits/chat/app.js's pattern-notes
   Panel(), which sets them directly since the shared Panel() component has
   no prop for it. */
.ds-247420 .chat-kit-page > .panel { flex: 0 1 auto; min-height: 0; display: flex; flex-direction: column; }
.ds-247420 .chat-kit-page > .panel > .panel-body { min-height: 0; max-height: 45vh; overflow-y: auto; }

/* Terminal caret — a solid block that sits on the last demo line. Sized in ch
   so it tracks the mono column it follows. The blink is motion, so it is
   declared ONLY inside prefers-reduced-motion: no-preference (the same
   opt-in shape .cm-ch-spinner / .cm-vs-label::before use); with reduced
   motion the caret is a steady block, still marking the cursor position. */
.ds-247420 .cursor-blink {
  display: inline-block;
  width: 1ch;
  height: 1em;
  margin-left: var(--space-1);
  background: var(--accent);
  vertical-align: text-bottom;
}
@media (prefers-reduced-motion: no-preference) {
  .ds-247420 .cursor-blink { animation: ds-cursor-blink 1s step-end infinite; }
  @keyframes ds-cursor-blink { 50% { opacity: 0; } }
}

/* == appended: panel width modifier == */
/* .panel.panel-inline-wide — a width-capped inline note panel. Companion to
   the .panel-inline / .panel-wide modifiers in panel-row.css: those two change
   the panel's SURFACE (tonal fill / chrome-stripped), this one changes only
   its MEASURE, so it composes with either. Introduced when ui_kits/docs's
   `[!] note` panel traded `style="max-width:640px"` for a class (the inline-
   style sweep) but never got a rule written, leaving that panel to run the
   full column width. 640px is the note measure, deliberately narrower than
   --measure-narrow (760px): the aside is meant to read as a set-aside block
   against the surrounding prose, not to match its column. It is a literal
   rather than a new token because it has exactly one consumer — promote it to
   --measure-note the moment a second surface wants the same cap.
   `margin-right: auto` (not `margin: 0 auto`) keeps the panel left-aligned to
   the prose it interrupts and preserves .panel's own --space-4 stack margin. */
.ds-247420 .panel.panel-inline-wide {
  max-width: 640px;
  margin-right: auto;
}
/* ============================================================
   Git status panel + diff view — changed-file list, unified diff.
   ============================================================ */

.ds-247420 .ds-git-status-grid {
  display: flex; flex-direction: column; gap: 2px;
}
.ds-247420 .ds-git-row {
  display: flex; align-items: center; gap: 10px;
  width: 100%; padding: 7px 12px;
  background: none; border: var(--bw-hair) solid transparent; border-radius: var(--r-2);
  color: var(--fg); font: inherit; text-align: left; cursor: pointer;
  transition: background var(--dur-snap) var(--ease), border-color var(--dur-snap) var(--ease);
}
.ds-247420 .ds-git-row:hover { background: var(--bg-2); border-color: var(--rule); }
.ds-247420 .ds-git-row.active { background: var(--accent-tint); border-color: var(--accent); }
.ds-247420 .ds-git-row:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
.ds-247420 .ds-git-row[disabled] { cursor: default; }

.ds-247420 .ds-git-status-chip {
  display: inline-flex; align-items: center; justify-content: center;
  flex: 0 0 auto; width: 18px; height: 18px;
  font-family: var(--ff-body); font-size: var(--fs-micro); font-weight: 700;
  border-radius: var(--r-1);
}
.ds-247420 .ds-git-status-chip.tone-add { color: var(--green-2); background: color-mix(in oklab, var(--green-2) 16%, transparent); }
.ds-247420 .ds-git-status-chip.tone-modify { color: var(--sun); background: color-mix(in oklab, var(--sun) 16%, transparent); }
.ds-247420 .ds-git-status-chip.tone-delete { color: var(--flame); background: color-mix(in oklab, var(--flame) 16%, transparent); }
.ds-247420 .ds-git-status-chip.tone-neutral { color: var(--fg-3); background: var(--bg-2); }

.ds-247420 .ds-git-row-icon { display: inline-flex; flex: 0 0 auto; color: var(--fg-3); }
.ds-247420 .ds-git-row-path {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-family: var(--ff-mono); font-size: var(--fs-sm);
}
.ds-247420 .ds-git-row-tag {
  flex: 0 0 auto; font-size: var(--fs-micro); color: var(--fg-3);
  border: var(--bw-hair) solid var(--rule); border-radius: var(--r-1); padding: 1px 6px;
}
.ds-247420 .ds-git-row-stats { flex: 0 0 auto; display: inline-flex; gap: 6px; font-family: var(--ff-body); font-size: var(--fs-micro); }
.ds-247420 .ds-git-stat-add { color: var(--green-2); }
.ds-247420 .ds-git-stat-del { color: var(--flame); }

.ds-247420 .ds-git-empty {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--space-2); padding: var(--space-6) var(--space-3);
  color: var(--fg-3); text-align: center;
}
.ds-247420 .ds-git-empty-glyph { opacity: 0.55; }

/* Diff view */
.ds-247420 .ds-git-diff {
  display: flex; flex-direction: column; gap: var(--space-2);
  background: var(--ink); color: var(--paper);
  border-radius: var(--r-2); overflow: hidden;
}
.ds-247420 .ds-git-diff-head {
  padding: var(--space-2) var(--space-3);
  border-bottom: var(--bw-hair) solid color-mix(in oklab, var(--paper) 18%, transparent);
  font-family: var(--ff-mono); font-size: var(--fs-xs);
}
.ds-247420 .ds-git-diff-empty { padding: var(--space-3); color: var(--fg-3); font-size: var(--fs-sm); }
.ds-247420 .ds-git-hunk + .ds-git-hunk { border-top: var(--bw-hair) solid color-mix(in oklab, var(--paper) 12%, transparent); }
.ds-247420 .ds-git-hunk-header {
  padding: 4px var(--space-3);
  color: var(--purple-2); font-family: var(--ff-mono); font-size: var(--fs-micro);
  background: color-mix(in oklab, var(--paper) 6%, transparent);
}
.ds-247420 .ds-git-hunk-body {
  margin: 0; padding: 0 0 var(--space-2);
  font-family: var(--ff-mono); font-size: var(--fs-xs); line-height: var(--lh-base);
  white-space: pre; overflow-x: auto; tab-size: 2;
}
.ds-247420 .ds-git-line {
  display: block; padding: 0 var(--space-3);
}
.ds-247420 .ds-git-line-add { background: color-mix(in oklab, var(--green-2) 16%, transparent); color: var(--paper); }
.ds-247420 .ds-git-line-del { background: color-mix(in oklab, var(--flame) 16%, transparent); color: var(--paper); }
.ds-247420 .ds-git-line-context { color: var(--paper-3); }

/* ============================================================
   WorktreeSwitcher — trigger + dropdown menu of worktrees/branches.
   ============================================================ */
.ds-247420 .ds-wts { display: inline-flex; }
.ds-247420 .ds-wts-trigger {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 5px 10px; min-height: 32px;
  background: var(--bg); border: var(--bw-hair) solid var(--rule); border-radius: var(--r-1);
  color: var(--fg); font: inherit; font-size: var(--fs-sm); cursor: pointer;
  max-width: 220px;
}
.ds-247420 .ds-wts-trigger:hover { background: var(--bg-2); }
.ds-247420 .ds-wts-trigger:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
.ds-247420 .ds-wts-trigger-branch {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-family: var(--ff-mono);
}
.ds-247420 .ds-wts-trigger-caret { display: inline-flex; flex: 0 0 auto; color: var(--fg-3); }

.ds-247420 .ds-wts-item-body { display: flex; align-items: center; gap: 8px; min-width: 0; }
.ds-247420 .ds-wts-item-check { display: inline-flex; flex: 0 0 16px; color: var(--accent-ink); }
.ds-247420 .ds-wts-item-text { display: flex; flex-direction: column; min-width: 0; gap: 1px; }
.ds-247420 .ds-wts-item-branch {
  font-family: var(--ff-mono); font-size: var(--fs-sm);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ds-247420 .ds-wts-item-path {
  font-size: var(--fs-micro); color: var(--fg-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ds-247420 .ds-dropdown-item[aria-disabled="true"] .ds-wts-item-branch { color: var(--fg); font-weight: 600; }
/* ============================================================
   PluginsConfig — plugin/extension list + detail modal.
   ============================================================ */

.ds-247420 .ds-plugins-overlay {
  position: fixed; inset: 0; z-index: var(--z-modal);
  background: color-mix(in oklab, var(--ink) 35%, transparent);
  display: flex; align-items: center; justify-content: center;
  padding: var(--space-3);
}
.ds-247420 .ds-plugins-modal {
  width: min(860px, 100%); height: min(76vh, 720px);
  background: var(--bg); border: var(--bw-hair) solid var(--rule); border-radius: var(--r-3);
  display: flex; flex-direction: column; overflow: hidden;
  box-shadow: var(--shadow-3, 0 8px 32px rgba(0,0,0,0.18));
}
.ds-247420 .ds-plugins-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--space-3) var(--space-4);
  border-bottom: var(--bw-hair) solid var(--rule); flex: 0 0 auto;
}
.ds-247420 .ds-plugins-title { font-size: var(--fs-md); font-weight: 600; color: var(--fg); }
.ds-247420 .ds-plugins-close {
  background: none; border: none; color: var(--fg-3); cursor: pointer;
  font-size: 20px; line-height: 1; padding: 2px 6px; border-radius: var(--r-1);
}
.ds-247420 .ds-plugins-close:hover { background: var(--bg-2); color: var(--fg); }
.ds-247420 .ds-plugins-close:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }

.ds-247420 .ds-plugins-body { flex: 1 1 auto; display: flex; overflow: hidden; }

.ds-247420 .ds-plugins-sidebar {
  width: 245px; flex: 0 0 auto; overflow-y: auto;
  border-right: var(--bw-hair) solid var(--rule); background: var(--bg-2);
  padding: var(--space-2);
}
.ds-247420 .ds-plugins-sidebar-status {
  padding: var(--space-3) var(--space-2); font-size: var(--fs-sm); color: var(--fg-3);
}
.ds-247420 .ds-plugins-sidebar-status.ds-plugins-status-error { color: var(--flame); }
.ds-247420 .ds-plugins-list { display: flex; flex-direction: column; gap: 2px; }

.ds-247420 .ds-plugins-row {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: var(--space-2); border-radius: var(--r-2);
  background: none; border: var(--bw-hair) solid transparent; text-align: left; cursor: pointer;
  color: var(--fg); font: inherit;
  transition: background var(--dur-snap) var(--ease), border-color var(--dur-snap) var(--ease);
}
.ds-247420 .ds-plugins-row:hover { background: var(--bg-3, var(--bg)); }
.ds-247420 .ds-plugins-row.active { background: var(--accent-tint); border-color: var(--accent); }
.ds-247420 .ds-plugins-row:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }

.ds-247420 .ds-plugins-row-body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.ds-247420 .ds-plugins-row-name {
  font-family: var(--ff-body); font-size: var(--fs-sm); color: var(--fg);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ds-247420 .ds-plugins-row-meta {
  font-size: var(--fs-micro); color: var(--fg-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ds-247420 .ds-plugins-row-busy { flex: 0 0 auto; font-size: var(--fs-micro); color: var(--fg-3); }

.ds-247420 .ds-plugins-dot {
  flex: 0 0 auto; width: 7px; height: 7px; border-radius: 50%;
}
.ds-247420 .ds-plugins-dot.tone-add { background: var(--green-2); }
.ds-247420 .ds-plugins-dot.tone-delete { background: var(--flame); }
.ds-247420 .ds-plugins-dot.tone-neutral { background: var(--fg-3); }

.ds-247420 .ds-plugins-main { flex: 1 1 auto; overflow-y: auto; padding: var(--space-4); }

.ds-247420 .ds-plugins-empty {
  height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--space-2); color: var(--fg-3);
}

.ds-247420 .ds-plugins-detail { display: flex; flex-direction: column; gap: var(--space-4); max-width: 640px; }
.ds-247420 .ds-plugins-detail-head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); flex-wrap: wrap;
}
.ds-247420 .ds-plugins-detail-title { display: flex; align-items: center; gap: 8px; min-width: 0; }
.ds-247420 .ds-plugins-detail-title .name {
  font-family: var(--ff-body); font-size: var(--fs-md); font-weight: 600; color: var(--fg);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ds-247420 .ds-plugins-version { font-size: var(--fs-micro); color: var(--fg-3); }

/* Same track/knob proportions as the shared .ds-toggle primitive
   (states-interactions.css) -- kept as its own class since ds-plugins-toggle
   is a plain <button>+.on instead of .ds-toggle's [aria-checked] contract,
   but sized and colored to match it exactly rather than drifting. */
.ds-247420 .ds-plugins-toggle {
  flex: 0 0 auto; width: 40px; height: 22px; border-radius: var(--r-pill, 999px); border: none; padding: 0;
  background: var(--rule); position: relative; cursor: pointer;
  transition: background var(--dur-snap) var(--ease);
}
.ds-247420 .ds-plugins-toggle.on { background: var(--accent-ink); }
.ds-247420 .ds-plugins-toggle[disabled] { cursor: wait; opacity: 0.65; }
.ds-247420 .ds-plugins-toggle-knob {
  position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 50%;
  background: var(--bg); box-shadow: var(--shadow-1, 0 1px 4px rgba(0,0,0,0.22));
  transition: left var(--dur-snap) var(--ease);
}
.ds-247420 .ds-plugins-toggle.on .ds-plugins-toggle-knob { left: 21px; }

.ds-247420 .ds-plugins-fact-grid {
  display: grid; grid-template-columns: minmax(96px, 130px) minmax(0, 1fr);
  gap: 9px var(--space-3); font-size: var(--fs-sm);
}
.ds-247420 .ds-plugins-fact-label { color: var(--fg-3); }
.ds-247420 .ds-plugins-fact-value { color: var(--fg-2); }
.ds-247420 .ds-plugins-fact-value.ds-plugins-mono { font-family: var(--ff-mono); overflow-wrap: anywhere; }
.ds-247420 .ds-plugins-fact-value.tone-text-add { color: var(--green-2); }
.ds-247420 .ds-plugins-fact-value.tone-text-delete { color: var(--flame); }
.ds-247420 .ds-plugins-fact-value.tone-text-neutral { color: var(--fg-3); }

.ds-247420 .ds-plugins-group-label {
  font-size: var(--fs-micro); font-weight: 600; color: var(--fg-3);
  letter-spacing: 0; margin-bottom: 6px;
}
.ds-247420 .ds-plugins-requires-list { display: flex; flex-wrap: wrap; gap: 6px; }
.ds-247420 .ds-plugins-chip {
  font-family: var(--ff-mono); font-size: var(--fs-micro); color: var(--fg-2);
  border: var(--bw-hair) solid var(--rule); border-radius: var(--r-1); padding: 2px 7px;
}
.ds-247420 .ds-plugins-requires-empty { font-size: var(--fs-sm); color: var(--fg-3); }

.ds-247420 .ds-plugins-detail-actions { display: flex; gap: var(--space-2); }
.ds-247420 .ds-plugins-btn {
  padding: 6px 12px; background: none; border: var(--bw-hair) solid var(--rule); border-radius: var(--r-2);
  color: var(--fg-2); cursor: pointer; font-size: var(--fs-sm);
}
.ds-247420 .ds-plugins-btn:hover { background: var(--bg-2); }
.ds-247420 .ds-plugins-btn[disabled] { cursor: not-allowed; opacity: 0.5; }

.ds-247420 .ds-plugins-footer {
  display: flex; align-items: center; justify-content: flex-end;
  padding: var(--space-2) var(--space-4);
  border-top: var(--bw-hair) solid var(--rule); flex: 0 0 auto;
}
.ds-247420 .ds-plugins-footer-count { font-size: var(--fs-micro); color: var(--fg-3); }
/* ============================================================
   ModelsConfig — provider/model availability panel.
   ============================================================ */
.ds-247420 .ds-mc { display: flex; flex-direction: column; gap: var(--space-3); }

.ds-247420 .ds-mc-loading, .ds-247420 .ds-mc-empty {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--space-2); padding: var(--space-6) var(--space-3);
  color: var(--fg-3); text-align: center;
}
.ds-247420 .ds-mc-empty-hint { font-size: var(--fs-xs); color: var(--fg-3); }

/* Summary bar: totals + last-checked + refresh/rebuild actions. */
.ds-247420 .ds-mc-summary {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
  flex-wrap: wrap;
}
.ds-247420 .ds-mc-summary-facts { display: flex; gap: var(--space-3); font-size: var(--fs-sm); color: var(--fg-2); }
.ds-247420 .ds-mc-summary-ts { color: var(--fg-3); }
.ds-247420 .ds-mc-summary-actions { display: flex; align-items: center; gap: var(--space-2); }
.ds-247420 .ds-mc-rebuild-error { font-size: var(--fs-xs); color: var(--flame); }

/* Two-pane body: provider tree (left) + model detail (right). */
.ds-247420 .ds-mc-body {
  display: grid; grid-template-columns: minmax(220px, 300px) 1fr; gap: var(--space-3);
  align-items: start;
}
@media (max-width: 720px) {
  .ds-247420 .ds-mc-body { grid-template-columns: 1fr; }
}

.ds-247420 .ds-mc-tree { display: flex; flex-direction: column; gap: 2px; }
.ds-247420 .ds-mc-tree-group + .ds-mc-tree-group { margin-top: 2px; }

.ds-247420 .ds-mc-key-chip {
  font-family: var(--ff-mono); font-size: var(--fs-micro); font-weight: 700;
  padding: 1px 6px; border-radius: var(--r-1);
}
.ds-247420 .ds-mc-key-chip.tone-ok { color: var(--green-2); background: var(--green-tint); }
.ds-247420 .ds-mc-key-chip.tone-fail { color: var(--fg-3); background: var(--bg-3); }

.ds-247420 .ds-mc-sampler {
  display: inline-block; margin: 2px 0 4px 12px;
  font-size: var(--fs-micro); padding: 1px 6px; border-radius: var(--r-1);
}
.ds-247420 .ds-mc-sampler.tone-ok { color: var(--green-2); background: var(--green-tint); }
.ds-247420 .ds-mc-sampler.tone-fail { color: var(--ink); background: var(--sun); }

/* Detail pane */
.ds-247420 .ds-mc-detail-empty { padding: var(--space-3); color: var(--fg-3); font-size: var(--fs-sm); }
.ds-247420 .ds-mc-detail-head {
  display: flex; align-items: center; gap: var(--space-2);
  margin-bottom: var(--space-2);
}
.ds-247420 .ds-mc-detail-title { font-family: var(--ff-body); font-size: var(--fs-base); font-weight: 700; }

.ds-247420 .ds-mc-discovery-error {
  display: flex; align-items: center; gap: var(--space-1-75);
  padding: var(--space-1-75) var(--space-2);
  margin-bottom: var(--space-2);
  color: var(--flame); background: color-mix(in oklab, var(--flame) 12%, transparent);
  border-radius: var(--r-1); font-size: var(--fs-xs);
}

.ds-247420 .ds-mc-model-list { display: flex; flex-direction: column; gap: var(--space-2); }
.ds-247420 .ds-mc-model-row {
  padding: var(--space-2); border: var(--bw-hair) solid var(--rule); border-radius: var(--r-2);
}
.ds-247420 .ds-mc-model-row.active { border-color: var(--accent); background: var(--accent-tint); }
.ds-247420 .ds-mc-model-head {
  display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
  margin-bottom: var(--space-1-75);
}
.ds-247420 .ds-mc-model-id { font-family: var(--ff-mono); font-size: var(--fs-sm); font-weight: 600; }
.ds-247420 .ds-mc-model-via { font-size: var(--fs-micro); color: var(--fg-3); }
.ds-247420 .ds-mc-usable-chip {
  font-size: var(--fs-micro); font-weight: 700; padding: 1px 6px; border-radius: var(--r-1);
  margin-left: auto;
}
.ds-247420 .ds-mc-usable-chip.tone-ok { color: var(--green-2); background: var(--green-tint); }
.ds-247420 .ds-mc-usable-chip.tone-fail { color: var(--fg-3); background: var(--bg-3); }

/* 7-mode availability grid — one chip per mode, wraps on narrow panes. */
.ds-247420 .ds-mc-mode-grid { display: flex; flex-wrap: wrap; gap: 4px; }
.ds-247420 .ds-mc-chip {
  font-family: var(--ff-mono); font-size: var(--fs-micro); font-weight: 600;
  padding: 2px 7px; border-radius: var(--r-1);
  border: var(--bw-hair) solid transparent;
}
.ds-247420 .ds-mc-chip.tone-ok { color: var(--green-2); background: var(--green-tint); }
.ds-247420 .ds-mc-chip.tone-fail { color: var(--on-color); background: var(--flame); }
.ds-247420 .ds-mc-chip.tone-skip { color: var(--fg-3); background: var(--bg-3); }
.ds-247420 .ds-mc-chip.tone-unknown { color: var(--fg-3); background: none; border-color: var(--rule); opacity: 0.6; }
/* ============================================================
   SkillsConfig — skill list + detail panel. Shares chrome with
   PluginsConfig (.ds-plugins-overlay/-modal/-header/-body/-sidebar/-list/
   -row/-dot/-main/-detail/-toggle/-fact-grid/-chip/-footer); the classes
   below are the skills-specific additions only.
   ============================================================ */

.ds-247420 .ds-skills-search-row {
  padding: var(--space-2) var(--space-4);
  border-bottom: var(--bw-hair) solid var(--rule); flex: 0 0 auto;
}

.ds-247420 .ds-skills-group { margin-bottom: var(--space-2); }
.ds-247420 .ds-skills-group-label {
  padding: var(--space-2) var(--space-2) 3px;
  font-size: var(--fs-micro); font-weight: 600; color: var(--fg-3);
  letter-spacing: 0;
}

.ds-247420 .ds-skills-description { font-size: var(--fs-sm); color: var(--fg-2); line-height: 1.5; }

.ds-247420 .ds-skills-body-group { display: flex; flex-direction: column; }
.ds-247420 .ds-skills-body-preview {
  font-family: var(--ff-mono); font-size: var(--fs-micro); color: var(--fg-2);
  background: var(--bg-2); border: var(--bw-hair) solid var(--rule); border-radius: var(--r-2);
  padding: var(--space-3); overflow: auto; max-height: 260px; white-space: pre-wrap; word-break: break-word;
}
/* ============================================================
   Slider — generic single-value range input
   Same overlay approach as .vx-vad-range (community.css): a real, fully
   transparent <input type="range"> sits on top of a custom track/fill/thumb
   so no browser's native thumb chrome ever paints, while pointer/keyboard/
   a11y semantics stay on the real input underneath.
   ============================================================ */
.ds-247420 .ds-slider { display: flex; flex-direction: column; gap: var(--space-1); width: 100%; }
.ds-247420 .ds-slider.is-disabled { opacity: 0.5; pointer-events: none; }
.ds-247420 .ds-slider-track {
  position: relative;
  height: 12px;
  margin: var(--space-2) 0;
  border-radius: var(--r-pill);
  background: var(--bg-3);
  overflow: visible;
}
.ds-247420 .ds-slider-fill {
  position: absolute; left: 0; top: 0; bottom: 0;
  width: calc(var(--ds-slider-pct, 0) * 1%);
  border-radius: var(--r-pill);
  background: var(--accent);
  transition: width var(--dur-snap) linear;
}
.ds-247420 .ds-slider-thumb {
  position: absolute; top: 50%; left: calc(var(--ds-slider-pct, 0) * 1%);
  width: 18px; height: 18px;
  border-radius: var(--r-pill);
  background: var(--bg-1);
  border: var(--bw-hair) solid var(--accent);
  box-shadow: var(--shadow-1);
  transform: translate(-50%, -50%);
  pointer-events: none;
  transition: box-shadow var(--dur-snap) var(--ease);
}
.ds-247420 .ds-slider-range {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  margin: 0; opacity: 0;
  cursor: pointer;
}
.ds-247420 .ds-slider-range:focus-visible ~ .ds-slider-thumb,
.ds-247420 .ds-slider-track:has(.ds-slider-range:focus-visible) .ds-slider-thumb {
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent) 40%, transparent), var(--shadow-1);
}
.ds-247420 .ds-slider-range:disabled { cursor: default; }

@media (prefers-reduced-motion: reduce) {
  .ds-247420 .ds-slider-fill,
  .ds-247420 .ds-slider-thumb {
    transition: none !important;
  }
}
/* ============================================================
   InputOTP — segmented PIN / one-time-code entry
   ============================================================ */
.ds-247420 .ds-otp { display: inline-flex; gap: var(--space-2); }
.ds-247420 .ds-otp-box {
  width: var(--ctl-md); height: var(--ctl-md);
  text-align: center;
  font-family: var(--ff-mono);
  font-size: var(--fs-lg);
  font-weight: 600;
  color: var(--fg);
  background: var(--bg-2);
  border: var(--bw-hair) solid var(--rule);
  border-radius: var(--r-1);
  transition: border-color var(--dur-snap) var(--ease), background var(--dur-snap) var(--ease), box-shadow var(--dur-snap) var(--ease);
}
.ds-247420 .ds-otp-box:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: var(--focus-ring-inset);
}
.ds-247420 .ds-otp-box-filled { border-color: var(--rule-strong); background: var(--bg-3); }
.ds-247420 .ds-otp-box-error { border-color: var(--red); }
.ds-247420 .ds-otp-box-error:focus-visible { box-shadow: var(--focus-ring-inset); }
.ds-247420 .ds-otp-box:disabled { opacity: 0.5; cursor: default; }

@media (prefers-reduced-motion: reduce) {
  .ds-247420 .ds-otp-box {
    transition: none !important;
  }
}
/* ============================================================
   Carousel — horizontal (or vertical) scroll-snap content carousel
   ============================================================ */
.ds-247420 .ds-carousel {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.ds-247420 .ds-carousel--vertical { flex-direction: column; }

.ds-247420 .ds-carousel-track {
  display: flex;
  flex: 1 1 auto;
  gap: var(--space-3);
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  -webkit-overflow-scrolling: touch;
}
.ds-247420 .ds-carousel-track--vertical {
  flex-direction: column;
  overflow-x: hidden;
  overflow-y: auto;
  scroll-snap-type: y mandatory;
  max-height: 480px;
}

.ds-247420 .ds-carousel-item {
  flex: 0 0 auto;
  scroll-snap-align: start;
}

.ds-247420 .ds-carousel-prev,
.ds-247420 .ds-carousel-next {
  flex: 0 0 auto;
  border-radius: var(--r-pill);
}

@media (prefers-reduced-motion: reduce) {
  .ds-247420 .ds-carousel-track {
    scroll-behavior: auto;
  }
}
/* ============================================================
   Calendar / DatePicker / DateRangePicker — month date-grid
   primitive plus its trigger+popover shells. Tokens only: no raw
   color/radius/z-index literal anywhere below (lint-tokens /
   lint-radius / lint-zindex hard gates).
   ============================================================ */
.ds-247420 .ds-cal {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-2);
  width: max-content;
  min-width: 264px;
}
.ds-247420 .ds-cal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}
.ds-247420 .ds-cal-title {
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--fg);
  text-align: center;
  flex: 1;
}
.ds-247420 .ds-cal-nav {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border: 0;
  border-radius: var(--r-1);
  background: transparent;
  color: var(--fg-3);
  cursor: pointer;
  transition: background-color var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease);
}
.ds-247420 .ds-cal-nav:hover { background: var(--bg-2); color: var(--fg); }
.ds-247420 .ds-cal-nav:focus-visible { box-shadow: var(--focus-ring-inset); outline: none; }
.ds-247420 .ds-cal-nav:active { transform: translateY(1px); }

.ds-247420 .ds-cal-weekdays {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: var(--space-1);
}
.ds-247420 .ds-cal-weekday-label {
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: 0;
  color: var(--fg-3);
  text-align: center;
  padding-block: var(--space-1);
}

.ds-247420 .ds-cal-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: var(--space-1);
}
.ds-247420 .ds-cal-day {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border: 0;
  border-radius: var(--r-1);
  background: transparent;
  color: var(--fg);
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  transition: background-color var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease);
}
.ds-247420 .ds-cal-day:hover { background: var(--bg-2); }
.ds-247420 .ds-cal-day:focus-visible { box-shadow: var(--focus-ring-inset); outline: none; }
.ds-247420 .ds-cal-day-outside { color: var(--fg-3); opacity: 0.55; }
.ds-247420 .ds-cal-day-today { font-weight: 700; box-shadow: inset 0 0 0 var(--bw-hair) var(--rule-strong); }
.ds-247420 .ds-cal-day-selected {
  background: var(--accent);
  color: var(--accent-fg);
  font-weight: 600;
}
.ds-247420 .ds-cal-day-selected:hover { background: var(--accent); }
.ds-247420 .ds-cal-day-in-range {
  background: var(--accent-tint);
  border-radius: var(--r-0);
}
.ds-247420 .ds-cal-day-range-start { border-radius: var(--r-1) var(--r-0) var(--r-0) var(--r-1); }
.ds-247420 .ds-cal-day-range-end { border-radius: var(--r-0) var(--r-1) var(--r-1) var(--r-0); }
.ds-247420 .ds-cal-day-disabled {
  color: var(--fg-3);
  opacity: 0.35;
  cursor: not-allowed;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .ds-247420 .ds-cal-nav, .ds-247420 .ds-cal-day { transition: none; }
}

/* -- DatePicker / DateRangePicker trigger -------------------------------- */
.ds-247420 .ds-dp { display: inline-flex; position: relative; }
.ds-247420 .ds-dp-trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1-5);
  min-height: 32px;
  padding: var(--space-1-5) var(--space-2-5);
  border: 0;
  border-radius: var(--r-2);
  background: var(--bg-2);
  color: var(--fg);
  font-family: inherit;
  font-size: var(--fs-sm);
  cursor: pointer;
  transition: background-color var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease);
}
.ds-247420 .ds-dp-trigger:hover { background: var(--bg-3); }
.ds-247420 .ds-dp-trigger:focus-visible { box-shadow: var(--focus-ring-inset); outline: none; }
.ds-247420 .ds-dp-trigger-label { white-space: nowrap; }

@media (prefers-reduced-motion: reduce) {
  .ds-247420 .ds-dp-trigger { transition: none; }
}
/* collab.css — LiveCursorOverlay / RemoteSelectionRings /
   RecentEditHighlightFlash / AgentPresenceChip / PresenceBar. Real-time
   multiplayer co-editing UI: remote pointers, selection rings, edit flashes,
   and a generalized collaborator presence chip row. `ds-collab-*` prefix,
   distinct from community.js's `cm-*` voice/member chips even though the
   presence chip visual language matches. */

.ds-247420 .ds-collab-cursor-overlay,
.ds-247420 .ds-collab-selection-overlay,
.ds-247420 .ds-collab-flash-overlay {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: var(--z-raised);
  overflow: hidden;
}

/* ---- LiveCursorOverlay -------------------------------------------------- */
.ds-247420 .ds-collab-cursor {
  position: absolute;
  display: flex;
  align-items: flex-start;
  gap: var(--space-1);
  transform: translate(-2px, -2px);
}
.ds-247420 .ds-collab-cursor svg { filter: drop-shadow(0 1px 1px color-mix(in oklab, var(--ink) 30%, transparent)); }
.ds-247420 .ds-collab-cursor-label {
  font-size: var(--fs-pico);
  font-weight: 600;
  line-height: 1.6;
  padding: 0 var(--space-1);
  border-radius: var(--r-hair);
  color: var(--on-color);
  white-space: nowrap;
  transform: translateY(2px);
}

/* ---- RemoteSelectionRings ------------------------------------------------ */
.ds-247420 .ds-collab-selection-ring {
  position: absolute;
  border: var(--bw-hair) solid var(--ring-color, var(--accent));
  background: color-mix(in oklab, var(--ring-color, var(--accent)) 14%, transparent);
  border-radius: var(--r-hair);
}

/* ---- RecentEditHighlightFlash -------------------------------------------
   Reduced-motion users get an instant flash with no animation (the fade-out
   keyframe lives only inside the no-preference query) rather than a fade. */
.ds-247420 .ds-collab-flash {
  position: absolute;
  background: color-mix(in oklab, var(--flash-color, var(--accent)) 35%, transparent);
  border-radius: var(--r-hair);
}
@media (prefers-reduced-motion: no-preference) {
  .ds-247420 .ds-collab-flash { animation: ds-collab-flash-fade var(--dur-reveal) var(--ease-exit) forwards; }
  @keyframes ds-collab-flash-fade {
    from { opacity: 1; }
    to { opacity: 0; }
  }
}
@media (prefers-reduced-motion: reduce) {
  .ds-247420 .ds-collab-flash { opacity: 1; }
}

/* ---- AgentPresenceChip / PresenceBar -------------------------------------
   Visual language matches community.js's cm-voice-user/cm-member-item chips
   (avatar circle + --avatar-bg custom property + initial + name), reused
   here under a distinct prefix since this is a different feature. */
.ds-247420 .ds-collab-bar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
.ds-247420 .ds-collab-bar-empty { color: var(--fg-3); font-size: var(--fs-sm); padding: var(--space-2) 0; }
/* workspace kit's live tab: PresenceBar summary strip stacked above
   SessionDashboard, filling the .ws-main flex column it renders inside. */
.ds-247420 .ds-workspace-live { display: flex; flex-direction: column; gap: var(--space-3); flex: 1; min-height: 0; }
.ds-247420 .ds-collab-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1-75);
  padding: var(--space-1) var(--space-2-5) var(--space-1) var(--space-1);
  border-radius: var(--r-pill);
  background: var(--bg-2);
}
/* .ds-collab-chip-avatar (24px identity circle) and .ds-collab-chip-status
   (8px status dot) below are both circular via border-radius: 50% but are
   intentionally distinct: different sizes, different content (initial vs.
   plain fill), different positioning role (layout box vs. absolute badge).
   Not extracting a shared class -- there's nothing substantive to share. */
.ds-247420 .ds-collab-chip-avatar {
  position: relative;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--avatar-bg, var(--bg-3));
  color: var(--fg-2);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: var(--fs-pico);
  font-weight: 600;
  flex-shrink: 0;
}
.ds-247420 .ds-collab-chip-status {
  position: absolute;
  right: -1px;
  bottom: -1px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  border: var(--bw-hair) solid var(--bg-2);
  background: var(--fg-3);
}
.ds-247420 .ds-collab-chip-status-active { background: var(--green-2); }
.ds-247420 .ds-collab-chip-status-idle { background: var(--sun); }
.ds-247420 .ds-collab-chip-status-offline { background: var(--fg-3); }
.ds-247420 .ds-collab-chip-name { font-size: var(--fs-tiny); color: var(--fg-2); white-space: nowrap; }

/* community.css */
/* ============================================================
   247420 design system — community surface (Discord-style chat)
   Tonal surfaces over borders. Indicator rails over hairlines.
   All tokens sourced from colors_and_type.css.
   ============================================================ */

/* ============================================================
   Small-label voice — the local composition of the house `.t-micro`
   recipe (src/css/app-shell/base.css). This sheet is a standalone
   published export consumed via its own <link>, so it cannot join the
   `.t-micro` selector list; it composes by referencing the identical
   token set instead. Every selector below that needs the label voice
   JOINS this rule and declares only what it deliberately differs on —
   never a second copy of these four declarations.
   ============================================================ */
.ds-247420 .cm-vs-label,
.ds-247420 .vx-section-label,
.ds-247420 .vx-chip-tag {
  font-family: var(--ff-mono);
  font-size: var(--fs-micro);
  text-transform: uppercase;
  letter-spacing: var(--tr-label);
  color: var(--fg-3);
}

/* ============================================================
   Shell — top-level flex container for the community layout
   ============================================================ */
.ds-247420 .cm-shell {
  display: flex;
  height: 100vh;
  height: 100dvh;
  width: 100%;
  overflow: hidden;
  /* Notched devices: keep the rail and bottom chrome out of the unsafe zones. */
  padding-bottom: env(safe-area-inset-bottom);
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
  background: var(--bg);
  color: var(--fg);
  font-family: var(--ff-body);
  font-size: var(--fs-sm);
}

.ds-247420 .cm-main {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-width: 0;
  min-height: 0;
  background: var(--bg);
  position: relative;
}

/* ============================================================
   Server rail — thin vertical column, leftmost
   ============================================================ */
.ds-247420 .cm-server-rail {
  flex: 0 0 72px;
  width: 72px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2-75) 0;
  background: color-mix(in oklab, var(--bg) 70%, var(--ink));
  overflow-y: auto;
  overflow-x: hidden;
  scrollbar-width: none;
}
.ds-247420 .cm-server-rail::-webkit-scrollbar { display: none; }

.ds-247420 .cm-server-icon {
  position: relative;
  width: 48px;
  height: 48px;
  border-radius: var(--r-3);
  background: var(--bg-2);
  color: var(--fg);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--ff-body);
  font-size: var(--fs-lg);
  font-weight: var(--fw-medium);
  letter-spacing: var(--tr-tight);
  cursor: pointer;
  overflow: hidden;
  transition:
    border-radius var(--dur-base) var(--ease),
    background var(--dur-snap) var(--ease),
    color var(--dur-snap) var(--ease),
    transform var(--dur-snap) var(--ease);
  user-select: none;
}
.ds-247420 .cm-server-icon img,
.ds-247420 .cm-server-icon svg {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.ds-247420 .cm-server-icon:hover {
  border-radius: var(--r-2);
  background: var(--accent);
  color: var(--accent-fg);
}
.ds-247420 .cm-server-icon.active {
  border-radius: var(--r-2);
  background: var(--accent);
  color: var(--accent-fg);
}

.ds-247420 .cm-server-pill {
  position: absolute;
  left: -10px;
  top: 50%;
  transform: translateY(-50%);
  width: 4px;
  height: 8px;
  background: var(--fg);
  border-radius: 0 var(--r-0) var(--r-0) 0;
  transition: height var(--dur-base) var(--ease);
}
.ds-247420 .cm-server-icon:hover .cm-server-pill { height: 20px; }
.ds-247420 .cm-server-icon.active .cm-server-pill { height: 36px; background: var(--fg); }

.ds-247420 .cm-server-badge {
  position: absolute;
  top: -2px;
  right: -2px;
  min-width: 18px;
  height: 18px;
  padding: 0 var(--space-1-5);
  /* --ink, not --paper: --paper is a fixed near-white, which measures ~3:1 on
     --warn (#FF5A52) -- below the 4.5:1 AA floor. --ink clears it (same fix
     as .btn-primary.danger in app-shell/files.css). */
  background: var(--warn);
  color: var(--ink);
  font-family: var(--ff-mono);
  font-size: var(--fs-micro);
  font-weight: var(--fw-bold);
  border-radius: var(--r-pill);
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--bg) 70%, var(--ink));
}

.ds-247420 .cm-server-back,
.ds-247420 .cm-server-add {
  width: 48px;
  height: 48px;
  border-radius: var(--r-3);
  background: var(--bg-2);
  color: var(--green);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--fs-xl);
  font-weight: var(--fw-regular);
  cursor: pointer;
  border: 0;
  transition:
    border-radius var(--dur-base) var(--ease),
    background var(--dur-snap) var(--ease),
    color var(--dur-snap) var(--ease);
}
.ds-247420 .cm-server-back { color: var(--fg-2); }
.ds-247420 .cm-server-back:hover,
.ds-247420 .cm-server-add:hover {
  border-radius: var(--r-2);
  background: var(--green-2);
  color: var(--paper);
}

.ds-247420 .cm-server-sep {
  width: 32px;
  height: 2px;
  background: color-mix(in oklab, var(--fg) 12%, transparent);
  border-radius: var(--r-hair);
  margin: var(--space-1) 0;
  flex-shrink: 0;
}

/* ============================================================
   Channel sidebar — server name header + channel list
   ============================================================ */
.ds-247420 .cm-channel-sidebar {
  flex: 0 0 240px;
  width: 240px;
  display: flex;
  flex-direction: column;
  min-height: 0;
  background: var(--bg-2);
  position: relative;
}

.ds-247420 .cm-server-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  padding: var(--space-2-75) var(--space-3);
  background: var(--bg-2);
  box-shadow: 0 1px 0 color-mix(in oklab, var(--fg) 8%, transparent);
  font-weight: var(--fw-medium);
  font-size: var(--fs-sm);
  letter-spacing: -0.01em;
  /* No cursor:pointer, no :hover background, no user-select:none. This is a
     static <div> (community/navigation.js) with no handler and no role — it
     just names the current server. Those three properties are the vocabulary
     of a pressable control, so together they promised a menu that does not
     exist: the pointer and the hover highlight both invited a click that
     nothing answers. Restore them only alongside a real handler + role. */
  flex-shrink: 0;
}
.ds-247420 .cm-server-header-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ds-247420 .cm-channel-list {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  padding: var(--space-2) 0 var(--space-3);
  display: flex;
  flex-direction: column;
  gap: var(--space-hair);
  scrollbar-width: thin;
  scrollbar-color: color-mix(in oklab, var(--fg) 18%, transparent) transparent;
}
.ds-247420 .cm-channel-list::-webkit-scrollbar { width: 8px; }
.ds-247420 .cm-channel-list::-webkit-scrollbar-track { background: transparent; }
.ds-247420 .cm-channel-list::-webkit-scrollbar-thumb {
  background: color-mix(in oklab, var(--fg) 18%, transparent);
  border-radius: var(--r-0);
}

/* ============================================================
   Channel category — collapsible group header
   ============================================================ */
.ds-247420 .cm-channel-category {
  display: flex;
  flex-direction: column;
  margin-top: var(--space-2-75);
}
.ds-247420 .cm-channel-category:first-child { margin-top: var(--space-1); }

.ds-247420 .cm-category-header {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-2);
  font-family: var(--ff-body);
  font-size: var(--fs-micro);
  font-weight: var(--fw-medium);
  color: var(--fg-3);
  letter-spacing: var(--tr-caps);
  text-transform: uppercase;
  cursor: pointer;
  user-select: none;
  transition: color var(--dur-snap) var(--ease);
}
.ds-247420 .cm-category-header:hover { color: var(--fg); }
.ds-247420 .cm-category-header:hover .cm-cat-add { opacity: 1; }

.ds-247420 .cm-cat-arrow {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 12px;
  height: 12px;
  font-size: var(--fs-pico);
  transform: rotate(90deg);
  transition: transform var(--dur-base) var(--ease);
  flex-shrink: 0;
}
.ds-247420 .cm-category-header.collapsed .cm-cat-arrow { transform: rotate(0deg); }

.ds-247420 .cm-cat-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Deliberately NOT the small-label voice: this is the `+` affordance sitting
   inside .cm-category-header, which IS uppercase + tracked. The zeroed
   letter-spacing and text-transform are resets of that inherited voice, not a
   copy of it — a bare glyph must not be tracked apart or case-folded. */
.ds-247420 .cm-cat-extra {
  font-family: var(--ff-mono);
  font-size: var(--fs-micro);
  color: var(--fg-3);
  font-weight: var(--fw-medium);
  letter-spacing: 0;
  text-transform: none;
}
.ds-247420 .cm-cat-add {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: var(--space-1);
  border: 0;
  background: transparent;
  color: var(--fg-3);
  font-size: var(--fs-sm);
  cursor: pointer;
  opacity: 0;
  border-radius: var(--r-1);
  transition: opacity var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease), background var(--dur-snap) var(--ease);
}
.ds-247420 .cm-cat-add:hover {
  color: var(--fg);
  background: color-mix(in oklab, var(--fg) 8%, transparent);
}
.ds-247420 .cm-cat-add:focus-visible {
  outline: var(--focus-w) solid var(--focus-color);
  outline-offset: 2px;
}

.ds-247420 .cm-cat-channels {
  display: flex;
  flex-direction: column;
  /* gap:1px is OFF-SCALE ON PURPOSE — channel rows are meant to read as one
     near-continuous stack with only a hairline seam between them. --space-hair
     (2px) doubles the seam and breaks the rows into visibly separate pills. */
  gap: 1px;
  padding: var(--space-hair) 0;
}

/* ============================================================
   Channel item — pillish row with active rail and hover state
   ============================================================ */
.ds-247420 .cm-channel-item-wrap {
  position: relative;
  padding: 0 var(--space-2);
}

.ds-247420 .cm-channel-item {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1-75) var(--space-2);
  border-radius: var(--r-1);
  color: var(--fg-3);
  cursor: pointer;
  user-select: none;
  transition:
    background var(--dur-snap) var(--ease),
    color var(--dur-snap) var(--ease);
}
.ds-247420 .cm-channel-item:hover {
  background: color-mix(in oklab, var(--fg) 6%, transparent);
  color: var(--fg);
}
.ds-247420 .cm-channel-item:hover .cm-ch-actions { opacity: 1; }

.ds-247420 .cm-channel-item.active {
  background: color-mix(in oklab, var(--fg) 10%, transparent);
  color: var(--fg);
  font-weight: var(--fw-medium);
}
.ds-247420 .cm-channel-item.active::before {
  content: '';
  position: absolute;
  left: -8px;
  top: 4px;
  bottom: 4px;
  width: 3px;
  background: var(--accent);
  border-radius: 0 var(--r-hair) var(--r-hair) 0;
}

.ds-247420 .cm-channel-item.voice-active {
  color: var(--green);
}
.ds-247420 .cm-channel-item.voice-active .cm-ch-icon { color: var(--green); }
.ds-247420 .cm-channel-item.voice-active:hover {
  background: color-mix(in oklab, var(--green) 12%, transparent);
}

.ds-247420 .cm-channel-item.voice-connecting {
  /* --sun measures 1.1-1.5:1 against every app background - amber is the
     tuned text-safe warning tone used for this severity elsewhere. */
  color: var(--amber);
}

.ds-247420 .cm-ch-icon {
  flex: 0 0 18px;
  width: 18px;
  height: 18px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--ff-mono);
  font-size: var(--fs-sm);
  color: var(--fg-3);
  opacity: 0.85;
}
.ds-247420 .cm-channel-item.active .cm-ch-icon { color: var(--fg-2); opacity: 1; }

.ds-247420 .cm-ch-spinner {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  border: 2px solid color-mix(in oklab, var(--amber) 30%, transparent);
  border-top-color: var(--amber);
  flex-shrink: 0;
}

@media (prefers-reduced-motion: no-preference) {
  .ds-247420 .cm-ch-spinner {
    animation: cm-spin var(--dur-slow) steps(8, end) infinite;
    will-change: transform;
    contain: layout;
  }
  @keyframes cm-spin {
    to { transform: rotate(360deg); }
  }
}

.ds-247420 .cm-ch-icon.voice-active-badge {
  color: var(--green);
  text-shadow: 0 0 8px var(--green-2);
}

.ds-247420 .cm-ch-voice-badge {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--green-2);
  flex-shrink: 0;
  position: relative;
}

@media (prefers-reduced-motion: no-preference) {
  .ds-247420 .cm-ch-voice-badge {
    animation: cm-voice-pulse 2s ease-in-out infinite;
  }
  @keyframes cm-voice-pulse {
    0%, 100% { opacity: 1; transform: scale(1); }
    50% { opacity: 0.5; transform: scale(1.3); }
  }
}

.ds-247420 .cm-ch-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--fs-sm);
}

.ds-247420 .cm-ch-badge {
  flex-shrink: 0;
  min-width: 16px;
  height: 16px;
  padding: 0 var(--space-1-5);
  /* --ink, not --paper: --paper is a fixed near-white, which measures ~3:1 on
     --warn (#FF5A52) -- below the 4.5:1 AA floor. --ink clears it (same fix
     as .btn-primary.danger in app-shell/files.css). */
  background: var(--warn);
  color: var(--ink);
  font-family: var(--ff-mono);
  font-size: var(--fs-micro);
  font-weight: var(--fw-bold);
  border-radius: var(--r-pill);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
}

.ds-247420 .cm-ch-actions {
  display: inline-flex;
  align-items: center;
  gap: var(--space-hair);
  opacity: 0;
  transition: opacity var(--dur-snap) var(--ease);
  flex-shrink: 0;
}
.ds-247420 .cm-channel-item.active .cm-ch-actions { opacity: 0.6; }

.ds-247420 .cm-ch-action-btn {
  width: 28px;
  height: 28px;
  padding: var(--space-1);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 0;
  color: var(--fg-3);
  cursor: pointer;
  border-radius: var(--r-1);
  font-size: var(--fs-tiny);
  transition: color var(--dur-snap) var(--ease), background var(--dur-snap) var(--ease);
}
.ds-247420 .cm-ch-action-btn:hover {
  color: var(--fg);
  background: color-mix(in oklab, var(--fg) 10%, transparent);
}
.ds-247420 .cm-ch-action-btn:focus-visible {
  outline: var(--focus-w) solid var(--focus-color);
  outline-offset: 2px;
}

/* Voice users nested under a voice channel */
.ds-247420 .cm-ch-voice-users {
  display: flex;
  flex-direction: column;
  /* gap:1px — same hairline-seam rationale as .cm-cat-channels above. */
  gap: 1px;
  padding: var(--space-hair) var(--space-2) var(--space-1) var(--space-5);
}

.ds-247420 .cm-ch-voice-user {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-half) var(--space-1-75);
  border-radius: var(--r-1);
  color: var(--fg-2);
  font-size: var(--fs-xs);
  cursor: pointer;
  transition: background var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease);
}
.ds-247420 .cm-ch-voice-user:hover {
  background: color-mix(in oklab, var(--fg) 6%, transparent);
  color: var(--fg);
}
.ds-247420 .cm-ch-voice-user.speaking {
  color: var(--green);
}
.ds-247420 .cm-ch-voice-user.speaking .cm-ch-voice-user-avatar {
  box-shadow: 0 0 0 2px var(--green-2);
}

.ds-247420 .cm-ch-voice-user-avatar {
  flex: 0 0 20px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--avatar-bg, var(--bg-3));
  color: var(--avatar-fg, var(--fg-2));
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: var(--fs-pico);
  font-weight: var(--fw-medium);
  overflow: hidden;
  /* Depth over a flat solid-color disc: a hairline inset ring so avatars
     read as a real surface element, not a wireframe placeholder. */
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--fg) 10%, transparent);
  transition: box-shadow var(--dur-snap) var(--ease);
}
.ds-247420 .cm-ch-voice-user-avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.ds-247420 .cm-ch-voice-user-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ============================================================
   Voice strip — slots above user panel when in voice
   ============================================================ */
.ds-247420 .cm-voice-strip {
  display: none;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-2-5) var(--space-2-75);
  background: color-mix(in oklab, var(--green) 18%, var(--bg-2));
  color: var(--fg);
  box-shadow: 0 -1px 0 color-mix(in oklab, var(--fg) 10%, transparent);
  flex-shrink: 0;
}
.ds-247420 .cm-voice-strip.open { display: flex; }

/* Joins the small-label voice above. Deliberate overrides: --green (this is a
   live-recording indicator, not neutral chrome) and weight 700 (it sits next to
   a pulsing dot and must read as the loudest thing in the strip). */
.ds-247420 .cm-vs-label {
  color: var(--green);
  font-weight: var(--fw-bold);
  display: inline-flex;
  align-items: center;
  gap: var(--space-1-75);
}
.ds-247420 .cm-vs-label::before {
  content: '';
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--green-2);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--green-2) 30%, transparent);
}

@media (prefers-reduced-motion: no-preference) {
  .ds-247420 .cm-vs-label::before {
    animation: cm-pulse 1.6s ease-in-out infinite;
  }
  @keyframes cm-pulse {
    0%, 100% { box-shadow: 0 0 0 3px color-mix(in oklab, var(--green-2) 30%, transparent); }
    50%      { box-shadow: 0 0 0 6px color-mix(in oklab, var(--green-2) 0%,  transparent); }
  }
}

.ds-247420 .cm-vs-channel {
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  color: var(--fg);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ds-247420 .cm-vs-status {
  font-size: var(--fs-xs);
  color: var(--fg-2);
  display: flex;
  align-items: center;
  gap: var(--space-1-75);
  flex-wrap: wrap;
}

.ds-247420 .cm-vs-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-1-75);
  height: 28px;
  padding: 0 var(--space-2-5);
  background: color-mix(in oklab, var(--fg) 10%, transparent);
  color: var(--fg);
  border: 0;
  border-radius: var(--r-1);
  font-family: var(--ff-body);
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  cursor: pointer;
  transition: background var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease);
}
.ds-247420 .cm-vs-btn:hover {
  background: color-mix(in oklab, var(--fg) 18%, transparent);
}
.ds-247420 .cm-vs-btn.danger {
  /* --ink, not --paper: --paper is a fixed near-white, which measures ~3:1 on
     --warn (#FF5A52) -- below the 4.5:1 AA floor. Same fix pattern as
     .btn-primary.danger in app-shell/files.css. */
  background: var(--warn);
  color: var(--ink);
}
.ds-247420 .cm-vs-btn.danger:hover {
  background: color-mix(in oklab, var(--warn) 80%, var(--ink));
}

/* ============================================================
   User panel — bottom of channel sidebar
   ============================================================ */
.ds-247420 .cm-user-panel {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2);
  background: color-mix(in oklab, var(--fg) 5%, var(--bg-2));
  flex-shrink: 0;
}

.ds-247420 .cm-user-avatar {
  position: relative;
  flex: 0 0 32px;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--avatar-bg, var(--bg-3));
  color: var(--avatar-fg, var(--fg-2));
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  overflow: hidden;
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--fg) 10%, transparent);
}
.ds-247420 .cm-user-avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.ds-247420 .cm-user-status-dot {
  position: absolute;
  right: -2px;
  bottom: -2px;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--green-2);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--fg) 5%, var(--bg-2));
}

.ds-247420 .cm-user-info {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  line-height: 1.2;
}
.ds-247420 .cm-user-name {
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  color: var(--fg);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Deliberately NOT the small-label voice: a user's handle/tag is content, not
   chrome, so it stays sentence-case and untracked. Mono + micro + --fg-3 only. */
.ds-247420 .cm-user-tag {
  font-family: var(--ff-mono);
  font-size: var(--fs-micro);
  color: var(--fg-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ds-247420 .cm-user-controls {
  display: inline-flex;
  align-items: center;
  gap: var(--space-hair);
}

.ds-247420 .cm-user-btn {
  width: 28px;
  height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 0;
  color: var(--fg-2);
  cursor: pointer;
  border-radius: var(--r-1);
  font-size: var(--fs-sm);
  transition: color var(--dur-snap) var(--ease), background var(--dur-snap) var(--ease);
}
.ds-247420 .cm-user-btn:hover {
  background: color-mix(in oklab, var(--fg) 10%, transparent);
  color: var(--fg);
}
.ds-247420 .cm-user-btn.muted {
  color: var(--warn);
}
.ds-247420 .cm-user-btn.deafened {
  color: var(--warn);
  background: color-mix(in oklab, var(--warn) 18%, transparent);
}

/* ============================================================
   Chat header — top of main column
   ============================================================ */
.ds-247420 .cm-chat-header {
  display: flex;
  align-items: center;
  gap: var(--space-2-5);
  padding: var(--space-2-75) var(--space-3);
  background: var(--bg);
  box-shadow: 0 1px 0 color-mix(in oklab, var(--fg) 8%, transparent);
  flex-shrink: 0;
  min-height: 48px;
}

.ds-247420 .cm-chat-header-icon {
  flex: 0 0 20px;
  width: 20px;
  height: 20px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--ff-mono);
  font-size: var(--fs-lg);
  color: var(--fg-3);
}

.ds-247420 .cm-chat-header-name {
  /* This is an <h1> (it is the visible title of the content column, and the
     community kits otherwise rendered no heading at all). The rule already
     pinned weight/size/colour, but a bare h1 also brings a UA block margin
     that would break this flex row — so the margin is zeroed explicitly. */
  margin: 0;
  font-weight: var(--fw-medium);
  font-size: var(--fs-sm);
  color: var(--fg);
  letter-spacing: -0.01em;
  flex-shrink: 0;
}

.ds-247420 .cm-chat-header-topic {
  flex: 1;
  min-width: 0;
  padding-left: var(--space-2-75);
  margin-left: var(--space-1);
  border-left: 1px solid color-mix(in oklab, var(--fg) 12%, transparent);
  color: var(--fg-3);
  font-size: var(--fs-xs);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ds-247420 .cm-chat-header-toolbar {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  flex-shrink: 0;
}

/* ============================================================
   Member list — right column
   ============================================================ */
.ds-247420 .cm-member-list {
  flex: 0 0 240px;
  width: 240px;
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  background: var(--bg-2);
  padding: var(--space-3) 0;
  scrollbar-width: thin;
  scrollbar-color: color-mix(in oklab, var(--fg) 18%, transparent) transparent;
  transition: width var(--dur-base) var(--ease), flex-basis var(--dur-base) var(--ease);
  contain: layout style;
}
.ds-247420 .cm-member-list:not(.open) {
  width: 0;
  flex-basis: 0;
  padding: 0;
  overflow: hidden;
  visibility: hidden;
  opacity: 0;
  transform: translateX(100%);
}
.ds-247420 .cm-member-list::-webkit-scrollbar { width: 8px; }
.ds-247420 .cm-member-list::-webkit-scrollbar-thumb {
  background: color-mix(in oklab, var(--fg) 18%, transparent);
  border-radius: var(--r-0);
}

.ds-247420 .cm-member-category {
  padding: var(--space-2-75) var(--space-3) var(--space-1-75);
  font-family: var(--ff-body);
  font-size: var(--fs-micro);
  font-weight: var(--fw-medium);
  color: var(--fg-3);
  letter-spacing: var(--tr-caps);
  text-transform: uppercase;
}

.ds-247420 .cm-member-item {
  display: flex;
  align-items: center;
  gap: var(--space-2-5);
  padding: var(--space-1-75) var(--space-2-75);
  margin: 0 var(--space-2);
  border-radius: var(--r-1);
  color: var(--fg-2);
  cursor: pointer;
  transition: background var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease);
  max-width: 100%;
  min-width: 0;
}
.ds-247420 .cm-member-item:hover {
  background: color-mix(in oklab, var(--fg) 6%, transparent);
  color: var(--fg);
}

.ds-247420 .cm-member-avatar {
  position: relative;
  flex: 0 0 28px;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--avatar-bg, var(--bg-3));
  color: var(--avatar-fg, var(--fg-2));
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: var(--fs-micro);
  font-weight: var(--fw-medium);
  overflow: hidden;
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--fg) 10%, transparent);
}
.ds-247420 .cm-member-avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.ds-247420 .cm-member-status {
  position: absolute;
  right: -2px;
  bottom: -2px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--fg-3);
  box-shadow: 0 0 0 2px var(--bg-2);
}
.ds-247420 .cm-member-status.online {
  background: var(--green-2);
}

.ds-247420 .cm-member-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--fs-sm);
}

/* ============================================================
   Voice user (standalone, e.g. in voice stage area)
   ============================================================ */
.ds-247420 .cm-voice-user {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1-75);
  padding: var(--space-2-5);
  border-radius: var(--r-2);
  background: var(--bg-2);
  transition: box-shadow var(--dur-snap) var(--ease), background var(--dur-snap) var(--ease);
}
.ds-247420 .cm-voice-user.speaking {
  background: color-mix(in oklab, var(--green-2) 12%, var(--bg-2));
  box-shadow: 0 0 0 2px var(--green-2);
}

.ds-247420 .cm-voice-user-avatar {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: var(--avatar-bg, var(--bg-3));
  color: var(--avatar-fg, var(--fg-2));
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: var(--fs-lg);
  font-weight: var(--fw-medium);
  overflow: hidden;
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--fg) 10%, transparent);
}
.ds-247420 .cm-voice-user-avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.ds-247420 .cm-voice-user.speaking .cm-voice-user-avatar {
  box-shadow: 0 0 0 2px var(--green-2);
}

.ds-247420 .cm-voice-user-name {
  font-size: var(--fs-xs);
  color: var(--fg);
  font-weight: var(--fw-medium);
  max-width: 100px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: center;
}

/* ============================================================
   Responsive — collapse sidebars on narrow viewports
   ============================================================ */
@media (max-width: 900px) {
  .ds-247420 .cm-channel-sidebar {
    position: absolute;
    z-index: calc(var(--z-drawer) + 1);
    top: 0;
    left: 72px;
    bottom: 0;
    transform: translateX(-110%);
    transition: transform var(--dur-base) var(--ease);
    box-shadow: 2px 0 16px color-mix(in oklab, var(--ink) 30%, transparent);
  }
  .ds-247420 .cm-channel-sidebar.open { transform: translateX(0); }
  .ds-247420 .cm-member-list { display: none; }
  .ds-247420 .cm-member-list.open {
    display: flex;
    position: absolute;
    z-index: calc(var(--z-drawer) + 1);
    top: 0;
    right: 0;
    bottom: 0;
    width: 240px;
    flex-basis: 240px;
    box-shadow: -2px 0 16px color-mix(in oklab, var(--ink) 30%, transparent);
  }
  .ds-247420 .cm-chat-header-topic { display: none; }
}

/* Drawer scrim — dims the chat while a drawer overlays it on narrow screens.
   Visual affordance only (pointer-events: none): tap-to-close needs a real
   element + handler in CommunityShell, which lives outside this stylesheet. */
@media (max-width: 900px) {
  .ds-247420 .cm-shell:has(.cm-channel-sidebar.open) .cm-main::before,
  .ds-247420 .cm-shell:has(.cm-member-list.open) .cm-main::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: var(--z-drawer);
    background: color-mix(in oklab, var(--ink) 40%, transparent);
    pointer-events: none;
  }
}

@media (max-width: 480px) {
  .ds-247420 .cm-server-rail {
    flex: 0 0 56px;
    width: 56px;
  }
  .ds-247420 .cm-server-icon,
  .ds-247420 .cm-server-add,
  .ds-247420 .cm-server-back {
    width: 44px;
    height: 44px;
    padding: var(--space-1);
  }
  .ds-247420 .cm-channel-sidebar,
  .ds-247420 .cm-channel-sidebar.open {
    left: 56px;
    width: 80vw;
    /* Use the width beside the 56px rail, not a 200px sliver. */
    max-width: min(320px, calc(100vw - 56px));
  }
  .ds-247420 .cm-ch-icon {
    flex: 0 0 24px;
    width: 24px;
    height: 24px;
  }
}

/* Touch target floors — applies on any coarse-pointer device (phones AND
   touch tablets/laptops), not just narrow viewports. */
@media (pointer: coarse) {
  .ds-247420 .cm-channel-item,
  .ds-247420 .cm-member-item { min-height: 44px; }
  .ds-247420 .cm-cat-add,
  .ds-247420 .cm-ch-action-btn,
  .ds-247420 .cm-user-btn,
  .ds-247420 .cm-vs-btn,
  .ds-247420 .cm-tp-new,
  .ds-247420 .cm-tp-close,
  .ds-247420 .cm-rb-cancel,
  .ds-247420 .cm-mh-btn {
    min-width: 44px;
    min-height: 44px;
  }
  .ds-247420 .cm-forum-search,
  .ds-247420 .cm-forum-sort,
  .ds-247420 .cm-forum-new { min-height: 44px; }
}

/* Ultrawide: clamp the message/forum/page column to a readable measure. */
@media (min-width: 1600px) {
  .ds-247420 .cm-main > .chat,
  .ds-247420 .cm-forum-list,
  .ds-247420 .cm-page-body {
    max-width: 110ch;
    margin-inline: auto;
    width: 100%;
  }
}

/* No hover on touch: hover-revealed channel actions must stay reachable. */
@media (hover: none) {
  .ds-247420 .cm-channel-item .cm-ch-actions { opacity: 0.6; }
  .ds-247420 .cm-category-header .cm-cat-add { opacity: 0.6; }
}

/* ---------- a11y polish: focus-visible coverage on community interactive elements ---------- */
/* Every interactive surface with a :hover state must also have a :focus-visible state so
   keyboard users get equivalent feedback. The selectors here mirror the existing :hover rules. */

.ds-247420 .cm-server-icon:focus-visible,
.ds-247420 .cm-server-back:focus-visible,
.ds-247420 .cm-server-add:focus-visible {
  outline: var(--focus-w) solid var(--focus-color);
  outline-offset: 2px;
  border-radius: var(--r-2);
}

.ds-247420 .cm-channel-item:focus-visible {
  outline: var(--focus-w) solid var(--focus-color);
  outline-offset: -2px;
  border-radius: var(--r-1);
}

.ds-247420 .cm-server-header:focus-visible,
.ds-247420 .cm-category-header:focus-visible {
  outline: var(--focus-w) solid var(--focus-color);
  outline-offset: -2px;
}

.ds-247420 .cm-member-item:focus-visible {
  outline: var(--focus-w) solid var(--focus-color);
  outline-offset: -2px;
  border-radius: var(--r-1);
}

.ds-247420 .cm-vs-btn:focus-visible,
.ds-247420 .cm-user-btn:focus-visible {
  outline: var(--focus-w) solid var(--focus-color);
  outline-offset: 2px;
  border-radius: var(--r-1);
}

/* ThreadPanel / Forum / Page interactive elements */
.ds-247420 .cm-tp-item:focus-visible,
.ds-247420 .cm-forum-item:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: -2px; border-radius: var(--r-1); }
.ds-247420 .cm-tp-new:focus-visible,
.ds-247420 .cm-tp-close:focus-visible,
.ds-247420 .cm-forum-new:focus-visible,
.ds-247420 .cm-forum-sort:focus-visible,
.ds-247420 .cm-page-edit:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: 2px; border-radius: var(--r-1); }
.ds-247420 .cm-forum-search:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--accent-ink); }

/* ---------- mobile header ---------- */
.ds-247420 .cm-mobile-header {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  height: calc(48px + env(safe-area-inset-top));
  padding: env(safe-area-inset-top) max(var(--space-2), env(safe-area-inset-right)) 0 max(var(--space-2), env(safe-area-inset-left));
  background: var(--bg-2);
  border-bottom: 1px solid var(--panel-3);
}
.ds-247420 .cm-mh-btn {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border: none;
  background: transparent;
  color: var(--fg);
  font-size: var(--fs-lg);
  line-height: 1;
  cursor: pointer;
  border-radius: var(--r-1);
}
.ds-247420 .cm-mh-btn:hover { background: var(--panel-3); }
.ds-247420 .cm-mh-btn:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: 2px; }
.ds-247420 .cm-mh-title {
  flex: 1 1 auto;
  min-width: 0;
  font-family: var(--font-mono, monospace);
  font-weight: var(--fw-medium);
  color: var(--fg);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ---------- reply bar ---------- */
.ds-247420 .cm-reply-bar {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1) var(--space-2);
  background: var(--bg-2);
  /* Hairline, not a 3px accent bar. This is a transient status strip pinned
     above the composer (role=status, "Replying to <author>"), not a card or a
     callout — the tinted fill already carries the attachment. A chunky left
     bar here was the generic accent-stripe tell; the real accent still lands
     on .cm-rb-author. */
  border-left: var(--bw-hair) solid var(--rule-strong);
  font-size: var(--fs-xs);
}
.ds-247420 .cm-rb-label { flex: none; color: var(--fg-2); }
.ds-247420 .cm-rb-author { color: var(--accent); font-weight: var(--fw-medium); }
.ds-247420 .cm-rb-preview {
  flex: 1 1 auto;
  min-width: 0;
  color: var(--fg-2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.ds-247420 .cm-rb-cancel {
  flex: none;
  width: 22px;
  height: 22px;
  border: none;
  background: transparent;
  color: var(--fg-2);
  font-size: var(--fs-tiny);
  line-height: 1;
  cursor: pointer;
  border-radius: var(--r-1);
}
.ds-247420 .cm-rb-cancel:hover { background: var(--panel-3); color: var(--fg); }
.ds-247420 .cm-rb-cancel:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: 1px; }

/* ---------- banner ---------- */
.ds-247420 .cm-banner {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  padding: var(--space-2) var(--space-3);
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
}
.ds-247420 .cm-banner.clickable { cursor: pointer; }
.ds-247420 .cm-banner.clickable:hover { background: color-mix(in oklab, currentColor 10%, transparent); }
.ds-247420 .cm-banner-msg { flex: 1 1 auto; min-width: 0; }
.ds-247420 .cm-banner-action {
  flex: none;
  border: 1px solid currentColor;
  background: transparent;
  color: inherit;
  font: inherit;
  font-weight: var(--fw-semibold);
  padding: var(--space-hair) var(--space-2);
  border-radius: var(--r-1);
  cursor: pointer;
}
.ds-247420 .cm-banner-action:hover { background: color-mix(in oklab, currentColor 16%, transparent); }
.ds-247420 .cm-banner-action:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
.ds-247420 .cm-banner.tone-warning {
  background: color-mix(in oklab, var(--flame) 18%, var(--bg));
  color: var(--flame);
}
.ds-247420 .cm-banner.tone-success {
  background: color-mix(in oklab, var(--success) 18%, var(--bg));
  color: var(--green);
}
.ds-247420 .cm-banner.tone-error {
  background: color-mix(in oklab, var(--danger) 18%, var(--bg));
  color: var(--danger);
}
.ds-247420 .cm-banner.tone-info {
  background: var(--accent-tint);
  color: var(--accent);
}

/* ---------- prefers-reduced-motion: kill the transitions that were not previously guarded ---------- */
@media (prefers-reduced-motion: reduce) {
  .ds-247420 .cm-server-icon,
  .ds-247420 .cm-channel-item,
  .ds-247420 .cm-member-item,
  .ds-247420 .cm-vs-btn,
  .ds-247420 .cm-user-btn,
  .ds-247420 .cm-ch-spinner,
  .ds-247420 .cm-ch-voice-badge,
  .ds-247420 .vx-ptt,
  .ds-247420 .vx-ptt-glow,
  .ds-247420 .vx-vad-meter,
  .ds-247420 .vx-vad-fill {
    transition: none !important;
    animation: none !important;
  }
}

/* ============================================================
   Voice surfaces (vx-*) — PTT, VAD meter, webcam, settings, queue
   ============================================================ */

.ds-247420 .vx-ptt {
  position: relative;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  min-width: 120px;
  min-height: 120px;
  padding: var(--space-3);
  border: var(--bw-rule) solid var(--rule-strong);
  border-radius: var(--r-pill);
  background: var(--bg-2);
  color: var(--fg);
  font-family: var(--ff-body);
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
  touch-action: none;
  transition: background var(--dur-base) var(--ease), border-color var(--dur-base) var(--ease), transform var(--dur-snap) var(--ease);
  overflow: hidden;
}
.ds-247420 .vx-ptt:hover { background: color-mix(in oklab, var(--fg) 8%, var(--bg-2)); }
.ds-247420 .vx-ptt:active { transform: scale(0.97); }
.ds-247420 .vx-ptt-icon { font-size: var(--fs-xl); line-height: 1; }
.ds-247420 .vx-ptt-label { font-size: var(--fs-tiny); letter-spacing: var(--tr-label); text-transform: uppercase; }
.ds-247420 .vx-ptt-glow {
  position: absolute; inset: -4px;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0;
}
@media (max-height: 420px) {
  /* Phone-in-landscape: topbar + chat-head + user-identity bar alone can
     consume most of a ~320px-tall viewport, leaving no room for the fixed
     120px .vx-ptt plus the voice grid above it -- shrink the PTT button so
     both remain visible instead of the label clipping off-screen. */
  .ds-247420 .vx-ptt { min-width: 88px; min-height: 64px; padding: var(--space-2); }
  .ds-247420 .vx-ptt-icon { font-size: var(--fs-lg); }
}
.ds-247420 .vx-ptt-recording, .ds-247420 .vx-ptt-live {
  background: var(--accent);
  color: var(--accent-fg);
  border-color: var(--accent);
}
.ds-247420 .vx-ptt-recording .vx-ptt-glow, .ds-247420 .vx-ptt-live .vx-ptt-glow {
  opacity: 1;
  box-shadow: 0 0 0 0 color-mix(in oklab, var(--accent) 60%, transparent);
  animation: vx-pulse 1.4s var(--ease) infinite;
}
.ds-247420 .vx-ptt-vad {
  border-color: var(--green-2);
  background: color-mix(in oklab, var(--green) 16%, var(--bg-2));
}
@keyframes vx-pulse {
  0% { box-shadow: 0 0 0 0 color-mix(in oklab, var(--accent) 55%, transparent); }
  70% { box-shadow: 0 0 0 14px color-mix(in oklab, var(--accent) 0%, transparent); }
  100% { box-shadow: 0 0 0 0 color-mix(in oklab, var(--accent) 0%, transparent); }
}

.ds-247420 .vx-vad { display: flex; flex-direction: column; gap: var(--space-1); width: 100%; }
.ds-247420 .vx-vad-track {
  position: relative;
  height: 12px;
  border-radius: var(--r-pill);
  background: var(--bg-3);
  overflow: visible;
}
.ds-247420 .vx-vad-fill {
  position: absolute; left: 0; top: 0; bottom: 0;
  border-radius: var(--r-pill);
  background: var(--fg-3);
  transition: width var(--dur-snap) linear;
}
.ds-247420 .vx-vad-fill-over { background: var(--green); }
.ds-247420 .vx-vad-marker {
  position: absolute; top: -3px; bottom: -3px;
  width: var(--bw-rule);
  background: var(--accent);
  transform: translateX(-50%);
  pointer-events: none;
}
.ds-247420 .vx-vad-range {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  margin: 0; opacity: 0;
  cursor: pointer;
}
.ds-247420 .vx-vad-readout {
  display: flex; justify-content: space-between;
  font-family: var(--ff-mono);
  font-size: var(--fs-micro);
  color: var(--fg-3);
}

.ds-247420 .vx-cam { display: flex; flex-direction: column; gap: var(--space-2); }
.ds-247420 .vx-cam-stage {
  position: relative;
  aspect-ratio: 4 / 3;
  width: 100%;
  border-radius: var(--r-2);
  overflow: hidden;
  background: var(--bg-3);
}
.ds-247420 .vx-cam-video { width: 100%; height: 100%; object-fit: cover; display: block; }
.ds-247420 .vx-cam-placeholder {
  position: absolute; inset: 0;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: var(--space-2);
  color: var(--fg-3);
  font-size: var(--fs-sm);
}
.ds-247420 .vx-cam-placeholder span:first-child { font-size: var(--fs-h3); }
.ds-247420 .vx-cam-controls { display: flex; gap: var(--space-2); align-items: center; }
.ds-247420 .vx-cam-controls .vx-select { flex: 1; }

.ds-247420 .vx-select {
  appearance: none;
  padding: var(--space-1) var(--space-2);
  border: var(--bw-hair) solid var(--rule-strong);
  border-radius: var(--r-1);
  background: var(--bg-2);
  color: var(--fg);
  font-family: var(--ff-body);
  font-size: var(--fs-sm);
  cursor: pointer;
}
.ds-247420 .vx-btn {
  padding: var(--space-1) var(--space-3);
  border: var(--bw-hair) solid var(--rule-strong);
  border-radius: var(--r-1);
  background: var(--bg-2);
  color: var(--fg);
  font-family: var(--ff-body);
  font-size: var(--fs-sm);
  font-weight: var(--fw-semibold);
  cursor: pointer;
  transition: background var(--dur-base) var(--ease);
}
.ds-247420 .vx-btn:hover { background: var(--bg-4, var(--bg-3)); }
.ds-247420 .vx-btn-on, .ds-247420 .vx-btn-primary { background: var(--accent); color: var(--accent-fg); border-color: var(--accent); }

.ds-247420 .vx-modal-backdrop {
  position: fixed; inset: 0;
  z-index: var(--z-modal);
  display: flex; align-items: center; justify-content: center;
  padding: var(--space-3);
  background: color-mix(in oklab, var(--ink) 55%, transparent);
}
.ds-247420 .vx-modal {
  width: 100%; max-width: 440px;
  max-height: 90vh;
  max-height: 90dvh;
  display: flex; flex-direction: column;
  border-radius: var(--r-3);
  background: var(--bg);
  color: var(--fg);
  box-shadow: var(--shadow-3);
  overflow: hidden;
}
.ds-247420 .vx-modal-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--space-3);
  border-bottom: var(--bw-hair) solid var(--rule);
}
.ds-247420 .vx-modal-title { margin: 0; font-size: var(--fs-lg); font-weight: var(--fw-semibold); }
.ds-247420 .vx-modal-x {
  border: 0; background: transparent; color: var(--fg-3);
  font-size: var(--fs-xl); line-height: 1; cursor: pointer; padding: 0 var(--space-1);
}
.ds-247420 .vx-modal-x:hover { color: var(--fg); }
.ds-247420 .vx-modal-body { padding: var(--space-3); overflow-y: auto; display: flex; flex-direction: column; gap: var(--space-3); }
.ds-247420 .vx-modal-foot {
  display: flex; justify-content: flex-end; gap: var(--space-2);
  padding: var(--space-3);
  border-top: var(--bw-hair) solid var(--rule);
}
.ds-247420 .vx-section { display: flex; flex-direction: column; gap: var(--space-2); }
/* Joins the small-label voice above with no overrides — it IS the plain voice. */
.ds-247420 .vx-segmented { display: flex; gap: var(--space-1); }
.ds-247420 .vx-seg {
  flex: 1;
  padding: var(--space-1) var(--space-2);
  border: var(--bw-hair) solid var(--rule-strong);
  border-radius: var(--r-1);
  background: var(--bg-2);
  color: var(--fg);
  font-family: var(--ff-body);
  font-size: var(--fs-tiny);
  font-weight: var(--fw-medium);
  letter-spacing: var(--tr-caps);
  cursor: pointer;
}
.ds-247420 .vx-seg-on { background: var(--accent); color: var(--accent-fg); border-color: var(--accent); }
.ds-247420 .vx-range-row { display: flex; align-items: center; gap: var(--space-2); }
.ds-247420 .vx-range { flex: 1; accent-color: var(--accent); cursor: pointer; }
.ds-247420 .vx-range-val { font-family: var(--ff-mono); font-size: var(--fs-tiny); color: var(--fg-2); min-width: 56px; text-align: right; }
.ds-247420 .vx-toggle-row {
  display: flex; align-items: center; justify-content: space-between;
  font-size: var(--fs-sm);
}
.ds-247420 .vx-toggle { accent-color: var(--accent); width: 18px; height: 18px; cursor: pointer; }
@media (pointer: coarse) {
  .ds-247420 .vx-toggle { width: 24px; height: 24px; }
}

.ds-247420 .vx-queue {
  display: flex; align-items: center; gap: var(--space-2);
  padding: var(--space-2);
  border-radius: var(--r-2);
  background: var(--bg-2);
}
.ds-247420 .vx-queue-empty { justify-content: center; }
.ds-247420 .vx-queue-empty-text { color: var(--fg-3); font-size: var(--fs-tiny); font-family: var(--ff-mono); }
.ds-247420 .vx-queue-ctrls { display: flex; gap: var(--space-1); flex-shrink: 0; }
.ds-247420 .vx-queue-btn {
  width: 32px; height: 32px;
  display: inline-flex; align-items: center; justify-content: center;
  border: var(--bw-hair) solid var(--rule-strong);
  border-radius: var(--r-1);
  background: var(--bg-3);
  color: var(--fg);
  cursor: pointer;
  font-size: var(--fs-sm);
}
.ds-247420 .vx-queue-btn:hover { background: var(--bg-4, var(--bg-2)); }
.ds-247420 .vx-queue-strip {
  display: flex; gap: var(--space-2);
  overflow-x: auto;
  flex: 1;
  padding-bottom: var(--space-hair);
}
.ds-247420 .vx-chip {
  display: inline-flex; align-items: center; gap: var(--space-1);
  flex-shrink: 0;
  padding: var(--space-1) var(--space-2);
  border: var(--bw-hair) solid var(--rule);
  border-radius: var(--r-pill);
  background: var(--bg-3);
  color: var(--fg);
  font-size: var(--fs-tiny);
  cursor: pointer;
}
.ds-247420 .vx-chip:hover { background: var(--bg-4, var(--bg-2)); border-color: var(--rule-strong); }
.ds-247420 .vx-chip-current { border-color: var(--accent); background: color-mix(in oklab, var(--accent) 16%, var(--bg-3)); }
.ds-247420 .vx-chip-live { border-color: var(--green-2); }
.ds-247420 .vx-chip-dot { width: 8px; height: 8px; border-radius: var(--r-pill); background: var(--fg-3); flex-shrink: 0; }
.ds-247420 .vx-chip-name { font-weight: var(--fw-medium); }
.ds-247420 .vx-chip-dur { font-family: var(--ff-mono); color: var(--fg-3); }
/* Joins the small-label voice above. Deliberate overrides: --green + weight 700
   because this is the "LIVE" badge on a session chip, same signal role as
   .cm-vs-label. */
.ds-247420 .vx-chip-tag {
  color: var(--green); font-weight: var(--fw-bold);
}

/* VoiceControls — vx-vc-* */
.ds-247420 .vx-vc {
  display: flex; align-items: center; gap: var(--space-1);
  padding: var(--space-1) var(--space-2);
  background: var(--bg-2);
  border-top: var(--bw-hair) solid var(--rule);
}
.ds-247420 .vx-vc-btn {
  width: 36px; height: 36px;
  display: inline-flex; align-items: center; justify-content: center;
  border: var(--bw-hair) solid var(--rule-strong);
  border-radius: var(--r-1);
  background: var(--bg-3); color: var(--fg);
  cursor: pointer; font-size: var(--fs-body); line-height: 1;
}
.ds-247420 .vx-vc-btn:hover { background: var(--bg-4, var(--bg-3)); }
.ds-247420 .vx-vc-on { background: var(--accent); color: var(--accent-fg); border-color: var(--accent); }
.ds-247420 .vx-vc-disabled, .ds-247420 .vx-vc-btn:disabled { opacity: .4; cursor: not-allowed; }
.ds-247420 .vx-vc-leave { margin-left: auto; background: var(--danger); color: var(--on-color); border-color: var(--danger); }
.ds-247420 .vx-vc-glyph { pointer-events: none; }

/* ThreadPanel — cm-thread-panel / cm-tp-* */
.ds-247420 .cm-thread-panel {
  display: flex; flex-direction: column;
  flex: 0 0 300px; width: 300px; min-width: 0;
  height: 100%; min-height: 0;
  background: var(--bg-2); color: var(--fg);
  border-left: var(--bw-hair) solid var(--rule);
}
@media (max-width: 900px) {
  .ds-247420 .cm-thread-panel {
    position: absolute;
    z-index: calc(var(--z-drawer) + 1);
    top: 0; right: 0; bottom: 0;
    height: auto;
    width: min(320px, 90vw);
    box-shadow: -2px 0 16px color-mix(in oklab, var(--ink) 30%, transparent);
  }
}
.ds-247420 .cm-tp-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--space-2); border-bottom: var(--bw-hair) solid var(--rule);
}
.ds-247420 .cm-tp-title { font-weight: var(--fw-bold); font-size: var(--fs-xs); }
.ds-247420 .cm-tp-head-actions { display: flex; gap: var(--space-1); }
.ds-247420 .cm-tp-new, .ds-247420 .cm-tp-close {
  border: none; background: transparent; color: var(--fg-2, var(--fg));
  cursor: pointer; font-size: var(--fs-body); width: 24px; height: 24px; border-radius: var(--r-1);
}
.ds-247420 .cm-tp-new:hover, .ds-247420 .cm-tp-close:hover { background: var(--bg-3); color: var(--fg); }
.ds-247420 .cm-tp-list { flex: 1; min-height: 0; overflow-y: auto; }
.ds-247420 .cm-tp-item {
  position: relative; /* anchors .cm-tp-dot to the row, not the panel */
  display: flex; flex-direction: column; gap: var(--space-hair);
  width: 100%; text-align: left; cursor: pointer;
  padding: var(--space-2); border: none; background: transparent;
  border-bottom: var(--bw-hair) solid var(--rule);
  color: var(--fg);
}
.ds-247420 .cm-tp-item:hover { background: var(--bg-3); }
.ds-247420 .cm-tp-item.is-active { background: var(--bg-3); box-shadow: inset 3px 0 0 var(--accent); }
.ds-247420 .cm-tp-item.is-unread .cm-tp-item-title { font-weight: var(--fw-bold); }
.ds-247420 .cm-tp-dot { position: absolute; left: 4px; top: 50%; transform: translateY(-50%); width: 8px; height: 8px; border-radius: 50%; background: var(--accent); }
.ds-247420 .cm-tp-item-title { font-size: var(--fs-xs); }
.ds-247420 .cm-tp-item-snippet { font-size: var(--fs-micro); color: var(--fg-2, var(--fg)); opacity: .8; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ds-247420 .cm-tp-item-meta { display: flex; gap: var(--space-2); font-size: var(--fs-micro); color: var(--fg-2, var(--fg)); opacity: .7; }
.ds-247420 .cm-tp-empty, .ds-247420 .cm-forum-empty, .ds-247420 .cm-channel-empty, .ds-247420 .cm-member-empty {
  display: flex; flex-direction: column; align-items: center; gap: var(--space-2);
  padding: var(--space-4) var(--space-3); text-align: center;
  color: var(--fg-2, var(--fg)); opacity: .7; font-size: var(--fs-sm);
}
.ds-247420 .cm-tp-empty svg, .ds-247420 .cm-forum-empty svg, .ds-247420 .cm-channel-empty svg, .ds-247420 .cm-member-empty svg { opacity: .6; }

/* Loading skeletons — cold-load placeholders for channel/member/thread/forum
   lists. Same .ds-skel shimmer primitive as sessions.js/files.js so every
   list surface in the kit uses one shimmer language, never a bare spinner. */
.ds-247420 .cm-channel-item-skeleton, .ds-247420 .cm-member-item-skeleton {
  display: flex; align-items: center; gap: var(--space-2);
  padding: var(--space-2); margin: var(--space-hair) var(--space-1);
}
.ds-247420 .cm-channel-item-skeleton .ds-skel-icon, .ds-247420 .cm-member-item-skeleton .ds-skel-icon {
  width: 20px; height: 20px; border-radius: var(--r-round, 50%); flex: none; background: var(--bg-3);
}
.ds-247420 .cm-channel-item-skeleton .ds-skel-title, .ds-247420 .cm-member-item-skeleton .ds-skel-title {
  height: 12px; width: 60%; border-radius: var(--r-1); background: var(--bg-3);
}
.ds-247420 .cm-list-item-skeleton {
  display: flex; flex-direction: column; gap: var(--space-1-75);
  padding: var(--space-2) var(--space-3); border-bottom: var(--bw-hair) solid var(--rule);
}
.ds-247420 .cm-list-item-skeleton .ds-skel-title { height: 12px; width: 55%; border-radius: var(--r-1); background: var(--bg-3); }
.ds-247420 .cm-list-item-skeleton .ds-skel-meta { height: 9px; width: 35%; border-radius: var(--r-1); background: var(--bg-3); }
/* Self-contained shimmer keyframe: community.css ships standalone (the
   community-app kit does not also load chat.css), so this cannot depend on
   chat.css's ds-skel-shimmer definition even though the visual language and
   token vocabulary match it exactly. */
@media (prefers-reduced-motion: no-preference) {
  .ds-247420 .cm-channel-item-skeleton .ds-skel-icon, .ds-247420 .cm-channel-item-skeleton .ds-skel-title, .ds-247420 .cm-member-item-skeleton .ds-skel-icon, .ds-247420 .cm-member-item-skeleton .ds-skel-title, .ds-247420 .cm-list-item-skeleton .ds-skel-title, .ds-247420 .cm-list-item-skeleton .ds-skel-meta {
    animation: cm-skel-shimmer 1.3s ease-in-out infinite;
    background: linear-gradient(90deg, var(--bg-3) 0%, var(--bg-2) 50%, var(--bg-3) 100%);
    background-size: 200% 100%;
  }
}
@keyframes cm-skel-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }

/* ForumView — cm-forum / cm-forum-* */
.ds-247420 .cm-forum { display: flex; flex-direction: column; height: 100%; min-height: 0; background: var(--bg-1, var(--bg-2)); color: var(--fg); }
.ds-247420 .cm-forum-toolbar { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; padding: var(--space-2); border-bottom: var(--bw-hair) solid var(--rule); }
.ds-247420 .cm-forum-search { flex: 1; padding: var(--space-1) var(--space-2); border: var(--bw-hair) solid var(--rule-strong); border-radius: var(--r-1); background: var(--bg-3); color: var(--fg); }
.ds-247420 .cm-forum-sort { padding: var(--space-1); border: var(--bw-hair) solid var(--rule-strong); border-radius: var(--r-1); background: var(--bg-3); color: var(--fg); }
.ds-247420 .cm-forum-new { padding: var(--space-1) var(--space-2); border: none; border-radius: var(--r-1); background: var(--accent); color: var(--accent-fg); cursor: pointer; }
.ds-247420 .cm-forum-list { flex: 1; min-height: 0; overflow-y: auto; }
.ds-247420 .cm-forum-item { display: flex; flex-direction: column; gap: var(--space-1); width: 100%; text-align: left; cursor: pointer; padding: var(--space-2) var(--space-3); border: none; background: transparent; border-bottom: var(--bw-hair) solid var(--rule); color: var(--fg); }
.ds-247420 .cm-forum-item:hover { background: var(--bg-3); }
.ds-247420 .cm-forum-item-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); }
.ds-247420 .cm-forum-item-title { font-weight: var(--fw-bold); font-size: var(--fs-xs); }
.ds-247420 .cm-forum-item-replies { font-size: var(--fs-micro); color: var(--fg-2, var(--fg)); opacity: .8; white-space: nowrap; }
.ds-247420 .cm-forum-item-snippet { font-size: var(--fs-micro); color: var(--fg-2, var(--fg)); opacity: .85; }
.ds-247420 .cm-forum-item-meta { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; font-size: var(--fs-micro); color: var(--fg-2, var(--fg)); opacity: .7; }
.ds-247420 .cm-forum-item-tags { display: inline-flex; gap: var(--space-1); }
/* 1px vertical is OFF-SCALE ON PURPOSE — the tag is an inline chip inside the
   forum meta row; --space-hair (2px) tips it past the row height. */
.ds-247420 .cm-forum-tag { padding: 1px var(--space-1-75); border-radius: var(--r-1); background: var(--bg-3); border: var(--bw-hair) solid var(--rule); }

/* PageView — cm-page / cm-page-* */
.ds-247420 .cm-page { display: flex; flex-direction: column; height: 100%; min-height: 0; overflow-y: auto; background: var(--bg-1, var(--bg-2)); color: var(--fg); }
.ds-247420 .cm-page-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); padding: var(--space-3); border-bottom: var(--bw-hair) solid var(--rule); }
.ds-247420 .cm-page-head-title { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
.ds-247420 .cm-page-title { margin: 0; font-size: var(--fs-xl); font-weight: var(--fw-bold); }
.ds-247420 .cm-page-meta { display: flex; align-items: center; gap: var(--space-2); font-size: var(--fs-xs); color: var(--fg-2, var(--fg)); }
.ds-247420 .cm-page-author { font-weight: var(--fw-medium); }
.ds-247420 .cm-page-time { opacity: .75; }
.ds-247420 .cm-page-edit { padding: var(--space-1) var(--space-2); border: var(--bw-hair) solid var(--rule-strong); border-radius: var(--r-1); background: var(--bg-3); color: var(--fg); cursor: pointer; }
.ds-247420 .cm-page-body { padding: var(--space-3); line-height: 1.6; }
.ds-247420 .cm-page-empty { color: var(--fg-2, var(--fg)); opacity: .82; }

/* ============================================================
   freddie pages — layout for FREDDIE_PAGES renderers.
   Scoped under .ds-247420 by the build prefixer. Tokens only.
   ============================================================ */
.ds-247420 .fd-page-root { display: block; height: 100%; }
.ds-247420 .fd-page-inner { display: flex; flex-direction: column; gap: var(--space-3, 16px); padding: var(--space-3, 16px); max-width: 1100px; }
.ds-247420 .fd-page-inner > * { min-width: 0; }
.ds-247420 .fd-loading { display: flex; align-items: center; gap: var(--space-2, 10px); padding: var(--space-4, 24px); color: var(--fg-2, var(--fg)); }
/* Base layout shared with .ds-empty-state (primitives.css); overrides padding
   (--space-5, denser than the shared --space-6) and color for this context. */
.ds-247420 .fd-empty { padding: var(--space-5, 32px) var(--space-3, 16px); color: var(--fg-2, var(--fg)); }
/* 28px is an ICON size (empty-state mark), not a type tier — the --fs-* ladder
   tops out at --fs-xl (21px) before every remaining rung becomes a fluid
   heading clamp, so neither substitutes without visibly resizing the mark.
   --fs-2xl bridges that gap as a fixed 28px token. */
.ds-247420 .fd-empty-glyph { font-size: var(--fs-2xl); opacity: .55; line-height: 1; }
.ds-247420 .fd-pre { margin: 0; padding: var(--space-2, 10px); background: var(--bg-3, var(--panel-bg-2)); border: var(--bw-hair, 1px) solid var(--rule, var(--panel-accent)); border-radius: var(--r-1, 8px); overflow: auto; max-height: 460px; white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; font-family: var(--font-mono, ui-monospace, monospace); font-size: var(--fs-micro); line-height: 1.5; }
.ds-247420 .fd-skill-body { max-height: 320px; }
.ds-247420 .fd-row-actions { display: inline-flex; gap: var(--space-1, 6px); align-items: center; }
.ds-247420 .fd-chat { display: flex; flex-direction: column; gap: var(--space-2, 10px); height: 100%; min-height: 0; }
.ds-247420 .fd-chat-thread { flex: 1 1 auto; min-height: 240px; overflow-y: auto; display: flex; flex-direction: column; gap: var(--space-2, 10px); padding: var(--space-2, 10px); }
.ds-247420 .fd-page-error { white-space: pre-wrap; overflow-wrap: anywhere; }
/* Visually-hidden polite live region — announces async busy/done to SR users. */
/* margin:-1px is OFF-SCALE ON PURPOSE — the canonical visually-hidden idiom;
   the negative margin cancels the 1px box so the live region contributes zero
   layout. A --space-* rung would reintroduce a measurable gap. */
.ds-247420 .fd-sr-live { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
/* page-level responsive: tighten padding on narrow viewports */
@media (max-width: 640px) {
  .ds-247420 .fd-page-inner { padding: var(--space-2, 10px); gap: var(--space-2, 10px); }
}

/* chat.css */
/* chat.css — chat-specific styles, including the base .chat-* rules used by
   Chat()/ChatMessage() (chat/threads.js, chat/message.js): .chat, .chat-head,
   .chat-thread, .chat-msg, .chat-avatar, .chat-bubble, .chat-stack,
   .chat-empty. This file also carries the AgentChat kit styles (the reusable
   multi-agent orchestration chat surface, .agentchat-*). */

/* Base chat surface — the default (non-flat) messenger layout: left/right
   avatar-disc + colored-bubble turns. .chat-msg-flat further down overrides
   these to the claude.ai/code full-width turn style; these are the rules it
   overrides FROM, and were previously entirely undefined, so the default
   layout rendered on raw flex/browser defaults (unstyled bubble shape, no
   avatar depth, no defined thread padding) until a Chat() consumer supplied
   its own colliding class names. */
.ds-247420 .chat {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}
.ds-247420 .chat-head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2-75) var(--space-3);
  border-bottom: 1px solid var(--panel-3, color-mix(in oklab, var(--fg) 10%, transparent));
  flex-shrink: 0;
}
.ds-247420 .chat-head .ds-chat-title { font-size: var(--fs-sm); font-weight: var(--fw-medium); margin: 0; }
.ds-247420 .chat-head .sub { font-size: var(--fs-tiny); color: var(--fg-3); }
.ds-247420 .chat-head .spread { flex: 1; }
.ds-247420 .chat-thread {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-3) var(--space-4);
  /* A short thread must not stretch its one message across the whole
     viewport height — pack content to the bottom (where the composer is)
     instead of centering it in a sea of empty space above and below. */
  justify-content: flex-end;
}
.ds-247420 .chat-empty {
  margin: auto;
  max-width: var(--measure-narrow, 60ch);
  text-align: center;
  padding: var(--space-5) var(--space-3);
  color: var(--fg-3);
}
.ds-247420 .chat-empty-title { font-size: var(--fs-lg); font-weight: var(--fw-medium); color: var(--fg-2); margin: 0 0 var(--space-1); }
.ds-247420 .chat-empty-sub { font-size: var(--fs-sm); margin: 0; }
.ds-247420 .chat-msg {
  display: flex;
  gap: var(--space-2);
  align-items: flex-start;
  max-width: 100%;
}
.ds-247420 .chat-msg.you { flex-direction: row-reverse; }
.ds-247420 .chat-msg.centered { justify-content: center; text-align: center; }
.ds-247420 .chat-avatar {
  width: 32px;
  height: 32px;
  flex-shrink: 0;
  border-radius: 50%;
  background: var(--bg-3);
  color: var(--fg);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: var(--fs-tiny);
  font-weight: var(--fw-medium);
  /* Depth over a flat solid-color disc: a hairline inset ring plus the same
     soft shadow --panel-shadow uses elsewhere, so avatars read as a real
     surface element instead of a wireframe placeholder. */
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--fg) 10%, transparent), 0 1px 2px color-mix(in oklab, var(--fg) 10%, transparent);
}
.ds-247420 .chat-stack {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  max-width: 70%;
  min-width: 0;
}
.ds-247420 .chat-msg.you .chat-stack { align-items: flex-end; }
.ds-247420 .chat-bubble {
  padding: var(--space-2) var(--space-2-75);
  background: var(--bg-2);
  color: var(--fg);
  /* r-2, not a pill — a pill-capsule bubble read as a generic chat-template
     shape inconsistent with every other rounded element in the app (which
     all use the r-0..r-3 scale, never 999px). */
  border-radius: var(--r-2);
  line-height: var(--lh-base, 1.5);
  word-break: break-word;
  font-size: var(--fs-sm);
}
.ds-247420 .chat-msg.you .chat-bubble { background: var(--accent); color: var(--accent-fg); }
.ds-247420 .chat-meta { display: flex; gap: var(--space-1-5); font-size: var(--fs-nano); color: var(--fg-3); padding: 0 var(--space-1); }

.ds-247420 .agentchat {
  display: flex;
  flex-direction: column;
  min-height: 0;
  flex: 1;
  gap: var(--space-2, 8px);
  /* The chat tab opts out of .ws-main's padding (mainFlush) so the thread can
     manage its own --measure centering, but that left every direct child -
     controls row, heading, empty-state, composer - flush against the column
     edge with zero gutter (the crumb bar above keeps its own padding, so the
     content below visibly touched the edge the crumb text did not). A
     uniform inline gutter here restores breathing room for every row without
     disturbing --measure centering (it centers within this padded box). */
  padding-inline: var(--space-5);
}

/* agent + model picker, new/stop, status — wraps on narrow widths */
.ds-247420 .agentchat-controls {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2, 8px);
}
.ds-247420 .agentchat-controls .select,
.ds-247420 .agentchat-controls [role="combobox"] { min-width: 130px; max-width: 240px; }
.ds-247420 .agentchat-controls .ds-select { min-width: 130px; max-width: 240px; }
/* One shared control metric for the most-seen chrome row (mirrors the
   .ds-dash-toolbar precedent): selects, buttons, export acts all 32px / r-1. */
/* The 28px right pad is OFF-SCALE ON PURPOSE: it is not rhythm but clearance
   for the native select's dropdown arrow, which the UA draws at a fixed inset.
   Snapping to --space-5 (32px) or --space-4 (24px) either wastes a column of
   text width or lets long option labels run under the arrow. */
.ds-247420 .agentchat-controls .ds-select { min-height: 32px; padding: var(--space-1) 28px var(--space-1) var(--space-2-5); font-size: var(--fs-sm); border-radius: var(--r-1); }
.ds-247420 .agentchat-controls .btn, .ds-247420 .agentchat-controls .btn-primary { padding: var(--space-1-5) var(--space-2-75); min-height: 32px; border-radius: var(--r-1); font-weight: 500; }
@media (pointer: coarse) {
  .ds-247420 .agentchat-controls .ds-select, .ds-247420 .agentchat-controls .btn, .ds-247420 .agentchat-controls .btn-primary { min-height: 44px; }
}

/* Narrow viewports: let the agent/model selects share the row two-up, then
   stack full-width on the smallest screens, and drop the status to its own
   line so nothing is squeezed below a usable tap size. */
@media (max-width: 640px) {
  .ds-247420 .agentchat-controls .select,
  .ds-247420 .agentchat-controls .ds-select,
  .ds-247420 .agentchat-controls [role="combobox"] { flex: 1 1 45%; max-width: none; }
  .ds-247420 .agentchat-status { margin-left: 0; flex-basis: 100%; }
}
@media (max-width: 360px) {
  .ds-247420 .agentchat-controls .select,
  .ds-247420 .agentchat-controls .ds-select,
  .ds-247420 .agentchat-controls [role="combobox"] { flex-basis: 100%; }
}

/* The .4em/.5em gaps on this status line, .agentchat-head below, and the
   narrow-viewport .agentchat-controls override are em-based ON PURPOSE, not
   un-migrated literals: each is the gap between a label and its own inline
   text/disc, so it must track that element's font-size (these rows are scaled
   by font-size alone at narrow widths). A --space-* px rung would hold the gap
   fixed while the type around it shrank. */
.ds-247420 .agentchat-status {
  display: inline-flex;
  align-items: center;
  gap: .4em;
  white-space: nowrap;
  margin-left: auto;
  font-size: var(--fs-tiny);
  color: var(--fg-3);
}
/* status disc — a CSS-drawn dot, not a glyph; pulses while streaming */
.ds-247420 .agentchat-status .status-dot-disc {
  flex: none; width: 8px; height: 8px; border-radius: 50%;
  background: var(--fg-3);
}
.ds-247420 .agentchat-status .status-dot-disc.status-dot-live {
  background: var(--accent);
  animation: agentchat-pulse 2s infinite;
}
@keyframes agentchat-pulse {
  0%   { box-shadow: 0 0 0 0 color-mix(in oklab, var(--accent) 50%, transparent); }
  70%  { box-shadow: 0 0 0 6px color-mix(in oklab, var(--accent) 0%, transparent); }
  100% { box-shadow: 0 0 0 0 color-mix(in oklab, var(--accent) 0%, transparent); }
}
@media (prefers-reduced-motion: reduce) {
  .ds-247420 .agentchat-status .status-dot-disc { animation: none !important; }
}

/* working-directory bar */
.ds-247420 .agentchat-cwd {
  display: flex;
  align-items: center;
  gap: var(--space-2, 8px);
  font-size: var(--fs-tiny);
  color: var(--fg-3);
  flex-wrap: wrap;
}
.ds-247420 .agentchat-cwd-text {
  font-family: var(--ff-mono);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* 60ch overflows narrow phones; clamp to the viewport so the path ellipsizes
     instead of pushing the row wider than the screen. */
  max-width: min(60ch, 60vw);
}
.ds-247420 .agentchat-cwd-btn {
  background: none;
  border: 1px solid var(--rule);
  color: inherit;
  border-radius: var(--r-1);
  padding: var(--space-hair) var(--space-2);
  cursor: pointer;
  font: inherit;
}
.ds-247420 .agentchat-cwd-btn:hover { border-color: var(--accent); }
.ds-247420 .agentchat-cwd-btn:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
.ds-247420 .agentchat-cwd-input {
  flex: 1;
  min-width: 12ch;
  background: var(--bg-2);
  border: var(--bw-hair) solid var(--rule);
  color: var(--fg);
  border-radius: var(--r-1);
  padding: var(--space-1) var(--space-2);
  font: inherit;
}
.ds-247420 .agentchat-cwd-input:focus-visible { outline: none; box-shadow: var(--focus-ring-inset); }
.ds-247420 .agentchat-cwd-input[aria-invalid='true'] { border-color: var(--flame); box-shadow: inset 0 0 0 var(--bw-hair) var(--flame); }
.ds-247420 .agentchat-cwd-input[aria-busy='true'] { border-color: var(--fg-3); box-shadow: inset 0 0 0 var(--bw-hair) var(--fg-3); }

/* cwd practicality upgrade: editing mode stacks the input row plus any
   optional roots/recent/browse rows the host wires, instead of a single flat
   row - each sub-row wraps independently at narrow widths. */
.ds-247420 .agentchat-cwd-editing { flex-direction: column; align-items: stretch; }
.ds-247420 .agentchat-cwd-row { display: flex; align-items: center; gap: var(--space-2, 8px); flex-wrap: wrap; }
.ds-247420 .agentchat-cwd-roots, .ds-247420 .agentchat-cwd-recent {
  display: flex; align-items: center; gap: var(--space-1-75); flex-wrap: wrap;
}
.ds-247420 .agentchat-cwd-recent-label { font-size: var(--fs-tiny); color: var(--fg-3); }
.ds-247420 .agentchat-cwd-chip {
  background: var(--bg-2); border: var(--bw-hair) solid var(--rule); color: var(--fg-2);
  border-radius: var(--r-pill); padding: var(--space-half) var(--space-2-5); font: inherit; font-size: var(--fs-tiny);
  cursor: pointer;
}
.ds-247420 .agentchat-cwd-chip:hover { border-color: var(--accent); color: var(--fg); }
.ds-247420 .agentchat-cwd-chip:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
.ds-247420 .agentchat-cwd-browse {
  border: var(--bw-hair) solid var(--rule); border-radius: var(--r-1);
  padding: var(--space-2, 8px); background: var(--bg-2);
  display: flex; flex-direction: column; gap: var(--space-1-75);
  max-height: 220px;
}
.ds-247420 .agentchat-cwd-browse-list { overflow-y: auto; display: flex; flex-direction: column; max-height: 160px; }
.ds-247420 .agentchat-cwd-browse-item {
  background: none; border: none; color: var(--fg-2); text-align: left;
  padding: var(--space-1-75) var(--space-2); border-radius: var(--r-1); font: inherit; font-size: var(--fs-tiny);
  cursor: pointer;
}
.ds-247420 .agentchat-cwd-browse-item:hover, .ds-247420 .agentchat-cwd-browse-item:focus-visible { background: var(--bg-3, var(--bg)); color: var(--fg); }
.ds-247420 .agentchat-cwd-browse-loading, .ds-247420 .agentchat-cwd-browse-empty { padding: var(--space-1-75) var(--space-2); font-size: var(--fs-tiny); color: var(--fg-3); }

/* head + thread */
.ds-247420 .agentchat-head {
  display: flex;
  align-items: baseline;
  gap: .5em;
}
.ds-247420 .agentchat-title { font-size: 1em; margin: 0; }
.ds-247420 .agentchat-sub { font-size: var(--fs-tiny); color: var(--fg-3); }
/* Optional inline content viewer beside the thread (sidePanel prop) - a
   docstudio-cue addition (its chat view keeps a document/PDF preview open
   next to the conversation rather than in a separate tab). SplitPanel (an
   existing kit primitive) owns the resizable layout; these rules only shape
   the two panes' internal structure. */
.ds-247420 .agentchat.has-side-panel { gap: 0; }
.ds-247420 .agentchat.has-side-panel > .ds-ep-split { flex: 1; min-height: 0; }
.ds-247420 .agentchat-main-col { display: flex; flex-direction: column; min-height: 0; height: 100%; gap: var(--space-2, 8px); }
.ds-247420 .agentchat-side-panel { display: flex; flex-direction: column; height: 100%; min-height: 0; border-left: var(--bw-hair) solid var(--bg-3); }
.ds-247420 .agentchat-side-panel-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--space-2, 8px) var(--space-3, 16px); border-bottom: var(--bw-hair) solid var(--bg-3);
  flex: 0 0 auto;
}
.ds-247420 .agentchat-side-panel-title { font-size: var(--fs-tiny); font-weight: 600; color: var(--fg-2); text-transform: uppercase; letter-spacing: var(--tr-label); }
.ds-247420 .agentchat-side-panel-close {
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: none; color: var(--fg-3); cursor: pointer;
  padding: var(--space-1); border-radius: var(--r-1);
}
.ds-247420 .agentchat-side-panel-close:hover { background: var(--bg-2); color: var(--fg); }
.ds-247420 .agentchat-side-panel-close:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
.ds-247420 .agentchat-side-panel-body { flex: 1; min-height: 0; overflow: auto; }
@media (max-width: 900px) {
  /* A resizable side-by-side split is impractical below tablet width - stack
     the preview under the thread instead of horizontally squeezing both. */
  .ds-247420 .agentchat.has-side-panel > .ds-ep-split.horiz { flex-direction: column; }
  .ds-247420 .agentchat-side-panel { border-left: none; border-top: var(--bw-hair) solid var(--bg-3); max-height: 45vh; }
}
/* Thread wrapper hosts the absolutely-positioned jump-to-latest button. */
.ds-247420 .agentchat-thread-wrap { position: relative; flex: 1; min-height: 0; display: flex; flex-direction: column; }
.ds-247420 .agentchat-thread {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: calc(var(--space-2, 8px) * var(--density, 1));
}

/* Jump-to-latest: hidden until the thread scroll listener adds .show. */
.ds-247420 .agentchat-jump {
  position: absolute; right: 16px; bottom: 12px; z-index: var(--z-raised);
  display: none; align-items: center; gap: .35em;
  /* em, not --space-*: this pill must scale with its own --fs-tiny text so the
     capsule stays proportional when a host overrides the chat font size. A px
     rung would fix the padding while the label grew/shrank inside it. */
  padding: .4em .8em; font: inherit; font-size: var(--fs-tiny); cursor: pointer;
  border-radius: var(--r-pill); border: var(--bw-hair) solid var(--rule);
  background: var(--bg-2); color: var(--fg-2, var(--fg));
  box-shadow: 0 2px 8px color-mix(in srgb, var(--fg) 14%, transparent);
}
.ds-247420 .agentchat-jump.show { display: inline-flex; }
.ds-247420 .agentchat-jump:hover { background: var(--bg-3); }
.ds-247420 .agentchat-jump:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }

/* Streaming caret: a thin blinking bar at the live edge of an assistant turn. */
.ds-247420 .chat-stream-caret {
  /* margin-left:1px is OFF-SCALE ON PURPOSE — a hairline optical nudge that
     separates the caret bar from the last glyph without reading as a space
     character. --space-hair (2px) is already wide enough to look like one. */
  display: inline-block; width: 2px; height: 1.05em; margin-left: 1px;
  vertical-align: text-bottom; background: var(--accent, var(--fg));
  animation: chat-caret-blink 1s step-end infinite;
}
@keyframes chat-caret-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .ds-247420 .chat-stream-caret { animation: none; opacity: .8; } }

/* Per-message hover-revealed action row (copy / retry / edit). */
.ds-247420 .chat-msg-actions { display: flex; gap: var(--space-hair); margin-top: var(--space-half); opacity: 0; transition: opacity var(--dur-snap, .12s) var(--ease, ease); }
.ds-247420 .chat-msg:hover .chat-msg-actions, .ds-247420 .chat-msg:focus-within .chat-msg-actions { opacity: 1; }
.ds-247420 .chat-msg-action {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-1);
  min-width: 26px; height: 26px; padding: 0 var(--space-1-75); cursor: pointer;
  border: none; border-radius: var(--r-1, 8px); background: none; color: var(--fg-3);
  font-family: var(--ff-body); font-size: var(--fs-tiny);
}
.ds-247420 .chat-msg-action-label { font-size: var(--fs-tiny); }
.ds-247420 .chat-msg-action:hover { background: var(--bg-2); color: var(--fg); }
.ds-247420 .chat-msg-action:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: 1px; opacity: 1; }
.ds-247420 .chat-msg-action.is-copied { color: var(--accent-ink); opacity: 1; }

/* Empty state: a fresh thread is an invitation, not a void.
   The em-based gap/padding through this block and the suggestion chips below
   are OFF the --space-* px scale ON PURPOSE: the empty state is typeset copy
   whose rhythm must track its own font-size (hosts scale this panel down on
   narrow viewports via font-size alone). Fixed px rungs here would decouple
   the chip padding from the label and break the capsule proportions. */
.ds-247420 .agentchat-empty {
  margin: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .4em;
  text-align: center;
  padding: var(--space-6, 32px) var(--space-4, 16px);
  color: var(--fg-3);
  max-width: 46ch;
}
.ds-247420 .agentchat-empty-title { margin: 0; font-size: 1.05em; color: var(--fg-2, var(--fg)); }
.ds-247420 .agentchat-empty-sub { margin: 0; font-size: var(--fs-sm); }
.ds-247420 .agentchat-empty-suggestions, .ds-247420 .chat-empty-suggestions {
  display: flex;
  flex-wrap: wrap;
  gap: .5em;
  justify-content: center;
  margin-top: .5em;
}
.ds-247420 .agentchat-empty-suggestion, .ds-247420 .chat-empty-suggestion {
  cursor: pointer;
  font: inherit;
  font-size: var(--fs-tiny);
  padding: .4em .8em;
  border-radius: var(--r-pill);
  border: 1px solid color-mix(in srgb, var(--fg) 16%, transparent);
  background: color-mix(in srgb, var(--fg) 5%, transparent);
  color: var(--fg-2, var(--fg));
}
.ds-247420 .agentchat-empty-suggestion:hover, .ds-247420 .chat-empty-suggestion:hover { background: color-mix(in srgb, var(--accent, var(--fg)) 12%, transparent); }
.ds-247420 .agentchat-empty-suggestion:focus-visible, .ds-247420 .chat-empty-suggestion:focus-visible { outline: 2px solid var(--accent, var(--fg)); outline-offset: var(--focus-offset); }
/* Follow-up chips (contextual, based on the last turn) vs empty-state seed
   chips (generic starter ideas) previously shared identical DOM shape with no
   visual signal distinguishing them - a dashed border reads as "derived from
   context" without introducing a new color/shape language. */
.ds-247420 .agentchat-followup { border-style: dashed; }

/* Guided install path in the empty state: copy line + monospaced command rows
   (each with its own copy button) + a recheck button. No animation. */
.ds-247420 .agentchat-install {
  margin-top: var(--space-4, 16px);
  display: flex;
  flex-direction: column;
  gap: var(--space-2, 8px);
  text-align: left;
  width: 100%;
}
.ds-247420 .agentchat-install-text { margin: 0; font-size: var(--fs-sm); color: var(--fg-3); }
.ds-247420 .agentchat-install-list { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: var(--space-1, 4px); }
.ds-247420 .agentchat-install-row {
  display: flex; align-items: center; gap: var(--space-2, 8px);
  padding: var(--space-1, 4px) var(--space-2, 8px);
  border: var(--bw-hair) solid var(--rule);
  border-radius: var(--r-1, 8px);
  background: var(--bg-2);
}
.ds-247420 .agentchat-install-agent { flex: 0 0 auto; font-size: var(--fs-tiny); color: var(--fg-3); min-width: 8ch; }
.ds-247420 .agentchat-install-cmd {
  flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-family: var(--ff-mono); font-size: var(--fs-sm); color: var(--fg-2, var(--fg));
}
.ds-247420 .agentchat-install-copy {
  flex: 0 0 auto; cursor: pointer; padding: var(--space-hair) var(--space-2); min-height: 24px;
  border: var(--bw-hair) solid var(--bg-3); border-radius: var(--r-1, 8px);
  background: none; color: var(--fg-3);
  font-family: var(--ff-body); font-size: var(--fs-tiny);
}
.ds-247420 .agentchat-install-copy:hover { background: var(--bg-3); color: var(--fg); }
.ds-247420 .agentchat-install-copy:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: 1px; }
.ds-247420 .agentchat-install-actions { display: flex; justify-content: flex-start; }

/* Host-supplied transcript export actions riding the controls row. */
.ds-247420 .agentchat-export-act {
  cursor: pointer; padding: var(--space-1) var(--space-2-5); min-height: 32px;
  border: var(--bw-hair) solid var(--bg-3); border-radius: var(--r-1, 8px);
  background: none; color: var(--fg-3);
  font-family: var(--ff-body); font-size: var(--fs-tiny);
}
.ds-247420 .agentchat-export-act:hover { background: var(--bg-2); color: var(--fg); }
.ds-247420 .agentchat-export-act:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: 1px; }

/* "working" tail: a long silent tool call shouldn't read as frozen.
   em gap/padding here and on .chat-thinking below is deliberate — both tails
   are inline status text sized off --fs-tiny, and their inset must shrink with
   the type, not sit on a fixed px rung. */
.ds-247420 .agentchat-working {
  display: flex;
  align-items: center;
  gap: .5em;
  padding: .3em .2em;
  color: var(--fg-3);
  font-size: var(--fs-tiny);
}
.ds-247420 .agentchat-working-text { color: var(--fg-3); }
/* Self-contained dots (chat-thinking-dots lives in a kit sheet not bundled
   into this DS distribution, so the agentchat tail draws its own). */
.ds-247420 .agentchat-working .chat-thinking-dots { display: inline-flex; gap: var(--space-half); }
.ds-247420 .agentchat-working .chat-thinking-dots span {
  width: 5px; height: 5px; border-radius: 50%;
  background: var(--fg); opacity: .4;
  animation: agentchat-dot-bounce 1.2s infinite ease-in-out;
}
.ds-247420 .agentchat-working .chat-thinking-dots span:nth-child(2) { animation-delay: .15s; }
.ds-247420 .agentchat-working .chat-thinking-dots span:nth-child(3) { animation-delay: .3s; }
@keyframes agentchat-dot-bounce {
  0%, 80%, 100% { transform: scale(.6); opacity: .3; }
  40% { transform: scale(1); opacity: .9; }
}
@media (prefers-reduced-motion: reduce) {
  .ds-247420 .agentchat-working .chat-thinking-dots span { animation: none; opacity: .7; }
}
/* Standalone thinking part (ThinkingNode renders .chat-bubble.chat-thinking with
   .chat-thinking-dots + .chat-thinking-text OUTSIDE the .agentchat-working tail).
   Without these base rules the dots are invisible empty spans and the text has
   no muted tone. Reuse the agentchat-dot-bounce keyframe. */
.ds-247420 .chat-thinking { display: flex; align-items: center; gap: .5em; color: var(--fg-3); font-size: var(--fs-tiny); background: none; padding: .3em .2em; }
.ds-247420 .chat-thinking-text { color: var(--fg-3); }
.ds-247420 .chat-thinking-dots { display: inline-flex; gap: var(--space-half); }
.ds-247420 .chat-thinking-dots span {
  width: 5px; height: 5px; border-radius: 50%;
  background: var(--fg); opacity: .4;
  animation: agentchat-dot-bounce 1.2s infinite ease-in-out;
}
.ds-247420 .chat-thinking-dots span:nth-child(2) { animation-delay: .15s; }
.ds-247420 .chat-thinking-dots span:nth-child(3) { animation-delay: .3s; }
@media (prefers-reduced-motion: reduce) {
  .ds-247420 .chat-thinking-dots span { animation: none; opacity: .7; }
}

/* Responsive: the control cluster + cwd bar must stay usable on phones/tablets.
   The cwd path can't eat the row beside its label + buttons on a narrow screen. */
.ds-247420 .agentchat-controls { flex-wrap: wrap; }
.ds-247420 .agentchat-cwd-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: min(60ch, 60vw); }
@media (max-width: 900px) {
  .ds-247420 .agentchat-controls .ds-select { min-width: 110px; max-width: 180px; }
}
@media (max-width: 640px) {
  .ds-247420 .agentchat-cwd-text { max-width: 42vw; }
  .ds-247420 .agentchat-controls { gap: .4em; }
}
/* 360px: buttons fill full width so they remain tappable at the narrowest phones */
@media (max-width: 400px) {
  .ds-247420 .agentchat-controls .btn, .ds-247420 .agentchat-controls .btn-primary, .ds-247420 .agentchat-controls button { flex: 1 1 100%; }
}
/* Coarse-pointer (touch): raise cwd button to 44px minimum tap target */
@media (pointer: coarse) {
  .ds-247420 .agentchat-cwd-btn, .ds-247420 .agentchat-cwd-chip, .ds-247420 .agentchat-cwd-browse-item { min-height: 44px; }
}

/* ----------------------------------------------------------------------------
   Global status disc — a CSS-drawn status indicator (no text glyph) usable
   outside .agentchat-status (session rows, dashboard cards). Tone via modifier.
---------------------------------------------------------------------------- */
/* Single canonical status disc — one source for live/error/connecting/stale
   everywhere (crumb dot, conversation row, dashboard card, composer status).
   The app's index.html disc/pulse override is deleted in favour of this. Only
   the -live disc pulses; -stale is deliberately static (a stuck session must NOT
   read as busy). Pulse colour is derived from --accent via color-mix so it
   tracks the theme instead of a hardcoded rgba. */
.ds-247420 .status-dot-disc {
  display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  background: var(--fg-3); flex: 0 0 auto;
}
/* Each variant carries a non-colour channel (fill vs halo vs hollow ring) so the
   states stay distinguishable for colour-blind users and when stale/connecting
   would otherwise share a hue. Mirrors the rail-tone shape differentiation. */
/* One canonical stale tone (disc + word + rail share it) — a muted grey-warn,
   deliberately desaturated so it is NOT the saturated --amber used by
   connecting/connecting-stream (which would re-collide hues).

   The mix is grey-dominant, NOT warn-dominant: at 70% --warn this measured
   1.09:1 against --bg in dark theme (and 1.28:1 on a panel) as the `idle` word
   in .ds-dash-status and .seg.is-idle — text that is present but effectively
   unreadable. --fg-3 is the tier-3 TEXT tone and already carries a legible
   contrast, so leading with it keeps the warn hue as a tint while restoring
   readability. Measured after the change in a live browser. */
.ds-247420 { --stale: color-mix(in oklab, var(--fg-3) 78%, var(--warn)); }
/* Live: a persistent faint concentric ring (resting shape channel, independent
   of motion) with the pulse layered on top — so even at the pulse trough the
   disc reads as solid-fill + ring, never a bare solid that aliases stale. */
.ds-247420 .status-dot-disc.status-dot-live { background: var(--accent-ink); box-shadow: 0 0 0 2px color-mix(in oklab, var(--accent-ink) 30%, transparent); animation: status-disc-pulse 1.8s ease-in-out infinite; }
.ds-247420 .status-dot-disc.status-dot-error { background: var(--flame); box-shadow: 0 0 0 1.5px color-mix(in oklab, var(--flame) 38%, transparent); }
.ds-247420 .status-dot-disc.status-dot-connecting { background: transparent; box-shadow: inset 0 0 0 2px var(--amber); }
/* Stale: a muted ring with a hollow centre — silhouette distinct from live's
   filled disc and connecting's amber hollow ring (hue + inset core). */
.ds-247420 .status-dot-disc.status-dot-stale { background: transparent; box-shadow: inset 0 0 0 1px var(--stale), inset 0 0 0 3px var(--bg); }
@keyframes status-disc-pulse {
  /* First shadow is the persistent resting ring; second is the animated halo —
     the ring is always present so the trough never collapses to a bare solid. */
  0%, 100% { box-shadow: 0 0 0 2px color-mix(in oklab, var(--accent) 30%, transparent), 0 0 0 2px color-mix(in oklab, var(--accent) 55%, transparent); }
  50%      { box-shadow: 0 0 0 2px color-mix(in oklab, var(--accent) 30%, transparent), 0 0 0 6px color-mix(in oklab, var(--accent) 0%, transparent); }
}
@media (prefers-reduced-motion: reduce) {
  /* The base rule already carries the persistent ring; just stop the pulse. */
  .ds-247420 .status-dot-disc.status-dot-live { animation: none; }
}

/* ----------------------------------------------------------------------------
   ConversationList — left-rail "Chats" column.
---------------------------------------------------------------------------- */
.ds-247420 .ds-sessions { display: flex; flex-direction: column; min-height: 0; height: 100%; }
/* agentgui's search-results export row sits as a sibling ABOVE ConversationList
   (not threaded through any of its props) - the wrapper must replicate the
   same flex-column layout .ds-sessions itself uses so ConversationList's own
   internal scroll region isn't broken by an extra non-flex ancestor. */
.ds-247420 .agentgui-search-rail-wrap { display: flex; flex-direction: column; min-height: 0; height: 100%; }
.ds-247420 .agentgui-search-export-row { flex: 0 0 auto; padding: var(--space-2) var(--space-3) 0; }
/* One row: quiet icon new-chat beside the search. The list's new-chat must not
   repeat the rail's primary CTA - two identical green buttons on screen at
   once read as a layout mistake, and the rail action already owns 'new'. */
.ds-247420 .ds-session-head { flex: 0 0 auto; display: flex; flex-direction: row; align-items: center; gap: var(--space-2); padding: var(--space-3); border-bottom: var(--bw-hair) solid var(--bg-3); }
.ds-247420 .ds-session-new {
  display: inline-flex; align-items: center; justify-content: center; gap: 0;
  flex: 0 0 auto; order: 2;
  width: 36px; min-height: 36px; padding: 0;
  background: transparent; color: var(--fg-2);
  border: var(--bw-hair) solid var(--rule);
  border-radius: var(--r-1); cursor: pointer; font-family: var(--ff-body);
  font-size: var(--fs-sm); font-weight: 500;
}
.ds-247420 .ds-session-new > span { display: none; }
.ds-247420 .ds-session-new:hover { background: var(--bg-2); color: var(--fg); }
/* Rail filter uses the shared .ds-search-input primitive (provides bg/border/
   radius/focus-ring); only the rail layout + touch floor live here. */
.ds-247420 .ds-session-head .ds-search-input { order: 1; flex: 1 1 auto; min-width: 0; min-height: 36px; padding: 0 var(--space-2-5); }
.ds-247420 .ds-session-new:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
/* Touch floor (must FOLLOW the base rules - same specificity, order decides). */
@media (pointer: coarse) {
  .ds-247420 .ds-session-new { width: 44px; min-height: 44px; }
  .ds-247420 .ds-session-head .ds-search-input { min-height: 44px; }
}
.ds-247420 .ds-session-list, .ds-247420 .ds-session-groups { flex: 1; min-height: 0; overflow-y: auto; padding: var(--space-2); }
.ds-247420 .ds-session-loadmore {
  width: 100%; margin-top: var(--space-1); padding: var(--space-2) var(--space-2-75);
  font-size: var(--fs-sm); font-family: inherit; color: var(--fg-3);
  background: none; border: var(--bw-hair) dashed var(--rule); border-radius: var(--r-1);
  cursor: pointer; text-align: center;
}
.ds-247420 .ds-session-loadmore:hover { color: var(--fg); border-color: var(--fg-3); }
.ds-247420 .ds-session-loadmore:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
@media (pointer: coarse) { .ds-247420 .ds-session-loadmore { min-height: 44px; } }
/* Grouped rows (Today/Yesterday/...) lay out like the flat list; the section
   label sticks to the top of the scroll area for Claude-Desktop-style headers. */
.ds-247420 .ds-session-group { display: flex; flex-direction: column; }
.ds-247420 .ds-session-group-rows { display: flex; flex-direction: column; gap: var(--space-hair); }
.ds-247420 .ds-session-group-label { position: sticky; top: 0; z-index: var(--z-sticky); background: var(--bg-1, var(--bg)); font-size: var(--fs-tiny); font-weight: 600; color: var(--fg-3); text-transform: uppercase; letter-spacing: var(--tr-label); padding: var(--space-2) var(--space-2) var(--space-1); }
.ds-247420 .ds-session-row {
  position: relative; display: flex; align-items: center; gap: var(--space-2);
  width: 100%; padding: var(--space-2); min-height: 52px; margin-bottom: var(--space-hair);
  background: transparent; border: none; border-radius: var(--r-1);
  cursor: pointer; text-align: left; color: var(--fg);
}
.ds-247420 .ds-session-row:hover { background: var(--bg-2); }
.ds-247420 .ds-session-row:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: calc(var(--focus-offset) * -1); }
.ds-247420 .ds-session-row.active { background: var(--accent-tint); box-shadow: inset 2px 0 0 var(--accent); }
.ds-247420 .ds-session-row.rail-green::before, .ds-247420 .ds-session-row.rail-purple::before, .ds-247420 .ds-session-row.rail-flame::before {
  content: ''; position: absolute; left: 0; top: 8px; bottom: 8px; width: 3px; border-radius: var(--r-hair);
}
/* Rail tones MUST match .row.rail-* in app-shell.css so the same semantic state
   reads as one colour across the conversation list and content rows. */
.ds-247420 .ds-session-row.rail-green::before { background: var(--accent-ink); }
.ds-247420 .ds-session-row.rail-purple::before { background: var(--purple-2); }
.ds-247420 .ds-session-row.rail-flame::before { background: var(--flame); }
.ds-247420 .ds-session-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: var(--space-hair); }
.ds-247420 .ds-session-title { font-size: var(--fs-sm); color: var(--fg); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ds-247420 .ds-session-sub { font-size: var(--fs-tiny); color: var(--fg-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ds-247420 .ds-session-meta { display: flex; align-items: center; gap: var(--space-2); flex: 0 0 auto; }
.ds-247420 .ds-session-agent { font-size: var(--fs-tiny); color: var(--fg-3); }
/* New-activity cue is a hollow accent RING so it stays shape-distinct from the
   running live disc (a solid pulsing accent dot) for colour-blind / reduced-motion users. */
.ds-247420 .ds-session-unread { width: 8px; height: 8px; border-radius: 50%; background: transparent; box-shadow: inset 0 0 0 1.5px var(--accent); display: inline-block; flex: none; }
.ds-247420 .ds-session-state { padding: var(--space-5) var(--space-3); text-align: center; color: var(--fg-3); font-size: var(--fs-sm); }
.ds-247420 .ds-session-state-error { color: var(--flame); }

/* Fork/branch tree nesting — child sessions indent under their parent with a
   collapse toggle and a small branch glyph, mirroring pi-web's SessionSidebar
   tree without reimplementing its inline-style React component. */
.ds-247420 .ds-session-row-nested { position: relative; }
.ds-247420 .ds-session-tree-toggle {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; padding: 0; background: none; border: none;
  color: var(--fg-3); cursor: pointer; transform: rotate(0deg); transition: transform var(--dur-snap) var(--ease);
}
.ds-247420 .ds-session-tree-toggle.open { transform: rotate(90deg); }
.ds-247420 .ds-session-tree-toggle:hover { color: var(--fg); }
.ds-247420 .ds-session-fork-icon { flex: 0 0 auto; display: inline-flex; color: var(--fg-3); }

/* Hover-revealed rename/delete row actions. */
.ds-247420 .ds-session-row-actions { flex: 0 0 auto; display: none; align-items: center; gap: var(--space-1); }
.ds-247420 .ds-session-row:hover .ds-session-row-actions,
.ds-247420 .ds-session-row:focus-within .ds-session-row-actions { display: flex; }
.ds-247420 .ds-session-row-action {
  display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; padding: 0; background: var(--bg-2); border: var(--border-w) solid var(--border);
  border-radius: var(--r-1); color: var(--fg-3); cursor: pointer;
}
.ds-247420 .ds-session-row-action:hover { background: var(--bg-3); color: var(--fg); border-color: var(--fg-3); }
.ds-247420 .ds-session-row-action-danger:hover { background: color-mix(in srgb, var(--flame) 12%, var(--bg-2)); color: var(--flame); border-color: var(--flame); }
@media (pointer: coarse) { .ds-247420 .ds-session-row-action { min-width: 44px; min-height: 44px; } }

/* Inline rename — the row's title area swaps for a text input at the same
   height; no modal, matches the row-in-place edit pattern used elsewhere. */
.ds-247420 .ds-session-rename-input {
  flex: 1; min-width: 0; font-size: var(--fs-sm); font-family: inherit; color: var(--fg);
  background: var(--bg); border: var(--border-w) solid var(--accent); border-radius: var(--r-1);
  padding: var(--space-1) var(--space-2);
}
.ds-247420 .ds-session-rename-input:focus-visible { outline: none; box-shadow: var(--focus-ring-inset); }

/* Inline delete confirm — same row height, two flat buttons, no modal;
   mirrors SessionDashboard's arm-then-confirm bulk-stop control. */
.ds-247420 .ds-session-confirm-msg { flex: 1; min-width: 0; font-size: var(--fs-sm); color: var(--fg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ds-247420 .ds-session-confirm-actions { flex: 0 0 auto; display: flex; gap: var(--space-1); }
.ds-247420 .ds-session-confirm-delete, .ds-247420 .ds-session-confirm-cancel {
  font-size: var(--fs-tiny); font-weight: 600; padding: var(--space-1) var(--space-2);
  border-radius: var(--r-1); cursor: pointer;
}
.ds-247420 .ds-session-confirm-delete { background: var(--flame); border: none; color: var(--bg); }
.ds-247420 .ds-session-confirm-delete:hover { filter: brightness(1.1); }
.ds-247420 .ds-session-confirm-cancel { background: var(--bg-2); border: var(--border-w) solid var(--border); color: var(--fg-2); }
.ds-247420 .ds-session-confirm-cancel:hover { background: var(--bg-3); color: var(--fg); }
@media (pointer: coarse) { .ds-247420 .ds-session-confirm-delete, .ds-247420 .ds-session-confirm-cancel { min-height: 44px; } }

/* ----------------------------------------------------------------------------
   SessionDashboard — grid of live-session cards.
---------------------------------------------------------------------------- */
.ds-247420 .ds-dash { display: flex; flex-direction: column; min-height: 0; }
.ds-247420 .ds-dash-header { flex: 0 0 auto; display: flex; align-items: center; gap: var(--space-3); row-gap: var(--space-2); flex-wrap: wrap; padding: var(--space-3) var(--space-4) 0; }
.ds-247420 .ds-dash-count { font-size: var(--fs-sm); color: var(--fg-2); font-weight: 600; }
/* Stop is a quiet destructive control in chrome, not a page-level red CTA;
   it only goes loud while ARMED (the one-shot press-again confirm). */
.ds-247420 .ds-dash-header .btn, .ds-247420 .ds-dash-header .btn-primary {
  margin-left: 0;
  padding: var(--space-1-5) var(--space-2-75); min-height: 32px; border-radius: var(--r-1); font-weight: 500;
}
.ds-247420 .ds-dash-header .btn-primary.danger {
  background: transparent; color: var(--warn);
  border: var(--bw-hair) solid var(--warn);
}
.ds-247420 .ds-dash-header .btn-primary.danger:hover {
  background: color-mix(in oklab, var(--warn) 12%, transparent); color: var(--warn);
}
/* --ink, not --paper, on the armed fill: --warn (#FF5A52) is a light-ish red,
   so paper-on-warn measures 2.72:1 (below the 4.5:1 floor) while ink-on-warn
   measures 6.27:1. This is the same --accent/--accent-ink split the token layer
   documents -- a saturated fill needs the dark text tone, not the light one. */
.ds-247420 .ds-dash-header .btn-primary.danger.is-armed { background: var(--warn); color: var(--ink); border-color: var(--warn); }
.ds-247420 .ds-dash-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: var(--space-2); padding: var(--space-3); }
@media (min-width: 1500px) { .ds-247420 .ds-dash-grid { grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); } }
.ds-247420 .ds-dash-card { display: flex; flex-direction: column; gap: var(--space-2); padding: var(--space-3); background: var(--bg-2); border: var(--bw-hair) solid var(--bg-3); border-radius: var(--r-1); }

/* Compact density — one line per session. The comfortable card stacks title +
   head + meta + actions as four flex rows and measured 208px tall on a real
   dashboard, so a 439px viewport showed two sessions at once; a command center
   whose job is scanning many sessions cannot afford a third of a screen each.
   Compact keeps every fact and drops only the per-card action row (reachable by
   opening the session), laying head and meta side by side instead of stacked.
   The grid also collapses to a single column: a min-240px card grid beside
   one-line rows would put unrelated sessions shoulder to shoulder and destroy
   the vertical scan the density exists to create. */
.ds-247420 .ds-dash-grid.is-compact { grid-template-columns: 1fr; gap: 0; padding: var(--space-1) var(--space-2); }
/* nowrap, not wrap: with wrapping allowed a long value pushes the next cluster
   onto its own line and the row grows. Measured that way -- rows came out 37 /
   55 / 75 / 176px instead of one height, the 176px case being a long phase
   label that expanded the head to 498px and squeezed meta to 202px so its
   contents stacked. Every child now elides instead of wrapping, which is what
   keeps a density a density. */
.ds-247420 .ds-dash-card.is-compact {
  flex-direction: row; align-items: baseline; flex-wrap: nowrap;
  gap: var(--space-2); padding: var(--space-1-75) var(--space-2);
  background: transparent; border: 0; border-radius: 0;
  border-bottom: var(--bw-hair) solid var(--bg-3);
  overflow: hidden;
}
/* The head carries status + agent + model; it may shrink and elide rather than
   expand past its content and starve the meta cluster. */
.ds-247420 .ds-dash-card.is-compact .ds-dash-card-head {
  flex: 0 1 auto; min-width: 0; overflow: hidden; white-space: nowrap;
}
.ds-247420 .ds-dash-card.is-compact .ds-dash-agent,
.ds-247420 .ds-dash-card.is-compact .ds-dash-model,
.ds-247420 .ds-dash-card.is-compact .ds-dash-status {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
/* meta takes the remaining width and stays on one line, so a long cwd elides
   rather than wrapping the row to two lines and defeating the density. */
.ds-247420 .ds-dash-card.is-compact .ds-dash-meta {
  flex: 1 1 0; min-width: 0; display: flex; flex-direction: row;
  align-items: baseline; gap: var(--space-2); overflow: hidden;
  flex-wrap: nowrap; white-space: nowrap;
}
.ds-247420 .ds-dash-card.is-compact .ds-dash-stat,
.ds-247420 .ds-dash-card.is-compact .ds-dash-activity {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
.ds-247420 .ds-dash-card.is-compact .ds-dash-cwd {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ds-247420 .ds-dash-card.is-compact .ds-dash-activity { flex: 0 0 auto; margin-inline-start: auto; }
/* The selected/active affordances are inset shadows on the comfortable card;
   with no border to sit inside, compact carries them as a leading rule. */
.ds-247420 .ds-dash-card.is-compact.is-active { box-shadow: inset 2px 0 0 var(--accent); }
.ds-247420 .ds-dash-card.is-compact:last-child { border-bottom: 0; }

/* Visual weight follows the status the host already computes -- it does NOT
   invent a severity. A dense list where every row paints identically makes the
   reader scan all of them; the point of ranking rows is that the eye should
   land on the one that needs attention. `error` is reserved by the host for
   something genuinely wrong (a blocked gate, an unparseable state, a dispatch
   that never completed), `running` for live work, `stale` for idle -- so those
   three get three weights and nothing else changes. */
/* Measured baseline before choosing these: .ds-dash-agent is already
   font-weight 600 / color --fg, and .ds-dash-cwd is already --fg-3. So the
   differentiator cannot BE --fg-3 (that is the default) -- error must rise
   above the baseline and idle must fall below it. */
.ds-247420 .ds-dash-card.is-compact.is-error { box-shadow: inset 2px 0 0 var(--flame); }
.ds-247420 .ds-dash-card.is-compact.is-error .ds-dash-agent { font-weight: 700; color: var(--fg); }
.ds-247420 .ds-dash-card.is-compact.is-running .ds-dash-agent { font-weight: 600; color: var(--fg); }
/* Idle recedes by weight and by the tier-2 tone -- NOT by opacity, which would
   scale every child including the status word and drop it under the contrast
   floor. --fg-2 stays a real text tone. */
.ds-247420 .ds-dash-card.is-compact.is-stale .ds-dash-agent { font-weight: 400; color: var(--fg-2); }
.ds-247420 .ds-dash-card.is-error { border-color: var(--flame); }
.ds-247420 .ds-dash-card-head { display: flex; align-items: center; gap: var(--space-2); }
.ds-247420 .ds-dash-status { font-size: var(--fs-tiny); font-weight: 600; text-transform: uppercase; letter-spacing: var(--tr-caps); }
/* running tone unified on --accent (the later is-running rule + the live disc all
   use --accent); the dead var(--green) duplicate that lived here was removed. */
.ds-247420 .ds-dash-status.is-error { color: var(--flame); }
.ds-247420 .ds-dash-agent { font-size: var(--fs-body); font-weight: 600; color: var(--fg); }
.ds-247420 .ds-dash-model { font-family: var(--ff-mono); font-size: var(--fs-tiny); color: var(--fg-3); margin-left: auto; }
.ds-247420 .ds-dash-meta { display: flex; flex-direction: column; gap: var(--space-hair); }
.ds-247420 .ds-dash-cwd { font-family: var(--ff-mono); font-size: var(--fs-tiny); color: var(--fg-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ds-247420 .ds-dash-stat { font-family: var(--ff-mono); font-size: var(--fs-tiny); color: var(--fg-2); }
.ds-247420 .ds-dash-stat-cost { color: var(--fg); font-weight: 600; }
.ds-247420 .ds-dash-activity { font-size: var(--fs-tiny); color: var(--fg-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ds-247420 .ds-dash-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-1); }
.ds-247420 .ds-dash-state { padding: var(--space-6) var(--space-4); text-align: center; color: var(--fg-3); font-size: var(--fs-body); }
.ds-247420 .ds-dash-state-error { color: var(--flame); }

@media (max-width: 640px) {
  .ds-247420 .ds-dash-grid { grid-template-columns: 1fr; padding: var(--space-3); }
}

/* ----------------------------------------------------------------------------
   ContextPane — compact right-hand context panel for the chat surface.
---------------------------------------------------------------------------- */
.ds-247420 .ds-context { display: flex; flex-direction: column; gap: var(--space-3); padding: var(--space-3); }
.ds-247420 .ds-context .panel { background: var(--bg-2); border: var(--bw-hair) solid var(--bg-3); border-radius: var(--r-2); }
.ds-247420 .ds-context .row .meta { font-family: var(--ff-mono); font-size: var(--fs-xs); color: var(--fg-2); }
.ds-247420 .ds-context .row .sub { font-size: var(--fs-xs); color: var(--fg-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ds-247420 .ds-context-actions { display: flex; gap: var(--space-2); }
/* Lighter fact groups for secondary context blocks (conversation/last turn/
   recent files): a small caps label + Row list with no card chrome, so a
   2-row fact doesn't compete visually with the primary bordered panel above
   it. Separated from neighbors by a hairline rule, not a border box. */
.ds-247420 .ds-context-group { padding-top: var(--space-3); border-top: var(--bw-hair) solid var(--rule); }
.ds-247420 .ds-context-group-label {
  font-size: var(--fs-tiny); font-weight: 600; color: var(--fg-3);
  letter-spacing: var(--tr-caps); text-transform: uppercase;
  padding: 0 0 var(--space-2);
}
.ds-247420 .ds-context-group .row + .row { margin-top: var(--space-hair); }

/* ----------------------------------------------------------------------------
   ContextMeter / ContextTreemap / ContextXRayPanel — token/cost breakdown
   surfaces added alongside ContextPane. Tone classes share one small
   vocabulary (system/user/assistant/other) across all three components so a
   segment's color pairs up wherever it appears.
---------------------------------------------------------------------------- */
.ds-247420 .ds-context-meter { display: flex; flex-direction: column; gap: var(--space-1); }
.ds-247420 .ds-context-meter-track {
  display: flex; width: 100%; height: 8px; border-radius: var(--r-hair);
  overflow: hidden; background: var(--bg-3);
}
.ds-247420 .ds-context-meter-seg { display: block; height: 100%; }
.ds-247420 .ds-context-meter-seg-system { background: var(--sky); }
.ds-247420 .ds-context-meter-seg-user { background: var(--green-2); }
.ds-247420 .ds-context-meter-seg-assistant { background: var(--purple-2); }
.ds-247420 .ds-context-meter-seg-other { background: var(--fg-3); }
.ds-247420 .ds-context-meter-foot {
  display: flex; justify-content: space-between; font-size: var(--fs-tiny); color: var(--fg-3);
}

.ds-247420 .ds-context-treemap { display: block; width: 100%; height: auto; border-radius: var(--r-1); }
.ds-247420 .ds-context-treemap-rect { stroke: var(--bg); stroke-width: 1; }
.ds-247420 .ds-context-treemap-rect-system { fill: var(--sky); }
.ds-247420 .ds-context-treemap-rect-user { fill: var(--green-2); }
.ds-247420 .ds-context-treemap-rect-assistant { fill: var(--purple-2); }
.ds-247420 .ds-context-treemap-rect-other { fill: var(--fg-3); }
.ds-247420 .ds-context-treemap-label { font-size: var(--fs-pico); fill: var(--on-accent); font-family: var(--ff-body); }
.ds-247420 .ds-context-treemap-empty { padding: var(--space-3); color: var(--fg-3); font-size: var(--fs-sm); }

.ds-247420 .ds-context-xray-head { display: flex; align-items: center; gap: var(--space-1-75); width: 100%; }
.ds-247420 .ds-context-xray-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.ds-247420 .ds-context-xray-dot-system { background: var(--sky); }
.ds-247420 .ds-context-xray-dot-user { background: var(--green-2); }
.ds-247420 .ds-context-xray-dot-assistant { background: var(--purple-2); }
.ds-247420 .ds-context-xray-dot-other { background: var(--fg-3); }
.ds-247420 .ds-context-xray-label { flex: 1; }
.ds-247420 .ds-context-xray-value { color: var(--fg-3); font-family: var(--ff-mono); font-size: var(--fs-xs); }
.ds-247420 .ds-context-xray-body { display: flex; flex-direction: column; gap: var(--space-1); }
.ds-247420 .ds-context-xray-row { display: flex; justify-content: space-between; font-size: var(--fs-tiny); color: var(--fg-2); }
.ds-247420 .ds-context-xray-empty { padding: var(--space-3); color: var(--fg-3); font-size: var(--fs-sm); }

/* ============================================================================
   Cohesion sweep (gui-cohesion.js): one-product polish across surfaces.
============================================================================ */

/* --- A1/A2/E3: per-block code copy. The <pre> is wrapped in .chat-code-block
   (position:relative) so the copy button sits top-right and reveals on hover/
   focus-within, matching the .chat-msg-actions pattern. The structured CodeNode
   head + preview head place the button inline (always visible). --- */
.ds-247420 .chat-code-block { position: relative; }
.ds-247420 .chat-code-copy {
  position: absolute; top: 6px; right: 6px;
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 48px; min-height: 24px; padding: var(--space-hair) var(--space-2);
  border: var(--bw-hair) solid var(--bg-3); border-radius: var(--r-1, 8px);
  background: var(--bg-2); color: var(--fg-2); cursor: pointer;
  font-family: var(--ff-body); font-size: var(--fs-tiny);
  opacity: 0; transition: opacity var(--dur-snap, .12s) var(--ease, ease);
}
.ds-247420 .chat-code-block:hover .chat-code-copy, .ds-247420 .chat-code-block:focus-within .chat-code-copy { opacity: 1; }
.ds-247420 .chat-code-copy.is-copied { color: var(--accent-ink); border-color: var(--accent); }
.ds-247420 .chat-code-copy:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: 1px; opacity: 1; }
/* Head-placed copy (structured CodeNode + FilePreviewCode) is always visible. */
.ds-247420 .chat-code-head .spread { flex: 1; }
.ds-247420 .chat-code-copy-head { position: static; opacity: 1; }

/* --- B1: streaming pre shell — monospaced bubble while a fenced block streams,
   so it does not reflow prose->block on settle. --- */
.ds-247420 .chat-stream-pre pre { margin: 0; white-space: pre-wrap; word-break: break-word; font-family: var(--ff-mono); font-size: var(--fs-sm); }
/* Persistent minimal chrome so a fenced block does not pop a border + lang tab +
   copy button only on settle - the streaming slab already reads as a code block. */
.ds-247420 .chat-stream-pre { position: relative; }
.ds-247420 .chat-stream-pre pre {
  border: var(--bw-hair) solid var(--rule); border-radius: var(--r-1);
  padding: var(--space-3); background: color-mix(in oklab, var(--fg) 4%, transparent);
}

/* --- reply-preview banner shown above the composer while replying to a message. --- */
.ds-247420 .cm-composer-wrap { display: flex; flex-direction: column; width: 100%; }
.ds-247420 .cm-reply-preview {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-2);
  padding: var(--space-1) var(--space-3); border-bottom: var(--bw-hair) solid var(--rule);
  background: color-mix(in oklab, var(--accent) 6%, transparent);
  font-family: var(--ff-mono); font-size: var(--fs-tiny); color: var(--fg-2);
}
.ds-247420 .cm-reply-preview-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ds-247420 .cm-reply-preview-close {
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: none; color: var(--fg-3); cursor: pointer; padding: var(--space-1);
  border-radius: var(--r-1); flex: 0 0 auto;
}
.ds-247420 .cm-reply-preview-close:hover { color: var(--fg); background: color-mix(in oklab, var(--fg) 8%, transparent); }
.ds-247420 .cm-reply-preview-close:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }

/* --- A5: composer context line (agent / model / cwd at point of typing). --- */
.ds-247420 .chat-composer-context {
  display: block; width: 100%; text-align: left; padding: var(--space-1) var(--space-3) 0;
  background: none; border: none; cursor: default; color: var(--fg-3);
  font-family: var(--ff-mono); font-size: var(--fs-tiny);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Below ~480px a single nowrap line silently truncates most of the agent /
   model / cwd context (no way to recover it - no title attr on the container,
   only on individual bits). Let it wrap instead of clipping. */
@media (max-width: 480px) {
  .ds-247420 .chat-composer-context { white-space: normal; overflow: visible; text-overflow: clip; }
}
.ds-247420 button.chat-composer-context { cursor: pointer; }
.ds-247420 button.chat-composer-context:hover { color: var(--fg-2); }
.ds-247420 button.chat-composer-context:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }

/* Composer send/cancel button row: a non-wrapping cluster pinned to the
   bottom-right of the flex-end composer (was unstyled, so the send button
   could wrap below the textarea instead of anchoring like claude.ai/code). */
.ds-247420 .chat-composer-toolbar { display: flex; align-items: center; gap: var(--space-1); flex: 0 0 auto; margin-left: auto; align-self: flex-end; }
/* Unify the toolbar cluster on one 32px / --r-1 square metric (the round 40px
   ghost buttons + 36px square send read as two button systems; the kit moved
   .btn* off --r-pill). chat.css loads after app-shell.css so this wins by order. */
.ds-247420 .chat-composer-toolbar .composer-btn,
.ds-247420 .chat-composer-toolbar .send { width: 32px; height: 32px; border-radius: var(--r-1); }
@media (pointer: coarse) {
  .ds-247420 .chat-composer-toolbar .composer-btn,
  .ds-247420 .chat-composer-toolbar .send { width: 44px; height: 44px; }
}

/* --- B2: follow-up chips below the last settled assistant turn. --- */
.ds-247420 .agentchat-followups { display: flex; flex-wrap: wrap; gap: var(--space-2); padding: var(--space-2) var(--space-3); }

/* --- C1: stale/idle dashboard status (static, NOT pulsing). --- */
.ds-247420 .ds-dash-status.is-stale { color: var(--stale); }
.ds-247420 .ds-dash-status.is-running { color: var(--accent-ink); }

/* --- C2: dashboard sort/filter toolbar + stream-state line. --- */
/* The toolbar is part of the ONE header row (count | heartbeat ... filter,
   sort, errors-only, stop): compact controls that align on a shared 32px
   height, never a stacked full-width form column. */
.ds-247420 .ds-dash-toolbar { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.ds-247420 .ds-dash-toolbar .ds-search-input {
  width: 200px; min-height: 32px; padding: var(--space-1-5) var(--space-2-5);
  font-size: var(--fs-sm); border-radius: var(--r-1);
}
.ds-247420 .ds-dash-toolbar .ds-select, .ds-247420 .ds-dash-toolbar select {
  min-height: 32px; padding: var(--space-1) var(--space-2-5); font-size: var(--fs-sm);
  width: auto; border-radius: var(--r-1);
}
.ds-247420 .ds-dash-stream { font-size: var(--fs-tiny); color: var(--fg-3); }
.ds-247420 .ds-dash-stream.is-connected { color: var(--accent-ink); }
.ds-247420 .ds-dash-stream.is-lost { color: var(--flame); }
.ds-247420 .ds-dash-stream.is-connecting { color: var(--amber); }
.ds-247420 .ds-dash-header .spread { flex: 1; }
.ds-247420 .ds-dash-errors-toggle {
  padding: var(--space-1) var(--space-2-5); min-height: 32px; border: var(--bw-hair) solid var(--bg-3);
  border-radius: var(--r-1); background: var(--bg-2); color: var(--fg-2);
  cursor: pointer; font-family: var(--ff-body); font-size: var(--fs-tiny);
}
.ds-247420 .ds-dash-errors-toggle.active { background: var(--accent-tint); color: var(--fg); border-color: var(--accent); }
.ds-247420 .ds-dash-errors-toggle:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: 1px; }

/* --- C3: per-card select checkbox. --- */
.ds-247420 .ds-dash-select {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 24px; min-height: 24px; padding: 0; cursor: pointer;
  border: none; background: none; color: var(--fg-3);
  font-family: var(--ff-mono); font-size: var(--fs-sm);
}
.ds-247420 .ds-dash-select[aria-checked="true"] { color: var(--accent-ink); }
.ds-247420 .ds-dash-select:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: 1px; }

/* --- C5: active dashboard card (current conversation). --- */
.ds-247420 .ds-dash-card.is-active { box-shadow: inset 2px 0 0 var(--accent); }
.ds-247420 .ds-dash-card.is-selected { background: var(--accent-tint); }
/* Stale/idle card: a positive amber inset bar (mirrors is-active) so a stuck
   agent is flagged in a dense grid, not merely faded near-invisibly. The word
   'idle' co-carries state, so this stays colour-blind safe. */
.ds-247420 .ds-dash-card.is-stale { box-shadow: inset 2px 0 0 var(--stale); }
/* Active identity always wins over stale amber when both classes are present. */
.ds-247420 .ds-dash-card.is-active.is-stale { box-shadow: inset 2px 0 0 var(--accent); }

/* --- H3: dashboard live disc pulses; stale/error do not (handled by H1). --- */

/* --- C6: per-tab conversation-list caption. --- */
.ds-247420 .ds-session-caption { padding: var(--space-2) var(--space-3) 0; font-size: var(--fs-tiny); color: var(--fg-3); }

/* --- F1: context-pane empty state. --- */
.ds-247420 .ds-context-empty { padding: var(--space-4) var(--space-3); color: var(--fg-3); font-size: var(--fs-sm); line-height: 1.5; }

/* --- E1: inline file preview pane (split view, non-modal). --- */
.ds-247420 .ds-preview-pane { display: flex; flex-direction: column; min-height: 0; height: 100%; overflow: hidden; }
.ds-247420 .ds-preview-pane:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: calc(var(--focus-offset) * -1); }
.ds-247420 .ds-preview-pane .ds-preview-body { flex: 1; min-height: 0; overflow: auto; }
.ds-247420 .ds-preview-pane-empty { align-items: center; justify-content: center; color: var(--fg-3); font-size: var(--fs-sm); }
.ds-247420 .ds-preview-code-wrap { display: flex; flex-direction: column; min-height: 0; }
.ds-247420 .ds-preview-code-head { flex: 0 0 auto; }
/* --- E2: prev/next stepper in preview head. --- */
.ds-247420 .ds-preview-step { display: inline-flex; gap: var(--space-hair); }
.ds-247420 .ds-preview-step .ds-file-act { min-width: 40px; padding: 0 var(--space-1-75); font-family: var(--ff-body); font-size: var(--fs-tiny); }

/* --- D1: file grid "show more" row. --- */
.ds-247420 .ds-file-more { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3); justify-content: center; }
.ds-247420 .ds-file-more-count { font-size: var(--fs-tiny); color: var(--fg-3); }
.ds-247420 .ds-file-more-btn {
  padding: var(--space-1) var(--space-2-75); min-height: 32px; border: var(--bw-hair) solid var(--bg-3);
  border-radius: var(--r-1); background: var(--bg-2); color: var(--fg-2); cursor: pointer;
  font-family: var(--ff-body); font-size: var(--fs-tiny);
}
.ds-247420 .ds-file-more-btn:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: 1px; }
/* --- D3: locked/no-access file row. --- */
.ds-247420 .ds-file-row.is-locked { opacity: .6; }

/* --- D4: roots picker segmented control. --- */
.ds-247420 .ds-roots-picker { display: inline-flex; gap: var(--space-hair); padding: var(--space-hair); background: var(--bg-2); border-radius: var(--r-1); }
.ds-247420 .ds-roots-tab {
  padding: var(--space-1) var(--space-2-5); min-height: 32px; border: none; border-radius: var(--r-1);
  background: none; color: var(--fg-3); cursor: pointer;
  font-family: var(--ff-body); font-size: var(--fs-tiny);
}
.ds-247420 .ds-roots-tab:hover { background: var(--bg-2); color: var(--fg); }
.ds-247420 .ds-roots-tab.active { background: var(--bg); color: var(--fg); }
.ds-247420 .ds-roots-tab:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: 1px; }

/* --- H4: touch targets. On a coarse pointer / narrow screen the hover-revealed
   action rows are made permanently visible and small controls floor to 44px so
   they are reachable by finger. --- */
@media (pointer: coarse), (max-width: 640px) {
  .ds-247420 .chat-msg-actions { opacity: 1; }
  .ds-247420 .chat-code-copy { opacity: 1; }
  .ds-247420 .chat-tool-copy { opacity: 1; min-width: 44px; min-height: 44px; }
  .ds-247420 .chat-msg-action { min-width: 44px; height: 44px; }
  .ds-247420 .ds-dash-select { min-width: 44px; min-height: 44px; }
  .ds-247420 .ds-file-more-btn, .ds-247420 .ds-roots-tab, .ds-247420 .ds-dash-errors-toggle { min-height: 44px; }
  .ds-247420 .agentchat-export-act { min-height: 44px; }
  .ds-247420 .agentchat-install-copy { min-height: 44px; min-width: 44px; }
}

/* --- SessionMeta: middot-separated session metadata strip. Class is
   .ds-session-meta-strip (.ds-session-meta is taken by ConversationList row
   meta above). Wraps at narrow widths. --- */
.ds-247420 .ds-session-meta-strip {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2);
  font-size: var(--fs-tiny); color: var(--fg-3);
}
.ds-247420 .ds-session-meta-item { display: inline-flex; align-items: center; gap: var(--space-1); min-width: 0; }
.ds-247420 .ds-session-meta-item + .ds-session-meta-item::before { content: '\00B7'; color: var(--fg-3); margin-right: var(--space-1); }
.ds-247420 .ds-session-meta-label { color: var(--fg-3); }
.ds-247420 .ds-session-meta-value { font-family: var(--ff-mono); color: var(--fg-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ds-247420 .ds-session-meta-copy, .ds-247420 .ds-session-meta-action {
  padding: var(--space-hair) var(--space-2); min-height: 24px; cursor: pointer;
  background: var(--bg-2); border: var(--bw-hair) solid var(--bg-3); border-radius: var(--r-1);
  color: var(--fg-3); font-family: var(--ff-body); font-size: var(--fs-micro);
}
.ds-247420 .ds-session-meta-copy:hover, .ds-247420 .ds-session-meta-action:hover { background: var(--bg-3); color: var(--fg); }
.ds-247420 .ds-session-meta-copy:focus-visible, .ds-247420 .ds-session-meta-action:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: 1px; }
@media (pointer: coarse), (max-width: 640px) {
  .ds-247420 .ds-session-meta-copy, .ds-247420 .ds-session-meta-action { min-height: 44px; min-width: 44px; }
}

/* ============================================================================
   Logic/predictability sweep (2026-06-10): composer ergonomics, turn notices,
   windowed thread, dashboard stopping/external states, cwd validation.
============================================================================ */

/* Enter-to-send hint: visible at rest so a new user with an empty composer
   can discover Enter-vs-Shift+Enter without having to focus it first;
   hidden under 420px to save rows. */
.ds-247420 .chat-composer-hint {
  display: block; width: 100%; order: 5; padding: var(--space-hair) var(--space-1) 0;
  font-size: var(--fs-micro); color: var(--fg-3);
}
@media (max-width: 420px) {
  .ds-247420 .chat-composer .chat-composer-hint { display: none; }
}

/* Transient composer note (paste/drop not supported), aria-live polite. */
.ds-247420 .chat-composer-note {
  padding: var(--space-1) var(--space-3); font-size: var(--fs-tiny); color: var(--fg-2);
}

/* Streaming elapsed-time counter: ticks m:ss while a turn is in flight.
   Tabular-nums so the digits don't jitter the layout every second. */
.ds-247420 .chat-composer-elapsed {
  order: 5; padding: var(--space-hair) var(--space-1) 0;
  font-size: var(--fs-micro); color: var(--fg-3);
  font-variant-numeric: tabular-nums; font-feature-settings: 'tnum';
}

/* Detected-attachment badge: a dismissible pill above the textarea, styled
   like the composer-note treatment (same tone/scale) rather than inventing
   new chrome. */
.ds-247420 .chat-composer-detected-badge {
  display: flex; align-items: center; gap: var(--space-2, 8px);
  padding: var(--space-1) var(--space-3); font-size: var(--fs-tiny); color: var(--fg-2);
  background: var(--bg-2); border: var(--bw-hair) solid var(--rule);
  border-radius: var(--r-1); width: 100%; order: -1;
}
.ds-247420 .chat-composer-detected-label { flex: 1 1 auto; }
.ds-247420 .chat-composer-detected-dismiss {
  flex: 0 0 auto; background: none; border: none; cursor: pointer;
  color: var(--fg-3); font: inherit; font-size: var(--fs-tiny);
  padding: var(--space-hair) var(--space-1-75); border-radius: var(--r-1);
}
.ds-247420 .chat-composer-detected-dismiss:hover { color: var(--fg); }
.ds-247420 .chat-composer-detected-dismiss:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
@media (pointer: coarse) {
  .ds-247420 .chat-composer-detected-dismiss { min-height: 44px; min-width: 44px; }
}

/* Drag-over ring: token accent, never a hex. */
.ds-247420 .chat-composer.dragover { outline: var(--focus-w) solid var(--focus-color); outline-offset: calc(var(--focus-offset) * -1); border-radius: var(--r-2); }

/* ChatSuggestions — blank-thread composer-priming CTA. */
.ds-247420 .chat-suggestions {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: var(--space-2); padding: var(--space-6) var(--space-3);
  max-width: 640px; margin: 0 auto;
}
.ds-247420 .chat-suggestions-heading { margin: 0; font-size: var(--fs-xl); }
.ds-247420 .chat-suggestions-subtext { margin: 0; color: var(--fg-2); font-size: var(--fs-sm); }
.ds-247420 .chat-suggestions-list {
  display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-2);
  margin-top: var(--space-2);
}
.ds-247420 .chat-suggestions-chip {
  appearance: none; background: var(--bg-2); border: var(--bw-hair) solid var(--rule);
  border-radius: var(--r-pill, 999px); color: inherit; font: inherit; font-size: var(--fs-sm);
  padding: var(--space-1-5) var(--space-3); cursor: pointer;
}
.ds-247420 .chat-suggestions-chip:hover, .ds-247420 .chat-suggestions-chip:focus-visible {
  background: var(--panel-2, var(--rule)); outline: none;
}
.ds-247420 .chat-suggestions-chip:focus-visible {
  outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset);
}
@media (pointer: coarse) {
  .ds-247420 .chat-suggestions-chip { min-height: 44px; }
}

/* Per-bit composer context: only the bit that owns a control is a button. */
.ds-247420 .chat-composer-context-bit {
  display: inline; padding: 0; margin: 0;
  background: none; border: none; cursor: pointer;
  font: inherit; color: inherit; text-decoration: underline dotted;
}
.ds-247420 .chat-composer-context-bit:hover { color: var(--fg-2); }
.ds-247420 .chat-composer-context-bit:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
@media (pointer: coarse) {
  .ds-247420 .chat-composer-context-bit { min-height: 44px; }
}

/* Out-of-band turn notices: neutral tone (NOT error red) — a stopped or
   incomplete turn must not read as a finished answer. */
.ds-247420 .chat-msg-notice {
  margin-top: var(--space-1); padding: var(--space-1-75) var(--space-2-5);
  border: var(--bw-hair) solid var(--rule); border-radius: var(--r-2);
  background: var(--bg-2); color: var(--fg-2); font-size: var(--fs-tiny);
}
.ds-247420 .chat-msg-notice.is-incomplete { border-style: dashed; }
/* Error is the one genuine failure state among these notices — reserving
   the destructive tone for it (unlike stopped/incomplete, which are neutral
   "not a finished answer" states, not failures). */
.ds-247420 .chat-msg-notice.is-error {
  border-color: var(--danger); color: var(--danger-ink, var(--danger));
  display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
}
.ds-247420 .chat-msg-retry-btn {
  border: var(--bw-hair) solid var(--danger); border-radius: var(--r-1);
  background: transparent; color: var(--danger-ink, var(--danger));
  font: inherit; font-size: var(--fs-tiny); padding: var(--space-hair) var(--space-2-5); cursor: pointer;
}
.ds-247420 .chat-msg-retry-btn:hover { background: var(--danger); color: var(--bg); }
.ds-247420 .chat-msg-retry-btn:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }

/* Tail-window streaming head ('streaming · N KB so far'). */
.ds-247420 .chat-stream-head {
  padding: var(--space-hair) 0 var(--space-1-75); font-family: var(--ff-mono);
  font-size: var(--fs-micro); color: var(--fg-3);
}

/* Windowed thread: 'show N earlier turns' control at the top. */
.ds-247420 .agentchat-earlier {
  display: flex; align-items: center; justify-content: center;
  gap: var(--space-3); padding: var(--space-2) var(--space-3);
}
.ds-247420 .agentchat-earlier-count { font-size: var(--fs-tiny); color: var(--fg-3); }
.ds-247420 .agentchat-earlier-btn {
  padding: var(--space-1) var(--space-2-75); min-height: 32px; border: var(--bw-hair) solid var(--bg-3);
  border-radius: var(--r-1); background: var(--bg-2); color: var(--fg-2); cursor: pointer;
  font-family: var(--ff-body); font-size: var(--fs-tiny);
}
.ds-247420 .agentchat-earlier-btn:hover { background: var(--bg-2); color: var(--fg); }
.ds-247420 .agentchat-earlier-btn:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: 1px; }
@media (pointer: coarse) {
  .ds-247420 .agentchat-earlier-btn { min-height: 44px; }
}

/* Inline cwd validation line (checking / error) under the cwd input. */
.ds-247420 .agentchat-cwd-hint { font-size: var(--fs-tiny); color: var(--fg-3); }
.ds-247420 .agentchat-cwd-hint.is-error { color: var(--flame); }
.ds-247420 .agentchat-cwd-hint.is-checking { color: var(--fg-2); font-style: italic; }

/* Dashboard: shared session title heading (same string as the rails). */
.ds-247420 .ds-dash-title {
  font-size: var(--fs-sm); font-weight: 600; color: var(--fg);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* Stopping state: in-flight cancel reads distinctly from running/error. */
.ds-247420 .ds-dash-status.is-stopping { color: var(--amber); }

/* External (observed, not owned) session card: no stop control exists. */
.ds-247420 .ds-dash-external {
  /* 1px vertical is OFF-SCALE ON PURPOSE — a --fs-micro uppercase pill whose
     height must stay inside the card's meta row. --space-hair (2px) tips it
     past the row and pushes the card taller. Horizontal snaps normally. */
  padding: 1px var(--space-1-75); border: var(--bw-hair) solid var(--bg-3); border-radius: var(--r-pill);
  font-size: var(--fs-micro); color: var(--fg-3); text-transform: uppercase; letter-spacing: var(--tr-caps);
}
.ds-247420 .ds-dash-card.is-external { border-style: dashed; }

/* One connection vocabulary: offline (is-lost kept as legacy alias). */
.ds-247420 .ds-dash-stream.is-offline { color: var(--flame); }

/* ============================================================
   10th run - Claude-Code-web design-language parity
   ============================================================ */

/* Flat full-width chat turns (claude.ai/code), replacing the messenger
   avatar-disc + colored-bubble layout. AgentChat passes flat:true; the demo
   Chat keeps the bubble layout. Turns are full-width to --measure; the user vs
   assistant distinction is a role label + a faint assistant background. */
/* Center the flat turn at the reading measure so a wide content column does not
   dump a one-sided dead gutter (claude.ai/code centers the thread). Padding
   scales through --density so data-density=compact tightens the thread. */
.ds-247420 .chat-msg-flat { display: block; max-width: var(--measure); margin-inline: auto; padding: calc(var(--space-3) * var(--density, 1)) var(--space-4); margin-block: 0; background: none; border-radius: var(--r-2); }
.ds-247420 .chat-msg-flat.them { background: color-mix(in oklab, var(--fg) 3%, transparent); }
.ds-247420 .chat-msg-flat.you { background: none; }
.ds-247420 .chat-msg-flat .chat-stack { max-width: var(--measure); width: 100%; margin: 0; align-items: stretch; }
.ds-247420 .chat-msg-flat.you .chat-stack { align-items: stretch; }
.ds-247420 .chat-role { font-size: var(--fs-tiny); font-weight: 600; text-transform: uppercase; letter-spacing: var(--tr-caps); color: var(--fg-3); margin-bottom: var(--space-1); }
.ds-247420 .chat-msg-flat.you .chat-role { color: var(--accent-ink); }
.ds-247420 .chat-msg-flat .chat-bubble { background: none; border: none; border-radius: 0; padding: 0; max-width: 100%; box-shadow: none; transform: none; }
.ds-247420 .chat-msg-flat.you .chat-bubble { background: none; color: inherit; }
/* Flat user turns reset their bubble to transparent, but the messenger rule
   (.chat-msg.you .chat-bubble code) tints inline code on --accent-fg, which is
   invisible/wrong on the bare page surface. Reset to the neutral fg-10% tint the
   sibling assistant turns get (wins over app-shell.css by later source order). */
.ds-247420 .chat-msg-flat.you .chat-bubble code { background: color-mix(in oklab, var(--fg) 10%, transparent); }
/* Flat turns strip the bubble padding, so the role label sat 4px above the first
   paragraph and the .them tint hugged text flush. Restore reading rhythm + give
   the tinted assistant row inner breathing room so it reads as a card. */
.ds-247420 .chat-msg-flat .chat-role { margin-bottom: var(--space-2); }
.ds-247420 .chat-msg-flat .chat-md { line-height: var(--lh-long); }
.ds-247420 .chat-msg-flat .chat-md p { margin: var(--space-2) 0; }
/* The `+ 2px` is an OFF-SCALE optical correction, not a rhythm step: the tinted
   assistant row needs marginally more inset than the untinted .you row so the
   fill does not appear to crowd the text. Expressing it as a --space-* rung
   would make it a visible step rather than the sub-perceptual nudge intended. */
.ds-247420 .chat-msg-flat.them { padding-block: calc(var(--space-3) * var(--density, 1) + 2px); }
/* Inline backtick code (renderInline tags <code class=chat-tick>) - was a dead
   class relying on the generic .chat-bubble code rule, so notices / non-bubble
   surfaces got unstyled monospace. Self-style it regardless of container. */
/* 1px vertical is OFF-SCALE ON PURPOSE — inline code sits INSIDE a line of running
   prose; any larger vertical pad makes the tinted box overlap the lines above and
   below. Horizontal is on the scale. */
.ds-247420 .chat-tick { font-family: var(--ff-mono); font-size: .92em; background: color-mix(in oklab, var(--fg) 10%, transparent); padding: 1px var(--space-1-75); border-radius: var(--r-1); }
/* Structured tool/code output earns more width than running prose (claude.ai/code
   lets diffs/tables break out wider than the reading measure). A flat turn that
   contains a tool card or code block widens itself AND its stack past --measure;
   prose-only turns stay clamped. */
.ds-247420 .chat-msg-flat:has(.chat-tool),
.ds-247420 .chat-msg-flat:has(.chat-bubble.chat-code) { max-width: calc(var(--measure) + 14ch); }
.ds-247420 .chat-msg-flat:has(.chat-tool) .chat-stack,
.ds-247420 .chat-msg-flat:has(.chat-bubble.chat-code) .chat-stack { max-width: calc(var(--measure) + 14ch); }
/* Hover tint exceeds each role's REST tint so the row gives feedback (and the
   ladder stays monotonic): assistant 5% > its 3% rest; user 4% > transparent. */
.ds-247420 .chat-msg-flat.them:hover { background: color-mix(in oklab, var(--fg) 5%, transparent); }
.ds-247420 .chat-msg-flat.you:hover { background: color-mix(in oklab, var(--fg) 4%, transparent); }
/* A turn whose only assistant content is a bordered tool card or code block
   already carries its own --bg surface + rule border; the turn-level .them tint
   would stack a second, wider-than-measure surface behind it. Suppress it so the
   card is the single surface (claude.ai/code keeps tool cards on bare canvas).
   Selectors mirror the breakout pair above so suppression + widening always
   cover the same turns. */
.ds-247420 .chat-msg-flat.them:has(.chat-tool),
.ds-247420 .chat-msg-flat.them:has(.chat-bubble.chat-code),
.ds-247420 .chat-msg-flat.them:has(.chat-tool):hover,
.ds-247420 .chat-msg-flat.them:has(.chat-bubble.chat-code):hover { background: none; }
.ds-247420 .chat-msg-flat:hover .chat-bubble { transform: none; box-shadow: none; background: none; }
.ds-247420 .chat-msg-flat .chat-avatar { display: none; }

/* Tool-call card: a bordered, status-toned, scannable card (claude.ai/code),
   not the raw mascot-tinted details dump. The node keeps the .chat-bubble
   class, so these rules de-frame it and restyle. Placed after the flat-bubble
   reset so they win on equal specificity. */
.ds-247420 .chat-msg .chat-tool {
  display: block; max-width: 100%; padding: 0; margin: var(--space-1) 0;
  background: var(--bg); color: var(--fg);
  border: var(--bw-hair) solid var(--rule); border-radius: var(--r-2);
  box-shadow: none; overflow: hidden;
}
.ds-247420 .chat-msg .chat-tool[open] { background: var(--bg); }
.ds-247420 .chat-msg .chat-tool .chat-tool-head {
  display: flex; align-items: center; gap: var(--space-2);
  padding: var(--space-2) var(--space-3); cursor: pointer;
  font-size: var(--fs-sm); list-style: none;
}
.ds-247420 .chat-msg .chat-tool .chat-tool-head::-webkit-details-marker { display: none; }
.ds-247420 .chat-msg .chat-tool .chat-tool-head:hover { background: var(--bg-2); }
.ds-247420 .chat-msg .chat-tool .chat-tool-head:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: calc(var(--focus-offset) * -1); }
.ds-247420 .chat-tool-icon { display: inline-flex; color: var(--fg-3); }
.ds-247420 .chat-tool-name { font-family: var(--ff-mono); font-weight: 600; color: var(--fg); }
.ds-247420 .chat-tool-label { color: var(--fg-3); font-size: var(--fs-tiny); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.ds-247420 .chat-tool-status {
  margin-left: auto; flex: 0 0 auto;
  font-size: var(--fs-micro); font-weight: 700; text-transform: uppercase; letter-spacing: var(--tr-caps);
  color: var(--fg-3);
  padding: var(--space-hair) var(--space-2); border-radius: var(--r-pill);
  background: color-mix(in oklab, var(--fg) 6%, transparent);
}
.ds-247420 .chat-msg .chat-tool.tool-running .chat-tool-status { color: var(--accent-ink); background: color-mix(in oklab, var(--accent) 12%, transparent); }
.ds-247420 .chat-msg .chat-tool.tool-error .chat-tool-status { color: var(--flame); background: color-mix(in oklab, var(--flame) 12%, transparent); }
/* Completed tool: a positive (success) tone so done reads distinctly from the
   neutral pill and from the accent-toned running pill. --success/--green-2
   measures under 4.5:1 as TEXT on paper/bg-2 (fine at 12%-tint fill/non-text
   use) - --green is the text-safe deep tone, same split the accent/accent-ink
   pair already uses. */
.ds-247420 .chat-msg .chat-tool.tool-done .chat-tool-status { color: var(--green); background: color-mix(in oklab, var(--success) 12%, transparent); }
.ds-247420 .chat-tool-body { border-top: var(--bw-hair) solid var(--rule); padding: var(--space-2) var(--space-3); display: flex; flex-direction: column; gap: var(--space-2); }
/* details' intrinsic min-content sizing: an unbroken token in the pre must not
   widen the tool card past the bubble column. */
.ds-247420 .chat-msg .chat-tool, .ds-247420 .chat-tool-body, .ds-247420 .chat-tool-section { min-width: 0; max-width: 100%; }
.ds-247420 .chat-tool-section { display: flex; flex-direction: column; gap: var(--space-1); }
.ds-247420 .chat-tool-section-label { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); font-size: var(--fs-tiny); font-weight: 600; text-transform: uppercase; letter-spacing: var(--tr-caps); color: var(--fg-3); }
.ds-247420 .chat-tool-copy { position: static; opacity: 0; }
.ds-247420 .chat-tool-section:hover .chat-tool-copy, .ds-247420 .chat-tool-section:focus-within .chat-tool-copy { opacity: 1; }
.ds-247420 .chat-tool-copy:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: 1px; opacity: 1; }
.ds-247420 .chat-tool-pre { margin: 0; padding: var(--space-2); background: var(--bg-2); border-radius: var(--r-1); font-family: var(--ff-mono); font-size: var(--fs-tiny); line-height: 1.45; overflow-x: auto; max-height: 320px; overflow-y: auto; }
.ds-247420 .chat-tool-pre.is-error { color: var(--flame); }
.ds-247420 .chat-tool-pre.chat-tool-empty { color: var(--fg-3); }

/* Fenced-code language tab on every rendered-markdown block (not just the
   structured CodeNode). injectCodeCopy reads the language- class. */
.ds-247420 .chat-code-block { position: relative; }
.ds-247420 .chat-code-lang {
  position: absolute; top: 0; left: 0; z-index: var(--z-raised);
  padding: var(--space-hair) var(--space-2); border-bottom-right-radius: var(--r-1);
  font-family: var(--ff-mono); font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: var(--tr-caps);
  color: var(--fg-3); background: color-mix(in oklab, var(--fg) 6%, transparent);
  pointer-events: none;
}
.ds-247420 .chat-code-block pre { padding-top: calc(var(--space-3) + 4px); }

/* Motion + microinteraction polish (all reduced-motion guarded). */
.ds-247420 .ds-session-row { transition: background var(--dur-snap) var(--ease), box-shadow var(--dur-snap) var(--ease); }
@media (prefers-reduced-motion: no-preference) {
  .ds-247420 .agentchat-thread { scroll-behavior: smooth; }
  .ds-247420 .ds-alert { animation: ds-alert-in var(--dur-base) var(--ease); }
}
@keyframes ds-alert-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
.ds-247420 .chat-code-copy { transition: opacity var(--dur-snap, .12s) var(--ease, ease), color var(--dur-snap) var(--ease), border-color var(--dur-snap) var(--ease), background var(--dur-snap) var(--ease); }
.ds-247420 .chat-msg-action { transition: background var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease); }
.ds-247420 .agentchat-install-copy { transition: background var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease); }
@media (prefers-reduced-motion: no-preference) {
  .ds-247420 .btn.is-armed, .ds-247420 .btn-primary.is-armed { animation: ds-arm-pulse var(--dur-slow) var(--ease); }
}
@keyframes ds-arm-pulse { 0% { box-shadow: 0 0 0 0 color-mix(in oklab, var(--warn) 55%, transparent); } 100% { box-shadow: 0 0 0 6px color-mix(in oklab, var(--warn) 0%, transparent); } }
.ds-247420 .ds-dash-card.is-new { box-shadow: inset 2px 0 0 var(--accent), 0 0 0 1px color-mix(in oklab, var(--accent) 40%, transparent); }
/* Error card carries the strongest left-bar tone (flame > stale amber > active
   accent), placed last so its inset bar wins source order over is-active/is-stale.
   The full-perimeter border-color (line ~396) stays for extra severity emphasis. */
.ds-247420 .ds-dash-card.is-error { box-shadow: inset 2px 0 0 var(--flame); }
/* A newly-arrived AND errored card: keep the LEFT inset bar flame (severity
   precedence, never accent) while restoring the is-new arrival outline as a
   full-perimeter accent glow that does not collide with the flame bar. Placed
   after both is-new and is-error so it wins source order. */
.ds-247420 .ds-dash-card.is-new.is-error { box-shadow: inset 2px 0 0 var(--flame), 0 0 0 1px color-mix(in oklab, var(--accent) 40%, transparent); }
@media (prefers-reduced-motion: no-preference) {
  .ds-247420 .ds-dash-card.is-new { animation: ds-card-in var(--dur-slow) var(--ease); }
}
@keyframes ds-card-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

/* Live command-center: status-bucket groups + header breakdown + heartbeat. */
.ds-247420 .ds-dash-group { display: flex; flex-direction: column; gap: var(--space-2); }
.ds-247420 .ds-dash-group-label { font-size: var(--fs-tiny); font-weight: 600; text-transform: uppercase; letter-spacing: var(--tr-label); color: var(--fg-3); padding: 0 var(--space-1); }
.ds-247420 .ds-dash-breakdown { display: flex; align-items: center; gap: var(--space-1); font-size: var(--fs-sm); color: var(--fg-2); }
/* Shape-channel discs mirror the .status-dot-disc vocabulary (solid /
   solid+halo / hollow ring) so a colour-blind user can scan the breakdown by
   shape, not hue alone. CSS-drawn discs, not glyphs (glyph policy clean). */
.ds-247420 .ds-dash-breakdown .seg { display: inline-flex; align-items: center; gap: var(--space-1); font-weight: 600; }
.ds-247420 .ds-dash-breakdown .seg::before { content: ''; width: 7px; height: 7px; border-radius: 50%; flex: none; }
.ds-247420 .ds-dash-breakdown .seg.is-running { color: var(--accent-ink); }
.ds-247420 .ds-dash-breakdown .seg.is-running::before { background: var(--accent); box-shadow: 0 0 0 1.5px color-mix(in oklab, var(--accent) 30%, transparent); }
.ds-247420 .ds-dash-breakdown .seg.is-error { color: var(--flame); }
.ds-247420 .ds-dash-breakdown .seg.is-error::before { background: var(--flame); box-shadow: 0 0 0 1.5px color-mix(in oklab, var(--flame) 38%, transparent); }
.ds-247420 .ds-dash-breakdown .seg.is-idle { color: var(--stale); }
.ds-247420 .ds-dash-breakdown .seg.is-idle::before { background: transparent; box-shadow: inset 0 0 0 1px var(--stale), inset 0 0 0 3px var(--bg); }
.ds-247420 .ds-dash-stream-disc { display: inline-flex; align-items: center; gap: var(--space-1); }
.ds-247420 .ds-dash-selectall { display: inline-flex; align-items: center; gap: var(--space-1); font-size: var(--fs-tiny); color: var(--fg-2); cursor: pointer; background: none; border: none; padding: var(--space-1); }
.ds-247420 .ds-dash-selectall:hover { color: var(--fg); }
.ds-247420 .ds-dash-selectall:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: 1px; }
.ds-247420 .ds-dash-clear { background: none; border: none; color: var(--fg-3); cursor: pointer; font-size: var(--fs-tiny); text-decoration: underline dotted; padding: var(--space-1); }
.ds-247420 .ds-dash-clear:hover { color: var(--fg); }
.ds-247420 .ds-dash-clear:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: 1px; border-radius: var(--r-1); }

/* Conversation-rail loading skeleton (cold ccsniff walk). */
.ds-247420 .ds-session-row-skeleton { display: flex; flex-direction: column; gap: var(--space-1-75); padding: var(--space-2) var(--space-3); }
.ds-247420 .ds-session-row-skeleton .ds-skel { background: var(--bg-3); border-radius: var(--r-1); }
.ds-247420 .ds-session-row-skeleton .ds-skel-title { height: 12px; width: 70%; }
.ds-247420 .ds-session-row-skeleton .ds-skel-meta { height: 9px; width: 45%; }
@media (prefers-reduced-motion: no-preference) {
  .ds-247420 .ds-session-row-skeleton .ds-skel { animation: ds-skel-shimmer 1.3s ease-in-out infinite; background: linear-gradient(90deg, var(--bg-3) 0%, var(--bg-2) 50%, var(--bg-3) 100%); background-size: 200% 100%; }
}
@keyframes ds-skel-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }

/* Agent-picker list loading skeleton (shares the .ds-skel shimmer base above). */
.ds-247420 .ds-agent-list-skeleton { display: flex; flex-direction: column; gap: var(--space-2); }
.ds-247420 .ds-agent-row-skeleton { display: flex; align-items: center; gap: var(--space-2); padding: var(--space-2) var(--space-3); }
.ds-247420 .ds-agent-row-skeleton .ds-skel { background: var(--bg-3); border-radius: var(--r-1); }
.ds-247420 .ds-agent-row-skeleton .ds-skel-icon { height: 18px; width: 18px; flex: none; border-radius: var(--r-round, 50%); }
.ds-247420 .ds-agent-row-skeleton .ds-skel-title { height: 12px; width: 40%; }
.ds-247420 .ds-agent-row-skeleton .ds-skel-meta { height: 9px; width: 25%; margin-left: auto; }
.ds-247420 .ds-agent-list-skeleton-status { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); }
@media (prefers-reduced-motion: no-preference) {
  .ds-247420 .ds-agent-row-skeleton .ds-skel { animation: ds-skel-shimmer 1.3s ease-in-out infinite; background: linear-gradient(90deg, var(--bg-3) 0%, var(--bg-2) 50%, var(--bg-3) 100%); background-size: 200% 100%; }
}

/* File code preview: paint Prism tokens (the bundle scoped them to the chat
   block only) + an optional non-selectable line-number gutter. */
.ds-247420 .ds-preview-code code { color: var(--fg); }
.ds-247420 .ds-preview-code .token.comment, .ds-247420 .ds-preview-code .token.prolog, .ds-247420 .ds-preview-code .token.doctype, .ds-247420 .ds-preview-code .token.cdata { color: var(--fg-3); }
.ds-247420 .ds-preview-code .token.punctuation { color: var(--fg-2); }
.ds-247420 .ds-preview-code .token.property, .ds-247420 .ds-preview-code .token.tag, .ds-247420 .ds-preview-code .token.boolean, .ds-247420 .ds-preview-code .token.number, .ds-247420 .ds-preview-code .token.constant, .ds-247420 .ds-preview-code .token.symbol { color: var(--purple-2); }
.ds-247420 .ds-preview-code .token.selector, .ds-247420 .ds-preview-code .token.attr-name, .ds-247420 .ds-preview-code .token.string, .ds-247420 .ds-preview-code .token.char, .ds-247420 .ds-preview-code .token.builtin { color: var(--code-string, var(--green)); }
.ds-247420 .ds-preview-code .token.atrule, .ds-247420 .ds-preview-code .token.attr-value, .ds-247420 .ds-preview-code .token.keyword { color: var(--code-keyword, var(--sky)); }
.ds-247420 .ds-preview-code .token.function, .ds-247420 .ds-preview-code .token.class-name { color: var(--code-fn, var(--flame)); }
.ds-247420 .ds-preview-code.has-gutter { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0 var(--space-2); }
.ds-247420 .ds-preview-code.has-gutter code { display: block; min-width: 0; overflow-x: auto; }
.ds-247420 .ds-preview-gutter { user-select: none; text-align: right; padding: 0 var(--space-2) 0 0; color: var(--fg-3); border-right: var(--bw-hair) solid var(--rule); font-family: var(--ff-mono); white-space: pre; line-height: inherit; }

/* Image preview: fit/actual control + alpha checkerboard. */
.ds-247420 .ds-preview-media-controls { display: flex; gap: var(--space-2); padding: var(--space-2); justify-content: center; }
.ds-247420 .ds-preview-media-alpha {
  background-image: linear-gradient(45deg, var(--bg-3) 25%, transparent 25%), linear-gradient(-45deg, var(--bg-3) 25%, transparent 25%), linear-gradient(45deg, transparent 75%, var(--bg-3) 75%), linear-gradient(-45deg, transparent 75%, var(--bg-3) 75%);
  background-size: 16px 16px; background-position: 0 0, 0 8px, 8px -8px, -8px 0;
}
.ds-247420 .ds-preview-media.is-actual { max-width: none; max-height: none; }

/* ----------------------------------------------------------------------------
   Compact-button tiers — ONE spec for the small quiet rect buttons that eight
   surfaces used to clone with drifting metrics/tones. chat.css loads after
   app-shell.css in the bundle, so these same-specificity rules win by order.
   Tier A (toolbar-grade, 32px) matches .ds-file-toolbar .btn, the reference;
   tier B (inline-row-grade, 24px) for actions inside dense row strips.
   Hover/focus/active-state rules on the originals still apply.
---------------------------------------------------------------------------- */
.ds-247420 .row-act, .ds-247420 .ds-dash-errors-toggle, .ds-247420 .agentchat-export-act, .ds-247420 .ds-file-more-btn {
  min-height: 32px; padding: var(--space-1-5) var(--space-2-75); border-radius: var(--r-1);
  font-family: var(--ff-body); font-size: var(--fs-tiny); font-weight: 500;
  background: transparent; border: var(--bw-hair) solid var(--rule); color: var(--fg-2);
  cursor: pointer;
}
.ds-247420 .row-act:hover, .ds-247420 .ds-dash-errors-toggle:hover, .ds-247420 .agentchat-export-act:hover, .ds-247420 .ds-file-more-btn:hover {
  background: var(--bg-2); color: var(--fg);
}
.ds-247420 .agentchat-install-copy, .ds-247420 .ds-upload-act, .ds-247420 .ds-session-meta-copy {
  min-height: 24px; padding: var(--space-hair) var(--space-2); border-radius: var(--r-1);
  font-family: var(--ff-body); font-size: var(--fs-tiny);
  background: transparent; border: var(--bw-hair) solid var(--rule); color: var(--fg-3);
  cursor: pointer;
}
.ds-247420 .agentchat-install-copy:hover, .ds-247420 .ds-upload-act:hover, .ds-247420 .ds-session-meta-copy:hover {
  background: var(--bg-2); color: var(--fg);
}
@media (pointer: coarse) {
  .ds-247420 .row-act, .ds-247420 .ds-dash-errors-toggle, .ds-247420 .agentchat-export-act, .ds-247420 .ds-file-more-btn, .ds-247420 .agentchat-install-copy, .ds-247420 .ds-upload-act, .ds-247420 .ds-session-meta-copy { min-height: 44px; }
}

/* 11th run: ultrawide widens the reading measure where there is room. */
@media (min-width: 1600px) {
  .ds-247420 .agentchat-thread { --measure: 84ch; }
}

/* Stop/cancel composer control: re-tone the accent send button as a quiet
   neutral control so 'stop generating' reads as distinct from accent send.
   Flame/warn stay reserved (flame=error STATUS, warn=destructive ACTION); a
   neutral quiet tone is the claude-code-aligned read for an in-flight stop. */
.ds-247420 .chat-composer .send.cancel {
  background: var(--bg-3);
  color: var(--fg);
  box-shadow: inset 0 0 0 var(--bw-hair, 1px) var(--rule-strong, var(--rule));
}
.ds-247420 .chat-composer .send.cancel:hover {
  background: color-mix(in oklab, var(--fg) 8%, var(--bg-3));
  filter: none;
}
.ds-247420 .chat-composer .send.cancel:active { filter: brightness(0.96); }

/* ChatMinimap — scroll-position density overview beside a chat thread.
   Purely token-driven (no raw color literals — see lintTokensOrThrow). Dot
   position/tooltip offsets are set imperatively in JS (chat-minimap.js) since
   they are per-render numeric layout, not a static rule. */
.ds-247420 .chat-minimap {
  position: relative;
  flex-shrink: 0;
  align-self: stretch;
  cursor: default;
  user-select: none;
  border-left: var(--bw-hair) solid var(--rule);
  background: var(--bg-2);
  overflow: visible;
}
.ds-247420 .chat-minimap-viewport {
  position: absolute;
  left: 0;
  right: 0;
  background: color-mix(in oklab, var(--fg-3) 12%, transparent);
  border-top: var(--bw-hair) solid var(--rule);
  border-bottom: var(--bw-hair) solid var(--rule);
  pointer-events: none;
  /* Minimap internal order: centerline < viewport < dot < tooltip. All four
     live inside the one raised widget, so they share its rung and differ
     only by a local offset. */
  z-index: var(--z-raised-1);
}
.ds-247420 .chat-minimap-centerline {
  position: absolute;
  left: 50%;
  top: 0;
  bottom: 0;
  width: 1px;
  background: var(--rule);
  transform: translateX(-50%);
  z-index: var(--z-raised);
}
.ds-247420 .chat-minimap-dot {
  position: absolute;
  left: 50%;
  width: 6px;
  height: 6px;
  border-radius: var(--r-pill, 999px);
  transform: translate(-50%, -50%) scale(1);
  transition: transform 0.1s;
  z-index: var(--z-raised-2);
}
.ds-247420 .chat-minimap-dot.is-user {
  background: color-mix(in oklab, var(--accent) 18%, transparent);
  border: 1.5px solid color-mix(in oklab, var(--accent) 70%, transparent);
}
.ds-247420 .chat-minimap-dot.is-assistant {
  background: color-mix(in oklab, var(--fg-3) 12%, transparent);
  border: 1.5px solid color-mix(in oklab, var(--fg-3) 50%, transparent);
}
.ds-247420 .chat-minimap-dot.is-nearest { transform: translate(-50%, -50%) scale(1.6); }
.ds-247420 .chat-minimap-tooltip {
  position: absolute;
  right: 100%;
  margin-right: var(--space-1);
  background: var(--bg);
  border-top: var(--bw-hair) solid var(--rule);
  border-right: var(--bw-hair) solid var(--rule);
  border-bottom: var(--bw-hair) solid var(--rule);
  border-left: 2px solid var(--fg-3);
  border-radius: var(--r-1);
  padding: calc(var(--space-1) / 2) var(--space-2);
  /* Top of the minimap's internal order. Deliberately NOT --z-tooltip: it is
     clipped to the minimap rail rather than floating over the app, so it must
     not outrank real overlays that may cover the rail. */
  z-index: var(--z-raised-3);
  pointer-events: none;
  opacity: 0.45;
  font-size: var(--fs-tiny);
  color: var(--fg-3);
  line-height: 1.4;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: top 0.1s, opacity 0.1s;
}
.ds-247420 .chat-minimap-tooltip.is-user { border-left-color: color-mix(in oklab, var(--accent) 70%, transparent); }
.ds-247420 .chat-minimap-tooltip.is-assistant { border-left-color: color-mix(in oklab, var(--fg-3) 50%, transparent); }
.ds-247420 .chat-minimap-tooltip.is-nearest { opacity: 1; color: var(--fg); }

/* editor-primitives.css */
/* editor-primitives.css — chrome for in-engine editors, inspectors, IDEs,
   debug HUDs. All rules under .ds-247420 scope (build prefixes). Tokens
   resolved from colors_and_type.css: --panel-{0..3}, --panel-text(-2/-3),
   --accent, --accent-fg, --rule, --rule-strong, --r-1/-2, --ff-mono. */

/* ---------------------------------------------------------------
   Docked panel — the engine-editor idiom: flush, square, dense,
   full-height. Unlike the dashboard .panel card (rounded, margined,
   shadowed) this docks edge-to-edge with a 1px --rule seam. Use for
   inspector / hierarchy / apps / events columns inside an editor.
   --------------------------------------------------------------- */
.ds-247420 .ds-ep-panel {
    display: flex; flex-direction: column;
    min-height: 0; height: 100%;
    margin: 0; border-radius: 0; box-shadow: none;
    background: var(--panel-1);
    color: var(--panel-text);
    font: var(--fs-tiny, 12px)/var(--lh-base, 1.55) var(--ff-mono, monospace);
    overflow: hidden;
}
.ds-247420 .ds-ep-panel-head {
    flex-shrink: 0;
    display: flex; align-items: center; justify-content: space-between; gap: var(--space-2);
    padding: var(--space-1-75) var(--space-2-5);
    border-bottom: 1px solid var(--rule);
    font-size: var(--fs-micro, 11px); font-weight: 600;
    text-transform: uppercase; letter-spacing: 0.1em;
    color: var(--panel-text-3);
    white-space: nowrap;
}
.ds-247420 .ds-ep-panel-head > :last-child {
    font-weight: 500; letter-spacing: 0;
    color: var(--panel-text-2);
    overflow: hidden; text-overflow: ellipsis;
}
.ds-247420 .ds-ep-panel-body {
    flex: 1; min-height: 0; overflow: auto;
    padding: var(--space-1-75) var(--space-2);
}
.ds-247420 .ds-ep-panel-body.flush { padding: 0; }
/* A sub-section divider inside a docked panel (e.g. app-prop mount). Only
   shows its top seam when it actually has content — an empty section is
   collapsed so no stray dashed rule floats over an empty inspector. */
.ds-247420 .ds-ep-panel-section {
    flex-shrink: 0;
    border-top: 1px solid var(--rule);
    overflow: auto;
}
.ds-247420 .ds-ep-panel-section:empty { display: none; border-top: 0; }

/* Dense status strip for editor chrome — thin, flush, bordered top. */
.ds-247420 .ds-ep-statusbar {
    flex-shrink: 0;
    display: flex; align-items: center; justify-content: space-between; gap: var(--space-2);
    min-height: 0; height: 26px;
    padding: 0 var(--space-2-5);
    border-top: 1px solid var(--rule);
    background: var(--panel-1);
    color: var(--panel-text-2);
    font: var(--fs-micro, 11px)/1 var(--ff-mono, monospace);
}
.ds-247420 .ds-ep-statusbar-left, .ds-247420 .ds-ep-statusbar-right { display: flex; align-items: center; gap: var(--space-2); min-width: 0; }
.ds-247420 .ds-ep-statusbar-left { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Buttons inside the dense strip shrink to fit the 26px band. */
.ds-247420 .ds-ep-statusbar button {
    min-height: 0; height: 20px; padding: 0 var(--space-2);
    border-radius: var(--r-1, 8px);
    font: var(--fs-micro, 11px)/1 var(--ff-mono, monospace);
}

/* Toolbar */
.ds-247420 .ds-ep-toolbar {
    display: flex; align-items: center; gap: var(--space-1-75);
    padding: var(--space-2) var(--space-2-5);
    background: var(--panel-1);
    color: var(--panel-text);
    border-bottom: 1px solid var(--rule);
    font: var(--fs-tiny, 12px)/var(--lh-base, 1.55) var(--ff-mono, monospace);
}
.ds-247420 .ds-ep-toolbar.dense { padding: var(--space-hair) var(--space-1); gap: var(--space-hair); }
.ds-247420 .ds-ep-toolbar-leading,
.ds-247420 .ds-ep-toolbar-center,
.ds-247420 .ds-ep-toolbar-trailing { display: flex; align-items: center; gap: var(--space-1); }
.ds-247420 .ds-ep-toolbar-trailing { margin-left: auto; }
.ds-247420 .ds-ep-toolbar-center { flex: 1; min-width: 0; }

/* ToolbarRow — flat wrapping action row, no slot structure. */
.ds-247420 .ds-ep-toolbar-row {
    display: flex; gap: var(--space-2, 8px); align-items: center; flex-wrap: wrap;
    margin-bottom: var(--space-2-5, 10px);
}
.ds-247420 .ds-ep-toolbar-row input,
.ds-247420 .ds-ep-toolbar-row select,
.ds-247420 .ds-ep-toolbar-row textarea {
    background: var(--panel-2, var(--bg-2));
    border: 1px solid var(--rule);
    color: var(--panel-text, var(--fg));
    padding: var(--space-1-5, 5px) var(--space-2-5, 10px);
    border-radius: var(--r-1, 8px);
    font-size: var(--fs-tiny, 12px);
    font-family: inherit;
}
.ds-247420 .ds-ep-toolbar-row input { flex: 1; min-width: 140px; }
.ds-247420 .ds-ep-toolbar-row input:focus-visible,
.ds-247420 .ds-ep-toolbar-row select:focus-visible,
.ds-247420 .ds-ep-toolbar-row textarea:focus-visible {
    outline: none; border-color: var(--accent);
}

/* Tabs */
.ds-247420 .ds-ep-tabs { display: flex; flex-direction: column; min-height: 0; height: 100%; position: relative; }
.ds-247420 .ds-ep-tabs-head {
    display: flex; flex-shrink: 0; flex-wrap: nowrap;
    overflow-x: auto; scroll-behavior: smooth;
    background: var(--panel-1);
    border-bottom: 1px solid var(--rule);
    position: relative;
}
/* Sliding indicator — a single 2px bar under the active tab that animates its
   left/width between tabs (webgeist's "expensive" underline-slide), instead of
   each tab toggling its own static border. A REAL child element positioned by
   the Tabs ref (inline left/width/top). It lives as a child of the OUTER
   .ds-ep-tabs column (position:relative), NOT the .ds-ep-tabs-head, because an
   absolutely-positioned child of a horizontal flex container mis-sizes to 0 in
   Chromium; the outer column is a vertical flex, which sizes it correctly.
   The ref sets top to the head's height so it sits on the head's bottom edge.
   .has-slider gates the static per-tab border off (below). */
.ds-247420 .ds-ep-tab-indicator {
    /* Sized by left+right insets (set inline by positionSlider), NOT width — an
       abspos element's `width` computes to 0 in some flex-sibling layouts in
       Chromium, whereas insets size reliably. left+right at the same value
       collapse it to 0 until positioned (no-JS: invisible). */
    position: absolute; top: 0; left: 0; right: 100%; height: 2px;
    background: var(--accent);
    transition: left var(--dur-base, 160ms) var(--ease, ease),
                right var(--dur-base, 160ms) var(--ease, ease);
    pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
    .ds-247420 .ds-ep-tab-indicator { transition: none; }
}
/* scroll: content-sized tabs (min/max-width, no stretch) so overflow-x on the
   head has real per-tab width to scroll against — the pi-web TabBar shape for
   an unbounded open-file/open-session strip, vs. the default fixed small set
   that stretches evenly across the available width. */
.ds-247420 .ds-ep-tabs-head.scroll .ds-ep-tab-wrap { flex: 0 0 auto; min-width: 80px; max-width: 180px; }
.ds-247420 .ds-ep-tabs-head.scroll .ds-ep-tab { flex: 1; min-width: 0; }
.ds-247420 .ds-ep-tab-wrap { display: flex; align-items: stretch; flex: 1; min-width: 0; }
.ds-247420 .ds-ep-tab {
    flex: 1; min-width: 0; appearance: none; background: transparent; border: 0;
    border-bottom: 2px solid transparent;
    color: var(--panel-text-3);
    padding: var(--space-2) var(--space-2-5);
    font: inherit; cursor: pointer;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    transition: color var(--dur-snap, 80ms) var(--ease, ease), border-color var(--dur-snap, 80ms) var(--ease, ease);
}
.ds-247420 .ds-ep-tab:hover { color: var(--panel-text-2); }
/* Close button — hidden until hover/focus within the tab, matching pi-web's
   hover-reveal close affordance; always visible on the active tab so touch/
   keyboard users without hover can still find it. */
.ds-247420 .ds-ep-tab-close {
    appearance: none; background: transparent; border: 0; border-radius: var(--r-0);
    display: flex; align-items: center; justify-content: center;
    width: 22px; height: 22px; margin: var(--space-1-75) var(--space-1) var(--space-1-75) 0; padding: 0;
    flex-shrink: 0; color: var(--panel-text-3); cursor: pointer;
    opacity: 0; transition: opacity var(--dur-snap, 80ms) var(--ease, ease), background var(--dur-snap, 80ms) var(--ease, ease), color var(--dur-snap, 80ms) var(--ease, ease);
}
.ds-247420 .ds-ep-tab-wrap:hover .ds-ep-tab-close,
.ds-247420 .ds-ep-tab-wrap:focus-within .ds-ep-tab-close,
.ds-247420 .ds-ep-tab-wrap.active .ds-ep-tab-close { opacity: 1; }
.ds-247420 .ds-ep-tab-close:hover { background: var(--panel-2); color: var(--panel-text-1); }
.ds-247420 .ds-ep-tab-close:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; opacity: 1; }
.ds-247420 .ds-ep-tab.active {
    /* --accent-ink for the TEXT, bare --accent for the RULE. The lime fill
       measures 1.07:1 against paper, so as a text colour it is invisible in
       light theme; --accent-ink is 8.85:1. Both resolve to the same value in
       dark, which is exactly why this class of bug hides until someone opens
       the light theme. Borders are fills and keep the lead accent. */
    color: var(--accent-ink);
    border-bottom-color: var(--accent);
}
/* When the JS slider is active, the active tab's own static border is
   redundant (the sliding bar draws it) — hide it so they don't double up. */
.ds-247420 .ds-ep-tabs-head.has-slider .ds-ep-tab.active { border-bottom-color: transparent; }
/* Flex column so a full-height docked child (e.g. an inspector panel) fills
   the tab body instead of collapsing under a block container. */
.ds-247420 .ds-ep-tabs-body { flex: 1; min-height: 0; overflow: auto; display: flex; flex-direction: column; }

/* EventLog — dense console-style log rows (one event per line). */
.ds-247420 .ds-ep-eventlog {
    display: flex; flex-direction: column;
    font: var(--fs-tiny, 12px)/1.4 var(--ff-mono, monospace);
    color: var(--panel-text);
}
.ds-247420 .ds-ep-eventrow {
    display: flex; align-items: baseline; gap: var(--space-2);
    padding: var(--space-half) var(--space-2);
    border-bottom: 1px solid color-mix(in oklab, var(--rule) 50%, transparent);
}
.ds-247420 .ds-ep-eventrow:hover { background: color-mix(in oklab, var(--accent) 6%, transparent); }
.ds-247420 .ds-ep-eventrow-type { color: var(--panel-text); font-weight: 600; flex-shrink: 0; }
.ds-247420 .ds-ep-eventrow-sub { color: var(--panel-text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* TreeView / TreeItem */
.ds-247420 .ds-ep-tree {
    display: flex; flex-direction: column;
    color: var(--panel-text);
    font: var(--fs-tiny, 12px)/var(--lh-base, 1.55) var(--ff-mono, monospace);
    overflow: auto;
}
.ds-247420 .ds-ep-tree-item { display: flex; flex-direction: column; }
.ds-247420 .ds-ep-tree-item:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
.ds-247420 .ds-ep-tree-row {
    display: flex; align-items: center; gap: var(--space-1-75);
    padding: var(--space-1) var(--space-2) var(--space-1) 0;
    border-radius: var(--r-1, 8px);
    cursor: pointer;
    color: var(--panel-text-2);
    /* --ff-body (system-ui) falls back per-script to the OS's native CJK
       font on a row with CJK text, and CJK glyphs commonly ship a taller
       default line-height than Latin ones -- with no line-height set here,
       a row's height was purely intrinsic to its own font metrics, so a CJK
       row measured visibly taller than a plain-English row beside it. A
       fixed line-height normalizes every row to the same height regardless
       of script (2 is generous enough that CJK glyphs never clip). */
    line-height: 2;
    transition: background var(--dur-snap, 80ms) var(--ease, ease), color var(--dur-snap, 80ms) var(--ease, ease);
}
.ds-247420 .ds-ep-tree-row:hover {
    background: color-mix(in oklab, var(--accent) 8%, transparent);
    color: var(--panel-text);
}
.ds-247420 .ds-ep-tree-item.selected > .ds-ep-tree-row {
    /* The 22% accent wash is a FILL and stays on the lead accent; the label on
       top of it is text and takes --accent-ink (bare --accent is 1.07:1 on
       paper -- invisible in light theme, identical to --accent-ink in dark,
       which is why it reads fine until someone switches themes). */
    background: color-mix(in oklab, var(--accent) 22%, transparent);
    color: var(--accent-ink);
}
.ds-247420 .ds-ep-tree-twist {
    width: 12px; flex-shrink: 0; text-align: center;
    color: var(--panel-text-3);
    transition: transform var(--dur-snap, 80ms) var(--ease, ease);
}
.ds-247420 .ds-ep-tree-twist.open { transform: rotate(90deg); }
.ds-247420 .ds-ep-tree-glyph { flex-shrink: 0; color: var(--panel-text-3); }
.ds-247420 .ds-ep-tree-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ds-247420 .ds-ep-tree-tag {
    flex-shrink: 0; padding-left: var(--space-1-75);
    font-size: 0.85em; color: var(--panel-text-3);
    /* Numeric tags (star counts etc.) vary in digit/comma length row to row
       ("607 stars" vs "184,966 stars"). Each tag's right edge already sits
       flush against the row's right edge (flex-shrink:0 in a full-width
       row), but a shorter number's smaller total width shifts its LEFT
       edge inward -- so the "stars" label drifts left/right per row even
       though nothing is actually misaligned at the anchor point. A fixed
       (not min-) width + right-align + tabular-nums turns that into a real
       column: every tag reserves the SAME width regardless of its own text
       length (min-width alone still let longer strings grow past the
       floor, re-introducing drift for e.g. 5-digit vs 4-digit counts) --
       13ch covers "184,966 stars" (the realistic 6-digit-star ceiling for
       this dataset) with headroom; a longer string still wins over a hard
       clip via ch-based overflow rather than truncating the count itself. */
    width: 13ch;
    overflow: hidden;
    white-space: nowrap;
    text-align: right;
    font-variant-numeric: tabular-nums;
}
.ds-247420 .ds-ep-tree-children { display: flex; flex-direction: column; }

/* PropertyGrid / PropertyField */
.ds-247420 .ds-ep-propgrid {
    display: flex; flex-direction: column; gap: var(--space-1-75);
    padding: var(--space-2);
    color: var(--panel-text);
    font: var(--fs-tiny, 12px)/var(--lh-base, 1.55) var(--ff-mono, monospace);
}
.ds-247420 .ds-ep-propfield {
    display: grid;
    grid-template-columns: minmax(80px, 120px) 1fr;
    gap: var(--space-1) var(--space-2-5);
    align-items: center;
}
.ds-247420 .ds-ep-propfield.inline { display: flex; gap: var(--space-2); }
.ds-247420 .ds-ep-propfield-label {
    color: var(--panel-text-3);
    font-size: 0.85em;
    text-transform: uppercase;
    letter-spacing: 0.08em;
}
.ds-247420 .ds-ep-propfield-value {
    color: var(--panel-text);
    display: flex; align-items: center; gap: var(--space-1); min-width: 0;
}
.ds-247420 .ds-ep-propfield-value input,
.ds-247420 .ds-ep-propfield-value select,
.ds-247420 .ds-ep-propfield-value textarea {
    appearance: none;
    background: var(--panel-2);
    color: var(--panel-text);
    border: 1px solid var(--rule);
    border-radius: var(--r-1, 8px);
    padding: var(--space-1) var(--space-2);
    font: inherit;
    min-width: 0; width: 100%;
}
.ds-247420 .ds-ep-propfield-value input:focus-visible,
.ds-247420 .ds-ep-propfield-value select:focus-visible,
.ds-247420 .ds-ep-propfield-value textarea:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: var(--focus-ring-inset);
}
.ds-247420 .ds-ep-propfield-hint {
    grid-column: 1 / -1;
    font-size: 0.8em;
    color: var(--panel-text-3);
}
/* x/y/z packed on one row, each field equal width, no horizontal overflow. */
.ds-247420 .ds-ep-vec3 {
    display: flex; gap: var(--space-1); width: 100%; min-width: 0;
}
.ds-247420 .ds-ep-vec3 > * { flex: 1 1 0; min-width: 0; }
/* Each axis cell (axis letter + scrub input) must show its full 3-decimal value
   without clipping. The axis letter is a fixed micro-glyph; the input flexes to
   fill the rest with tight padding and centered text so 0.000 / -0.0 / 1.000 are
   always legible even in the narrow 3-up layout. */
.ds-247420 .ds-ep-vec3 > * {
    display: flex; align-items: center; gap: var(--space-hair);
}
.ds-247420 .ds-ep-vec3 .ds-ep-propfield-hint {
    flex: 0 0 auto; width: 9px; text-align: center;
    grid-column: auto; font-size: 0.8em; opacity: 0.7;
}
.ds-247420 .ds-ep-vec3 .ds-input-bare {
    flex: 1 1 auto; min-width: 0; width: auto;
    padding: var(--space-hair) var(--space-1); text-align: center; text-overflow: clip;
}
/* Dense numeric inputs in the inspector — compact, tight radius, mono. */
.ds-247420 .ds-ep-propfield-value input,
.ds-247420 .ds-ep-propfield-value .ds-input-bare {
    padding: var(--space-hair) var(--space-1-75);
    border-radius: var(--r-1, 8px);
    font: var(--fs-tiny, 12px)/1.3 var(--ff-mono, monospace);
}
.ds-247420 .ds-input-bare {
    appearance: none;
    background: var(--panel-2);
    color: var(--panel-text);
    border: 1px solid var(--rule);
    border-radius: var(--r-1, 8px);
    padding: var(--space-hair) var(--space-1-75);
    font: var(--fs-tiny, 12px)/1.3 var(--ff-mono, monospace);
    min-width: 0;
}
.ds-247420 .ds-input-bare:focus-visible { outline: none; border-color: var(--accent); box-shadow: var(--focus-ring-inset); }
/* A property field whose control spans the full panel width (label above,
   control below) — used for wide segmented controls that would clip in the
   narrow value column. */
.ds-247420 .ds-ep-propfield.block {
    display: flex; flex-direction: column; align-items: stretch; gap: var(--space-1);
}
.ds-247420 .ds-ep-propfield.block > .ds-ep-propfield-value { width: 100%; }
.ds-247420 .ds-ep-propfield.block .ds-ep-btngrp { width: 100%; }
.ds-247420 .ds-ep-propfield.block .ds-ep-btngrp-btn { flex: 1 1 0; text-align: center; }

/* PropertyGridRow — bordered-row variant for a list of independently-editable
   records (each record its own bottom-divided block, last one flush). */
.ds-247420 .ds-ep-propgrid-row {
    border-bottom: 1px solid var(--rule);
    padding: var(--space-2, 8px) 0;
}
.ds-247420 .ds-ep-propgrid-row:last-child { border-bottom: none; }

/* InlineEditableField — transparent-until-focus input, error state exposed
   both visually (danger border) and semantically (aria-invalid, read by the
   caller's own error text via aria-describedby if wired).
   Specificity note: `.ds-ep-propfield-value input` (descendant combinator,
   2 classes + 1 tag = (0,2,1) specificity) sets a visible border on every
   input nested in a PropertyField -- the exact idiom InlineEditableField is
   meant to be dropped into. A bare `input.ds-ep-inline-input` is only
   (0,1,1) and loses; `:where(.ds-ep-propfield-value, .ds-ep-inline-standalone)`
   is intentionally NOT used here because :where() has zero specificity and
   would still lose. Instead the selector repeats .ds-ep-inline-input twice
   (a valid, standard way to bump specificity by one class without adding an
   ambient wrapper dependency) to reliably out-rank the PropertyField rule
   regardless of nesting context. */
.ds-247420 .ds-ep-inline-input.ds-ep-inline-input {
    background: transparent;
    border: 1px solid transparent;
    color: inherit; font: inherit;
    width: 100%;
    border-radius: var(--r-1, 8px);
    padding: var(--space-hair, 2px) var(--space-1, 4px);
}
.ds-247420 .ds-ep-inline-input.ds-ep-inline-input:focus-visible {
    outline: none;
    border-color: var(--accent);
    background: var(--panel-2, var(--bg-2));
}
.ds-247420 .ds-ep-inline-input.ds-ep-inline-input.has-error,
.ds-247420 .ds-ep-inline-input.ds-ep-inline-input[aria-invalid="true"] {
    border-color: var(--danger);
}

/* Flush dense action bar docked at a panel bottom (inspector Edit/Delete etc.).
   Compact buttons, square-ish, bordered top — not big floating pills. */
.ds-247420 .ds-ep-actionbar {
    flex-shrink: 0;
    display: flex; align-items: center; gap: var(--space-1-75);
    padding: var(--space-1-75) var(--space-2);
    border-top: 1px solid var(--rule);
    background: var(--panel-1);
}
.ds-247420 .ds-ep-actionbar > * { flex: 1 1 0; }
.ds-247420 .ds-ep-actionbar button {
    width: 100%;
    /* Snapped to --ctl-sm (was a lone 28px literal that happened to match). */
    min-height: var(--ctl-sm); height: var(--ctl-sm); padding: 0 var(--space-2-5);
    border-radius: var(--r-1, 8px);
    font: var(--fs-tiny, 12px)/1 var(--ff-mono, monospace);
}

/* Inline buttons inside a tree row (e.g. app-list Place) and the editor toolbars
   are compact — small, square-ish, not big pills. Excludes the segmented
   IconButtonGroup which manages its own sizing. */
.ds-247420 .ds-ep-tree-row button:not(.ds-ep-btngrp-btn),
.ds-247420 .ds-ep-toolbar button:not(.ds-ep-btngrp-btn) {
    min-height: 24px; height: 24px; padding: 0 var(--space-2);
    border-radius: var(--r-1, 8px);
    font: var(--fs-tiny, 12px)/1 var(--ff-mono, monospace);
}
@media (pointer: coarse) {
    .ds-247420 .ds-ep-tree-row button:not(.ds-ep-btngrp-btn),
    .ds-247420 .ds-ep-toolbar button:not(.ds-ep-btngrp-btn) { min-height: 36px; height: 36px; }
}

/* Pager */
.ds-247420 .ds-ep-pager {
    display: flex; align-items: center; gap: var(--space-1-75, 6px);
    margin-top: var(--space-2, 8px);
    font: var(--fs-tiny, 12px)/1 var(--ff-mono, monospace);
    color: var(--panel-text-3, var(--fg-3));
}
/* 24px pointer-target floor on both pager controls. Measured live on a real
   paged table: the prev/next buttons rendered 36x21 and the numbered ones
   23x21 -- under the minimum target size in height for both, and in BOTH axes
   for the numbered case. Padding alone cannot guarantee the floor because
   font-size varies per consumer, so it is stated explicitly. */
.ds-247420 .ds-ep-pager-btn {
    appearance: none; cursor: pointer;
    min-height: 24px;
    background: var(--panel-2, var(--bg-2));
    border: 1px solid var(--rule);
    color: var(--panel-text, var(--fg));
    padding: var(--space-half, 3px) var(--space-2-5, 10px);
    border-radius: var(--r-1, 8px);
    font: inherit;
}
.ds-247420 .ds-ep-pager-btn:hover:not([disabled]) { background: color-mix(in oklab, var(--accent) 10%, transparent); }
.ds-247420 .ds-ep-pager-btn:disabled { opacity: 0.4; cursor: default; }
.ds-247420 .ds-ep-pager-label { white-space: nowrap; }

.ds-247420 .ds-ep-pager-numbered { flex-wrap: wrap; }
.ds-247420 .ds-ep-pager-num {
    appearance: none; cursor: pointer;
    min-height: 24px;
    background: var(--panel-2, var(--bg-2));
    border: 1px solid var(--rule);
    color: var(--panel-text, var(--fg));
    /* max() rather than a bare 1.75em: at the small font a pager uses, 1.75em
       computed to 23px -- just under the target floor. */
    min-width: max(24px, 1.75em);
    padding: var(--space-half, 3px) var(--space-1, 4px);
    border-radius: var(--r-1, 8px);
    font: inherit;
    text-align: center;
}
.ds-247420 .ds-ep-pager-num:hover { background: color-mix(in oklab, var(--accent) 10%, transparent); }
.ds-247420 .ds-ep-pager-num.is-current {
    background: var(--accent);
    color: var(--accent-ink);
    border-color: var(--accent);
}
.ds-247420 .ds-ep-pager-ellipsis { padding: 0 var(--space-hair, 2px); opacity: 0.6; }
/* Touch floor. Pager controls render ~21px tall, so on a coarse pointer they
   were the smallest interactive targets in the kit — every other control family
   here (.ds-sub-btn, .ds-session-row, .ds-file-act, .btn) already floors at
   44px, and paging is a repeated, mis-tap-prone action. The visible mark stays
   compact; only the hit area grows. */
@media (pointer: coarse) {
  .ds-247420 .ds-ep-pager-btn, .ds-247420 .ds-ep-pager-num { min-height: 44px; min-width: 44px; }
  .ds-247420 .ds-ep-pager { gap: var(--space-2); }
}
.ds-247420 .ds-ep-pager-total { white-space: nowrap; margin-left: auto; }

/* Grid / GridItem — 24-column responsive layout. GridItem's flex-basis is
   set per-tier via --xs-basis/--sm-basis/--md-basis/--lg-basis/--xl-basis
   custom properties (computed in JS from a 1-24 span); each @media tier
   below picks up whichever property is set for that breakpoint, cascading
   down from xs as the mobile-first default. Mirrors BP_SM/MD/LG/XL. */
.ds-247420 .ds-ep-grid {
    display: flex; flex-wrap: wrap;
    gap: var(--space-2, 8px);
}
.ds-247420 .ds-ep-grid-item {
    box-sizing: border-box;
    flex-grow: var(--xs-grow, 1);
    flex-basis: var(--xs-basis, 100%);
    max-width: var(--xs-basis, 100%);
    display: var(--xs-display, inherit);
}
@media (min-width: 480px) {
    .ds-247420 .ds-ep-grid-item[style*="--sm-basis"] {
        flex-grow: var(--sm-grow, 1);
        flex-basis: var(--sm-basis, 100%);
        max-width: var(--sm-basis, 100%);
        display: var(--sm-display, inherit);
    }
}
@media (min-width: 768px) {
    .ds-247420 .ds-ep-grid-item[style*="--md-basis"] {
        flex-grow: var(--md-grow, 1);
        flex-basis: var(--md-basis, 100%);
        max-width: var(--md-basis, 100%);
        display: var(--md-display, inherit);
    }
}
@media (min-width: 1024px) {
    .ds-247420 .ds-ep-grid-item[style*="--lg-basis"] {
        flex-grow: var(--lg-grow, 1);
        flex-basis: var(--lg-basis, 100%);
        max-width: var(--lg-basis, 100%);
        display: var(--lg-display, inherit);
    }
}
@media (min-width: 1440px) {
    .ds-247420 .ds-ep-grid-item[style*="--xl-basis"] {
        flex-grow: var(--xl-grow, 1);
        flex-basis: var(--xl-basis, 100%);
        max-width: var(--xl-basis, 100%);
        display: var(--xl-display, inherit);
    }
}

/* JsonViewer */
.ds-247420 .ds-ep-json {
    font-size: var(--fs-tiny, 12px);
    font-family: var(--ff-mono, ui-monospace, Consolas, monospace);
    white-space: pre-wrap; word-break: break-all;
    background: var(--panel-2, var(--bg-2));
    color: var(--panel-text, var(--fg));
    padding: var(--space-2, 8px);
    border-radius: var(--r-1, 8px);
    max-height: 300px;
    overflow-y: auto;
    margin: 0;
}
.ds-247420 .ds-ep-json-empty {
    color: var(--panel-text-3, var(--fg-3));
    white-space: normal;
    text-align: center;
}
/* JsonViewer syntax tokens (mode:'highlight'/'tree') — semantic theme tokens
   only (retuned per-theme in colors_and_type.css); fallbacks chain to
   coarser tokens, never a raw literal, so standalone kits stay themed. */
.ds-247420 .ds-ep-json-k { color: var(--sky, var(--accent)); }
.ds-247420 .ds-ep-json-s { color: var(--green-2, var(--green)); }
.ds-247420 .ds-ep-json-n { color: var(--flame, var(--warn)); }
.ds-247420 .ds-ep-json-b { color: var(--purple-2, var(--purple)); }
.ds-247420 .ds-ep-json-z { color: var(--panel-text-3, var(--fg-3)); font-style: italic; }
/* Collapsible tree mode — native details/summary, indent guides on children. */
.ds-247420 .ds-ep-json-tree { white-space: normal; }
.ds-247420 .ds-ep-json-kids {
    padding-left: var(--space-3-5, 20px);
    margin-left: var(--space-1, 4px);
    border-left: 1px solid var(--rule);
}
.ds-247420 .ds-ep-json-sum {
    cursor: pointer; user-select: none;
    list-style: none;
    border-radius: var(--r-1, 8px);
}
.ds-247420 .ds-ep-json-sum::-webkit-details-marker { display: none; }
.ds-247420 .ds-ep-json-sum::before {
    content: '+';
    display: inline-block; width: var(--space-3, 16px);
    color: var(--panel-text-3, var(--fg-3));
}
.ds-247420 .ds-ep-json-node[open] > .ds-ep-json-sum::before { content: '-'; }
.ds-247420 .ds-ep-json-sum:hover { background: color-mix(in oklab, var(--accent) 8%, transparent); }
.ds-247420 .ds-ep-json-sum:focus-visible {
    outline: var(--focus-w) solid var(--focus-color);
    outline-offset: calc(var(--focus-offset) * -1);
}
.ds-247420 .ds-ep-json-tag {
    margin-left: var(--space-1, 4px);
    font-size: var(--fs-micro, 11px);
    color: var(--panel-text-3, var(--fg-3));
}
.ds-247420 .ds-ep-json-leaf { padding-left: var(--space-3, 16px); word-break: break-all; }
/* Copy affordance (copyable:true) — floats over the viewer's top-right. */
.ds-247420 .ds-ep-json-wrap { position: relative; }
.ds-247420 .ds-ep-json-copy {
    position: absolute; top: var(--space-1-75, 6px); right: var(--space-1-75, 6px); z-index: var(--z-raised);
    appearance: none; cursor: pointer;
    background: var(--panel-1, var(--bg-1));
    border: 1px solid var(--rule);
    color: var(--panel-text-2, var(--fg-2));
    font: var(--fs-micro, 11px)/1 var(--ff-mono, monospace);
    padding: var(--space-half, 3px) var(--space-2, 8px);
    border-radius: var(--r-1, 8px);
    opacity: 0.75;
}
.ds-247420 .ds-ep-json-copy:hover { opacity: 1; border-color: var(--accent); }
.ds-247420 .ds-ep-json-copy.copied { color: var(--green-2, var(--green)); border-color: var(--green-2, var(--green)); opacity: 1; }
.ds-247420 .ds-ep-json-copy:focus-visible {
    outline: var(--focus-w) solid var(--focus-color);
    outline-offset: var(--focus-offset);
    opacity: 1;
}

/* Dock — 3x3 grid for in-engine overlay (canvas pass-through center) */
.ds-247420 .ds-ep-dock {
    display: grid;
    grid-template-areas:
        "top top top"
        "left center right"
        "bottom bottom bottom";
    grid-template-rows: auto 1fr auto;
    grid-template-columns: auto 1fr auto;
    height: 100%; width: 100%;
    color: var(--panel-text);
}
.ds-247420 .ds-ep-dock-top    { grid-area: top;    background: var(--panel-1); border-bottom: 1px solid var(--rule); }
.ds-247420 .ds-ep-dock-bottom { grid-area: bottom; background: var(--panel-1); border-top: 1px solid var(--rule); }
.ds-247420 .ds-ep-dock-left   { grid-area: left;   background: var(--panel-1); border-right: 1px solid var(--rule); overflow: auto; }
.ds-247420 .ds-ep-dock-right  { grid-area: right;  background: var(--panel-1); border-left: 1px solid var(--rule); overflow: auto; }
.ds-247420 .ds-ep-dock-center {
    grid-area: center;
    background: transparent;
    pointer-events: none;
    position: relative;
}
.ds-247420 .ds-ep-dock-center > * { pointer-events: auto; }

/* Mobile — stack dock regions vertically instead of the 3x3 grid */
@media (max-width: 600px) {
    .ds-247420 .ds-ep-dock {
        display: flex;
        flex-direction: column;
    }
}

/* IconButtonGroup */
.ds-247420 .ds-ep-btngrp {
    display: inline-flex;
    border: 1px solid var(--rule);
    border-radius: var(--r-1, 8px);
    overflow: hidden;
    background: var(--panel-2);
}
.ds-247420 .ds-ep-btngrp.dense .ds-ep-btngrp-btn { padding: var(--space-hair) var(--space-1-75); min-width: 22px; }
.ds-247420 .ds-ep-btngrp-btn {
    appearance: none; border: 0;
    background: transparent;
    color: var(--panel-text-2);
    padding: var(--space-1) var(--space-2-5);
    font: inherit; cursor: pointer;
    border-right: 1px solid var(--rule);
    transition: background var(--dur-snap, 80ms) var(--ease, ease), color var(--dur-snap, 80ms) var(--ease, ease);
}
.ds-247420 .ds-ep-btngrp-btn:last-child { border-right: 0; }
.ds-247420 .ds-ep-btngrp-btn:hover:not([disabled]) {
    background: color-mix(in oklab, var(--accent) 10%, transparent);
    color: var(--panel-text);
}
.ds-247420 .ds-ep-btngrp-btn.active {
    background: var(--accent);
    color: var(--accent-fg);
}
.ds-247420 .ds-ep-btngrp-btn[disabled] {
    opacity: 0.45;
    cursor: not-allowed;
}

/* Breakpoint custom property mirrors (consumers can use var()) */
.ds-247420 {
    --bp-sm: 480px;
    --bp-md: 768px;
    --bp-lg: 1024px;
    --bp-xl: 1440px;
}

/* ---------------------------------------------------------------
   ResizeHandle / SplitPanel
   --------------------------------------------------------------- */
.ds-247420 .ds-ep-resize {
    background: var(--rule);
    flex: 0 0 auto;
    touch-action: none;
    user-select: none;
    transition: background 120ms ease;
}
.ds-247420 .ds-ep-resize:hover, .ds-247420 .ds-ep-resize:focus-visible {
    background: var(--accent);
}
.ds-247420 .ds-ep-resize.axis-h { width: 6px; cursor: col-resize; align-self: stretch; }
.ds-247420 .ds-ep-resize.axis-v { height: 6px; cursor: row-resize; }
.ds-247420 .ds-ep-split { display: flex; width: 100%; height: 100%; }
.ds-247420 .ds-ep-split.horiz { flex-direction: row; }
.ds-247420 .ds-ep-split.vert  { flex-direction: column; }
.ds-247420 .ds-ep-split-pane { overflow: auto; }
.ds-247420 .ds-ep-split.horiz > .ds-ep-split-pane:not(.grow) { width: var(--split-size); }
.ds-247420 .ds-ep-split.vert  > .ds-ep-split-pane:not(.grow) { height: var(--split-size); }
.ds-247420 .ds-ep-split-pane.grow { flex: 1 1 0; }

/* ---------------------------------------------------------------
   ContextMenu
   --------------------------------------------------------------- */
.ds-247420 .ds-ep-ctxmenu-backdrop {
    position: fixed; inset: 0; z-index: var(--z-dropdown);
}
.ds-247420 .ds-ep-ctxmenu {
    position: absolute;
    min-width: 180px;
    max-width: calc(100vw - 8px);
    max-height: calc(100vh - 8px);
    max-height: calc(100dvh - 8px);
    overflow-y: auto;
    overscroll-behavior: contain;
    background: var(--panel-1, var(--panel-bg));
    color: var(--panel-text, var(--fg-text));
    border: 1px solid var(--rule);
    border-radius: var(--r-2, 8px);
    box-shadow: var(--shadow-2);
    padding: var(--space-1);
    display: flex; flex-direction: column;
    outline: none;
}
.ds-247420 .ds-ep-ctxmenu-item {
    display: flex; align-items: center; gap: var(--space-2);
    padding: var(--space-2) var(--space-2-5);
    background: transparent; border: 0;
    color: inherit; font: inherit; text-align: left;
    border-radius: var(--r-1, 8px);
    cursor: pointer;
}
.ds-247420 .ds-ep-ctxmenu-item:hover:not(.disabled),
.ds-247420 .ds-ep-ctxmenu-item:focus-visible {
    background: var(--panel-2, var(--rule));
}
.ds-247420 .ds-ep-ctxmenu-item.danger { color: var(--danger); }
.ds-247420 .ds-ep-ctxmenu-item.disabled { opacity: 0.45; cursor: not-allowed; }
.ds-247420 .ds-ep-ctxmenu-icon { width: 16px; display: inline-flex; justify-content: center; }
.ds-247420 .ds-ep-ctxmenu-sep { height: 1px; background: var(--rule); margin: var(--space-1) var(--space-1-75); }

/* ---------------------------------------------------------------
   Drawer
   --------------------------------------------------------------- */
.ds-247420 .ds-ep-drawer-backdrop {
    position: fixed; inset: 0; z-index: var(--z-drawer);
    background: var(--scrim);
}
.ds-247420 .ds-ep-drawer {
    position: absolute;
    background: var(--panel-1, var(--panel-bg));
    color: var(--panel-text, var(--fg-text));
    box-shadow: var(--shadow-overlay);
    outline: none;
    transition: transform 180ms ease;
    padding: var(--space-2-75);
    padding-top: max(var(--space-2-75), env(safe-area-inset-top));
    padding-bottom: max(var(--space-2-75), env(safe-area-inset-bottom));
    padding-left: max(var(--space-2-75), env(safe-area-inset-left));
    padding-right: max(var(--space-2-75), env(safe-area-inset-right));
}
.ds-247420 .ds-ep-drawer.side-left   { left: 0; top: 0; bottom: 0; width: 86vw; max-width: 320px; }
.ds-247420 .ds-ep-drawer.side-right  { right: 0; top: 0; bottom: 0; width: 86vw; max-width: 320px; }
.ds-247420 .ds-ep-drawer.side-bottom { left: 0; right: 0; bottom: 0; max-height: 80vh; max-height: 80dvh; border-top-left-radius: 12px; border-top-right-radius: 12px; }
@media (min-width: 768px) {
    .ds-247420 .ds-ep-drawer.side-left, .ds-247420 .ds-ep-drawer.side-right { width: 360px; max-width: 360px; }
}

/* ---------------------------------------------------------------
   Dialog
   --------------------------------------------------------------- */
.ds-247420 .ds-ep-dialog-backdrop {
    position: fixed; inset: 0; z-index: var(--z-modal);
    background: var(--scrim-strong);
    display: flex; align-items: center; justify-content: center;
    padding: max(var(--space-3), env(safe-area-inset-top)) max(var(--space-3), env(safe-area-inset-right)) max(var(--space-3), env(safe-area-inset-bottom)) max(var(--space-3), env(safe-area-inset-left));
}
.ds-247420 .ds-ep-dialog {
    background: var(--panel-1, var(--panel-bg));
    color: var(--panel-text, var(--fg-text));
    border: 1px solid var(--rule);
    border-radius: var(--r-2, 10px);
    box-shadow: var(--shadow-overlay);
    min-width: 280px; max-width: min(560px, 100%);
    max-height: 90vh;
    max-height: min(90dvh, calc(100dvh - 32px));
    overflow: auto;
    display: flex; flex-direction: column;
    outline: none;
}
/* Padding tokenized to the --space-* scale (was raw 14/16/10/6) and shaved to
   app density — a dialog is working chrome, not a marketing card. */
.ds-247420 .ds-ep-dialog-head { padding: var(--space-2-75) var(--space-3) var(--space-1-5); border-bottom: 1px solid var(--rule); }
.ds-247420 .ds-ep-dialog-title { margin: 0; font-size: var(--fs-body); font-weight: 600; }
.ds-247420 .ds-ep-dialog-body { padding: var(--space-2-75) var(--space-3); }
.ds-247420 .ds-ep-dialog-actions {
    display: flex; gap: var(--space-2); justify-content: flex-end;
    padding: var(--space-2) var(--space-2-75); border-top: 1px solid var(--rule);
}
.ds-247420 .ds-ep-dialog-btn {
    /* Snapped to the --ctl-sm rung + tokenized padding (was 8px 14px). */
    min-height: var(--ctl-sm); padding: 0 var(--space-3);
    display: inline-flex; align-items: center; justify-content: center;
    border: 1px solid var(--rule);
    border-radius: var(--r-1, 8px);
    background: var(--panel-2, transparent);
    color: inherit; font: inherit; cursor: pointer;
}
.ds-247420 .ds-ep-dialog-btn:hover { background: var(--panel-3, var(--bg-3)); }
.ds-247420 .ds-ep-dialog-btn.kind-primary {
    background: var(--accent); color: var(--accent-fg); border-color: var(--accent);
}
.ds-247420 .ds-ep-dialog-btn.kind-primary:hover { filter: brightness(0.92); }
.ds-247420 .ds-ep-dialog-btn.kind-danger {
    background: var(--danger); color: var(--on-accent); border-color: var(--danger);
}
.ds-247420 .ds-ep-dialog-btn.kind-danger:hover { filter: brightness(0.92); }

/* ---------------------------------------------------------------
   Toast
   --------------------------------------------------------------- */
.ds-247420 .ds-ep-toast-host {
    position: fixed;
    top: max(16px, env(safe-area-inset-top));
    right: max(16px, env(safe-area-inset-right));
    z-index: var(--z-toast);
    display: flex; flex-direction: column; gap: var(--space-2);
    pointer-events: none;
}
.ds-247420 .ds-ep-toast {
    pointer-events: auto;
    background: var(--panel-1, var(--panel-bg));
    color: var(--panel-text, var(--fg-text));
    border: 1px solid var(--rule);
    /* DELIBERATE accent spine (same intent as .panel-spine, expressed as a
       border because a toast has no free ::before — the host stacks them and
       .has-action reflows the box). Load-bearing, not decoration: this edge is
       the ONLY channel carrying toast severity — .kind-success/.kind-warn/
       .kind-error below recolour border-left-color and nothing else. Do not
       demote to a hairline; that would erase the status signal. */
    border-left: var(--bw-chunk) solid var(--accent);
    border-radius: var(--r-1, 8px);
    padding: var(--space-2) var(--space-2-75);
    box-shadow: var(--shadow-3);
    min-width: 220px; max-width: 360px;
    opacity: 1;
    transition: opacity 180ms ease, transform 180ms ease;
}
.ds-247420 .ds-ep-toast.kind-success { border-left-color: var(--success); }
.ds-247420 .ds-ep-toast.kind-warn    { border-left-color: var(--warn); }
.ds-247420 .ds-ep-toast.kind-error   { border-left-color: var(--danger); }
.ds-247420 .ds-ep-toast.leaving { opacity: 0; transform: translateY(-6px); }
.ds-247420 .ds-ep-toast.has-action {
    display: flex; align-items: center; gap: var(--space-2);
}
.ds-247420 .ds-ep-toast-msg { flex: 1; }
.ds-247420 .ds-ep-toast-action {
    flex-shrink: 0;
    background: transparent;
    color: var(--accent-ink, var(--accent));
    border: none;
    font: inherit;
    font-weight: 600;
    padding: var(--space-1) var(--space-1-75);
    border-radius: var(--r-1, 8px);
    cursor: pointer;
}
.ds-247420 .ds-ep-toast-action:hover { background: var(--panel-2, var(--panel-bg)); }
.ds-247420 .ds-ep-toast-action:focus-visible { outline: 2px solid var(--accent-ink, var(--accent)); outline-offset: 1px; }
@media (max-width: 480px) {
    .ds-247420 .ds-ep-toast-host { left: 16px; right: 16px; align-items: center; }
    .ds-247420 .ds-ep-toast { width: 100%; max-width: 100%; }
}

/* ---------------------------------------------------------------
   Focus-visible across interactive primitives
   --------------------------------------------------------------- */
.ds-247420 .ds-ep-tab:focus-visible,
.ds-247420 .ds-ep-tree-row:focus-visible,
.ds-247420 .ds-ep-btngrp-btn:focus-visible,
.ds-247420 .ds-ep-ctxmenu-item:focus-visible,
.ds-247420 .ds-ep-resize:focus-visible,
.ds-247420 .ds-ep-dialog-btn:focus-visible,
.ds-247420 .ds-ep-pager-btn:focus-visible {
    outline: var(--focus-w) solid var(--focus-color);
    outline-offset: var(--focus-offset);
}

/* ---------------------------------------------------------------
   Coarse pointer — bump tap targets to 44px
   --------------------------------------------------------------- */
@media (pointer: coarse) {
    .ds-247420 .ds-ep-tab,
    .ds-247420 .ds-ep-btngrp-btn,
    .ds-247420 .ds-ep-ctxmenu-item,
    .ds-247420 .ds-ep-dialog-btn,
    .ds-247420 .ds-ep-tree-row,
    .ds-247420 .ds-ep-pager-btn {
        min-height: 44px;
        padding-top: var(--space-2-5);
        padding-bottom: var(--space-2-5);
    }
    .ds-247420 .ds-ep-resize.axis-h { width: 14px; }
    .ds-247420 .ds-ep-resize.axis-v { height: 14px; }
}

/* ---------------------------------------------------------------
   Reduced motion — disable transitions on transient surfaces
   --------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
    .ds-247420 .ds-ep-drawer,
    .ds-247420 .ds-ep-dialog,
    .ds-247420 .ds-ep-toast,
    .ds-247420 .ds-ep-resize {
        transition: none !important;
    }
    /* Catch the transitions on tabs, tree rows, and primitive buttons that weren't
       previously wrapped — vestibular users get a static UI. */
    .ds-247420 .ds-ep-tab,
    .ds-247420 .ds-ep-tree-row,
    .ds-247420 .ds-ep-tree-twist,
    .ds-247420 .ds-ep-toolbar button,
    .ds-247420 .ds-ep-iconbtn,
    .ds-247420 .ds-ep-iconbtn-group button {
        transition: none !important;
        animation: none !important;
    }
}

/* ---------- focus-visible on editor-primitive interactive elements ---------- */
.ds-247420 .ds-ep-tab:focus-visible,
.ds-247420 .ds-ep-tree-row:focus-visible,
.ds-247420 .ds-ep-toolbar button:focus-visible,
.ds-247420 .ds-ep-iconbtn:focus-visible,
.ds-247420 .ds-ep-iconbtn-group button:focus-visible,
.ds-247420 .ds-ep-resize:focus-visible {
    outline: var(--focus-w) solid var(--focus-color);
    outline-offset: var(--focus-offset);
    border-radius: var(--r-1);
}

/* ---------------------------------------------------------------
   Form primitives — Checkbox, Radio, RadioGroup, Toggle, Field
   See also .ds-check-box in app-shell.css — a separate CSS-drawn
   multi-select checkbox family for FileRow/FileCell/SessionCard
   row selection; intentionally distinct rendering (CSS-drawn vs
   native), do not let one drift toward the other by accident.
   --------------------------------------------------------------- */
.ds-247420 .ds-check-row,
.ds-247420 .ds-radio-row,
.ds-247420 .ds-toggle-row {
    display: inline-flex; align-items: center; gap: var(--space-2, 8px);
    /* Desktop density: ~30px rows, not the 44px touch target (which stacked a
       lot of dead vertical space in inspector/settings forms). The full 44px
       target is restored under pointer:coarse below. */
    min-height: 30px;
    padding: var(--space-1, 4px) var(--space-2, 8px);
    cursor: pointer;
    color: var(--fg);
    font: inherit;
}
.ds-247420 .ds-check-row:has(input:disabled),
.ds-247420 .ds-radio-row:has(input:disabled),
.ds-247420 .ds-toggle-row:has(button:disabled) { opacity: 0.5; cursor: not-allowed; }
/* Touch devices: restore the 44px minimum tap target. */
@media (pointer: coarse) {
    .ds-247420 .ds-check-row, .ds-247420 .ds-radio-row, .ds-247420 .ds-toggle-row { min-height: 44px; }
}

.ds-247420 .ds-check, .ds-247420 .ds-radio {
    width: 18px; height: 18px;
    accent-color: var(--accent, var(--fg));
    flex-shrink: 0;
    cursor: inherit;
}
.ds-247420 .ds-check:focus-visible, .ds-247420 .ds-radio:focus-visible {
    outline: var(--focus-w) solid var(--focus-color);
    outline-offset: var(--focus-offset);
}

.ds-247420 .ds-check-label, .ds-247420 .ds-radio-label, .ds-247420 .ds-toggle-label {
    color: var(--fg);
    font-size: var(--fs-sm, 14px);
}

.ds-247420 .ds-radio-group {
    border: 0; padding: 0; margin: 0;
    display: flex; flex-direction: column; gap: var(--space-1, 4px);
}
.ds-247420 .ds-radio-group.horiz { flex-direction: row; flex-wrap: wrap; gap: var(--space-3, 16px); }
.ds-247420 .ds-radio-group > legend { padding: 0 0 var(--space-1, 4px); }

.ds-247420 .ds-toggle {
    appearance: none; border: 0;
    width: 44px; height: 24px;
    border-radius: var(--r-pill, 999px);
    background: var(--panel-2, var(--bg-2));
    border: 1px solid var(--rule);
    position: relative;
    cursor: pointer;
    padding: 0;
    flex-shrink: 0;
}
.ds-247420 .ds-toggle:hover { border-color: var(--rule-strong, var(--fg-3)); }
.ds-247420 .ds-toggle-knob {
    position: absolute;
    top: 1px; left: 1px;
    width: 20px; height: 20px;
    border-radius: 50%;
    background: var(--fg);
    transform: translateX(0);
}
.ds-247420 .ds-toggle[aria-checked="true"] {
    background: var(--accent);
    border-color: var(--accent);
}
.ds-247420 .ds-toggle[aria-checked="true"] .ds-toggle-knob {
    background: var(--accent-fg);
    transform: translateX(20px);
}
.ds-247420 .ds-toggle:focus-visible {
    outline: var(--focus-w) solid var(--focus-color);
    outline-offset: var(--focus-offset);
}
.ds-247420 .ds-toggle:disabled { cursor: not-allowed; opacity: 0.5; }

@media (prefers-reduced-motion: no-preference) {
    .ds-247420 .ds-toggle { transition: background var(--dur-base, 160ms) var(--ease), border-color var(--dur-base, 160ms) var(--ease); }
    .ds-247420 .ds-toggle-knob { transition: transform var(--dur-base, 160ms) var(--ease), background var(--dur-base, 160ms) var(--ease); }
}

.ds-247420 .ds-field-wrap {
    display: flex; flex-direction: column; gap: var(--space-1, 4px);
    margin-bottom: var(--space-2, 8px);
}
/* .ds-field-label / .ds-field-required / .ds-field-hint / .ds-field-error
   canonical definitions now live in app-shell.css alongside the rest of the
   .ds-field family (this file's .ds-field-wrap consumes them by class name).
   Removed here to stop the two bodies fighting on stylesheet load order. */
.ds-247420 .ds-field-wrap [aria-invalid="true"] {
    border-color: var(--danger) !important;
}

.ds-247420 .sr-only {
    position: absolute; width: 1px; height: 1px;
    /* margin:-1px is OFF-SCALE ON PURPOSE — it is not rhythm, it is the
       canonical visually-hidden idiom: the negative margin cancels the 1px
       box so the clipped element contributes zero layout. Snapping it to a
       --space-* rung would reintroduce a measurable gap. */
    padding: 0; margin: -1px; overflow: hidden;
    clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}

@media (prefers-reduced-motion: reduce) {
    .ds-247420 .ds-toggle, .ds-247420 .ds-toggle-knob { transition: none !important; }
}

/* ---------------------------------------------------------------
   Tooltip
   --------------------------------------------------------------- */
.ds-247420 .ds-tooltip {
    position: fixed;
    left: 0; top: 0;
    z-index: var(--z-tooltip);
    background: var(--panel-1, var(--panel-bg));
    color: var(--panel-text, var(--fg-text));
    border: 1px solid var(--rule);
    border-radius: var(--r-1, 8px);
    padding: var(--space-1-75) var(--space-2-5);
    font-size: var(--fs-micro);
    line-height: 1.4;
    max-width: 280px;
    box-shadow: var(--shadow-3);
    pointer-events: none;
}
.ds-247420 .ds-tooltip.kind-danger { border-color: var(--danger); color: var(--danger); }
@media (prefers-reduced-motion: no-preference) {
    .ds-247420 .ds-tooltip { transition: opacity 120ms ease; }
}

/* ---------------------------------------------------------------
   Popover (shared surface for Popover + Dropdown)
   --------------------------------------------------------------- */
.ds-247420 .ds-popover {
    position: fixed;
    left: 0; top: 0;
    z-index: var(--z-dropdown);
    background: var(--panel-1, var(--panel-bg));
    color: var(--panel-text, var(--fg-text));
    border: 1px solid var(--rule);
    border-radius: var(--r-2, 8px);
    box-shadow: var(--shadow-3);
    padding: 0;
    outline: none;
    min-width: 160px;
    max-width: calc(100vw - 8px);
    max-height: calc(100vh - 8px);
    max-height: calc(100dvh - 8px);
    overflow-y: auto;
    overscroll-behavior: contain;
}
@media (prefers-reduced-motion: no-preference) {
    .ds-247420 .ds-popover { transition: opacity 140ms ease, transform 140ms ease; }
}

/* ---------------------------------------------------------------
   Dropdown
   --------------------------------------------------------------- */
.ds-247420 .ds-dropdown-menu { padding: var(--space-1); }
.ds-247420 .ds-dropdown-list { display: flex; flex-direction: column; }
.ds-247420 .ds-dropdown-item {
    display: flex; align-items: center; gap: var(--space-2);
    /* Dense desktop row (was an unconditional 44px touch target — a menu of
       these was very tall on desktop). ~30px here; the 44px tap target is
       restored under @media(pointer:coarse) below, matching the .ds-check-row
       treatment. */
    min-height: 30px;
    padding: var(--space-1-5) var(--space-2-75);
    background: transparent; border: 0;
    color: inherit; font: inherit; text-align: left;
    border-radius: var(--r-1, 8px);
    cursor: pointer;
}
.ds-247420 .ds-dropdown-item:hover,
.ds-247420 .ds-dropdown-item:focus-visible {
    background: var(--panel-2, var(--rule));
    outline: none;
}
@media (pointer: coarse) {
    .ds-247420 .ds-dropdown-item { min-height: 44px; padding: var(--space-2) var(--space-3); }
}
.ds-247420 .ds-dropdown-item:focus-visible {
    outline: var(--focus-w) solid var(--focus-color);
    outline-offset: calc(var(--focus-offset) * -1);
}
.ds-247420 .ds-dropdown-item[aria-disabled="true"] {
    opacity: 0.5;
    pointer-events: none;
    cursor: not-allowed;
}
.ds-247420 .ds-dropdown-item.is-danger { color: var(--danger); }
.ds-247420 .ds-dropdown-glyph { width: 16px; display: inline-flex; justify-content: center; flex-shrink: 0; }
.ds-247420 .ds-dropdown-label { flex: 1; min-width: 0; }
.ds-247420 .ds-dropdown-separator { height: 1px; background: var(--rule); margin: var(--space-1) var(--space-1-75); }
.ds-247420 .ds-dropdown-trigger {
    appearance: none;
    background: var(--panel-2, transparent);
    color: inherit; font: inherit;
    border: 1px solid var(--rule);
    border-radius: var(--r-1, 8px);
    padding: 0 var(--space-2-75);
    cursor: pointer;
    /* Snapped to the --ctl-sm rung so this small control shares the height
       ladder with buttons/fields instead of a lone 32px literal. */
    min-height: var(--ctl-sm); height: var(--ctl-sm);
    display: inline-flex; align-items: center;
}
@media (pointer: coarse) {
    .ds-247420 .ds-dropdown-trigger { min-height: 44px; }
}

/* ---------------------------------------------------------------
   MenuButton — checkmarked select menu, mirrors Dropdown's chrome
   --------------------------------------------------------------- */
.ds-247420 .ov-menubutton-menu { padding: var(--space-1); }
.ds-247420 .ov-menubutton-list { display: flex; flex-direction: column; }
.ds-247420 .ov-menubutton-item {
    display: flex; align-items: center; gap: var(--space-2);
    min-height: 30px;
    padding: var(--space-1-5) var(--space-2-75);
    background: transparent; border: 0;
    color: inherit; font: inherit; text-align: left;
    border-radius: var(--r-1, 8px);
    cursor: pointer;
    width: 100%;
}
.ds-247420 .ov-menubutton-item:hover,
.ds-247420 .ov-menubutton-item:focus-visible {
    background: var(--panel-2, var(--rule));
    outline: none;
}
@media (pointer: coarse) {
    .ds-247420 .ov-menubutton-item { min-height: 44px; padding: var(--space-2) var(--space-3); }
}
.ds-247420 .ov-menubutton-item[aria-disabled="true"] {
    opacity: 0.5;
    pointer-events: none;
    cursor: not-allowed;
}
.ds-247420 .ov-menubutton-check { width: 16px; display: inline-flex; justify-content: center; flex-shrink: 0; color: var(--accent, currentColor); }
.ds-247420 .ov-menubutton-label { flex: 1; min-width: 0; }
.ds-247420 .ov-menubutton-empty { padding: var(--space-2) var(--space-2-75); color: var(--fg-muted); font-size: var(--fs-sm); }
.ds-247420 .ov-menubutton-item.is-unavailable {
    display: flex; align-items: center; justify-content: space-between; gap: var(--space-2);
    padding: var(--space-1-5) var(--space-2-75);
    color: var(--fg-muted);
}
.ds-247420 .ov-menubutton-retry {
    appearance: none; background: transparent; border: 1px solid var(--rule);
    border-radius: var(--r-1, 8px); color: inherit; font: inherit; font-size: var(--fs-sm);
    padding: var(--space-hair) var(--space-1-5); cursor: pointer;
}
.ds-247420 .ov-menubutton-retry:hover, .ds-247420 .ov-menubutton-retry:focus-visible { background: var(--panel-2, var(--rule)); outline: none; }
.ds-247420 .ov-menubutton-trigger {
    appearance: none;
    background: var(--panel-2, transparent);
    color: inherit; font: inherit;
    border: 1px solid var(--rule);
    border-radius: var(--r-1, 8px);
    padding: 0 var(--space-2-75);
    cursor: pointer;
    min-height: var(--ctl-sm); height: var(--ctl-sm);
    display: inline-flex; align-items: center;
}
@media (pointer: coarse) {
    .ds-247420 .ov-menubutton-trigger { min-height: 44px; }
}

/* ---------------------------------------------------------------
   InfoRow / InfoSection / DiagnosticsPanel — static debug readouts
   --------------------------------------------------------------- */
.ds-247420 .ds-ep-diagnostics { display: flex; flex-direction: column; gap: var(--space-3); }
.ds-247420 .ds-ep-diagnostics-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); }
.ds-247420 .ds-ep-diagnostics-title { margin: 0; font-size: var(--fs-lg); }
.ds-247420 .ds-ep-diagnostics-refresh {
    appearance: none; background: var(--panel-2, transparent); color: inherit; font: inherit;
    border: 1px solid var(--rule); border-radius: var(--r-1, 8px);
    padding: var(--space-1) var(--space-2-75); cursor: pointer;
}
.ds-247420 .ds-ep-diagnostics-refresh:disabled { opacity: 0.6; cursor: not-allowed; }
.ds-247420 .ds-ep-diagnostics-refresh:hover:not(:disabled), .ds-247420 .ds-ep-diagnostics-refresh:focus-visible { background: var(--panel-3, var(--rule)); outline: none; }
.ds-247420 .ds-ep-infosection { border: 1px solid var(--rule); border-radius: var(--r-2, 8px); padding: var(--space-2-75); min-height: 4.5rem; }
.ds-247420 .ds-ep-infosection-title { margin: 0 0 var(--space-1-5); font-size: var(--fs-sm); text-transform: uppercase; letter-spacing: 0.04em; color: var(--fg-muted); }
.ds-247420 .ds-ep-infosection-rows { display: flex; flex-direction: column; gap: var(--space-1); }
.ds-247420 .ds-ep-infosection-loading { padding: var(--space-2) 0; color: var(--fg-muted); font-size: var(--fs-sm); }
.ds-247420 .ds-ep-inforow { display: flex; align-items: baseline; gap: var(--space-2); justify-content: space-between; }
.ds-247420 .ds-ep-inforow-label { color: var(--fg-muted); font-size: var(--fs-sm); flex-shrink: 0; }
.ds-247420 .ds-ep-inforow-value { font-family: var(--font-mono, monospace); font-size: var(--fs-sm); text-align: right; word-break: break-word; }

/* BatchProgressLabel */
.ds-247420 .ds-ep-batchprogress { font-variant-numeric: tabular-nums; }

/* Interaction primitives — drag/drop + keyboard shortcuts */
.ds-247420 .ds-reorderable { display: flex; gap: var(--space-1, 4px); }
.ds-247420 .ds-reorderable-vertical { flex-direction: column; }
.ds-247420 .ds-reorderable-horizontal { flex-direction: row; }
.ds-247420 .ds-reorder-item {
    display: flex; align-items: center; gap: var(--space-2, 8px);
    padding: var(--space-1, 4px); border-radius: var(--r-1, 8px);
    background: var(--panel-1); color: var(--panel-text);
    transition: transform var(--dur-snap, 120ms) var(--ease, ease), opacity var(--dur-snap, 120ms) var(--ease, ease);
}
.ds-247420 .ds-reorder-handle {
    min-width: 44px; min-height: 44px;
    display: inline-flex; align-items: center; justify-content: center;
    appearance: none; background: transparent; color: var(--panel-text-2, var(--panel-text));
    border: 0; border-radius: var(--r-1, 8px);
    cursor: grab; font: 14px/1 var(--ff-mono, monospace); letter-spacing: -2px;
    touch-action: none; user-select: none;
}
.ds-247420 .ds-reorder-handle:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
.ds-247420 .ds-reorder-handle:active { cursor: grabbing; }
.ds-247420[data-dragging="true"] { opacity: 0.6; cursor: grabbing; transform: scale(0.98); }
.ds-247420[data-drop-target-active="true"] {
    outline: 2px dashed var(--focus-ring, var(--accent)); outline-offset: var(--focus-offset);
    background: color-mix(in srgb, var(--accent) 8%, var(--panel-1));
}
.ds-247420 .ds-reorder-insertion-line {
    height: 2px; background: var(--focus-ring, var(--accent));
    border-radius: var(--r-hair); animation: ds-insertion-in var(--dur-snap, 120ms) var(--ease, ease);
}
@keyframes ds-insertion-in { from { transform: scaleX(0); opacity: 0; } to { transform: scaleX(1); opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
    .ds-247420 .ds-reorder-item, .ds-247420[data-dragging="true"] { transition: none; transform: none; }
    .ds-247420 .ds-reorder-insertion-line { animation: none; }
}
.ds-247420 .ds-kbd {
    display: inline-block; font: 0.85em/1 var(--ff-mono, monospace);
    padding: var(--space-1, 4px) calc(var(--space-1, 4px) * 1.5);
    border: 1px solid var(--rule, var(--panel-text-3));
    border-radius: var(--r-1, 8px);
    background: var(--panel-2, var(--panel-1)); color: var(--panel-text);
}
.ds-247420 .ds-kbd-group { margin: var(--space-3, 16px) 0; }
.ds-247420 .ds-kbd-group h3 { font-size: 0.9em; opacity: 0.7; margin: 0 0 var(--space-1, 4px); text-transform: capitalize; }
/* One shortcut per row: the key chip on the left, its description on the right,
   so the dialog reads as "G  Translate gizmo" rather than a wall of bare keys. */
.ds-247420 .ds-kbd-group ul { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: var(--space-1, 4px); }
.ds-247420 .ds-kbd-row { display: flex; align-items: baseline; gap: var(--space-2, 8px); }
.ds-247420 .ds-kbd-row .ds-kbd { flex: 0 0 auto; min-width: 3.5em; text-align: center; }
.ds-247420 .ds-kbd-label { flex: 1 1 auto; opacity: 0.85; }

/* ---------------------------------------------------------------
   CommandPalette (ov-cmd)
   --------------------------------------------------------------- */
.ds-247420 .ov-cmd-backdrop {
    position: fixed; inset: 0;
    z-index: var(--z-modal);
    background: color-mix(in oklab, var(--fg) 32%, transparent);
    display: flex; align-items: flex-start; justify-content: center;
    padding: 12vh var(--space-3, 16px) var(--space-3, 16px);
}
.ds-247420 .ov-cmd-panel {
    width: min(560px, 100%);
    max-height: 64vh;
    display: flex; flex-direction: column;
    background: var(--bg-2); color: var(--fg);
    border: 1px solid var(--rule);
    border-radius: var(--r-2, 14px);
    box-shadow: var(--shadow-3);
    overflow: hidden;
}
.ds-247420 .ov-cmd-input {
    appearance: none; border: 0;
    border-bottom: 1px solid var(--rule);
    background: transparent; color: var(--fg);
    padding: var(--space-3, 16px);
    font: var(--fs-body, 16px)/1.4 var(--ff-body, sans-serif);
    outline: none;
}
.ds-247420 .ov-cmd-input::placeholder { color: var(--fg-3); }
.ds-247420 .ov-cmd-list { overflow: auto; padding: var(--space-1, 4px); min-height: 0; }
.ds-247420 .ov-cmd-group {
    padding: var(--space-2, 8px) var(--space-2, 8px) var(--space-1, 4px);
    font: var(--fs-micro, 11px)/1 var(--ff-mono, monospace);
    text-transform: uppercase; letter-spacing: var(--tr-label, 0.1em);
    color: var(--fg-3);
}
.ds-247420 .ov-cmd-item {
    display: flex; align-items: center; gap: var(--space-2, 8px);
    width: 100%; min-height: 40px;
    padding: var(--space-2, 8px) var(--space-3, 16px);
    background: transparent; border: 0;
    color: var(--fg); font: inherit; text-align: left;
    border-radius: var(--r-1, 8px); cursor: pointer;
}
.ds-247420 .ov-cmd-item:hover { background: var(--panel-2, var(--rule)); }
.ds-247420 .ov-cmd-item.is-active { background: var(--accent); color: var(--accent-fg); }
.ds-247420 .ov-cmd-glyph { width: 18px; display: inline-flex; justify-content: center; flex-shrink: 0; }
.ds-247420 .ov-cmd-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ds-247420 .ov-cmd-hint {
    flex-shrink: 0; color: var(--fg-3);
    font: var(--fs-tiny, 12px)/1 var(--ff-mono, monospace);
}
.ds-247420 .ov-cmd-item.is-active .ov-cmd-hint { color: var(--accent-fg); }
.ds-247420 .ov-cmd-empty { padding: var(--space-4, 24px); text-align: center; color: var(--fg-3); }

/* ---------------------------------------------------------------
   EmojiPicker (ov-emoji)
   --------------------------------------------------------------- */
.ds-247420 .ov-emoji-root {
    position: fixed; left: 0; top: 0;
    /* Anchored to a trigger, not a blocking surface — dropdown rung. */
    z-index: var(--z-dropdown);
    width: 280px;
    display: flex; flex-direction: column;
    background: var(--bg-2); color: var(--fg);
    border: 1px solid var(--rule);
    border-radius: var(--r-2, 14px);
    box-shadow: var(--shadow-3);
    outline: none; overflow: hidden;
}
.ds-247420 .ov-emoji-search {
    flex-shrink: 0; margin: var(--space-2, 8px); margin-bottom: 0;
    padding: var(--space-1-5, 6px) var(--space-2, 8px);
    border: 1px solid var(--rule); border-radius: var(--r-1, 8px);
    background: var(--bg-1, var(--bg-3)); color: var(--fg);
    font: inherit; font-size: var(--fs-sm, 13px);
}
.ds-247420 .ov-emoji-search:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: 1px; }
.ds-247420 .ov-emoji-tabs {
    display: flex; flex-shrink: 0;
    border-bottom: 1px solid var(--rule);
    background: var(--bg-3);
}
.ds-247420 .ov-emoji-tab {
    flex: 1; appearance: none; border: 0; background: transparent;
    padding: var(--space-2, 8px); cursor: pointer; font-size: var(--fs-lg, 18px);
    border-bottom: 2px solid transparent;
}
.ds-247420 .ov-emoji-tab:hover { background: var(--bg-2); }
.ds-247420 .ov-emoji-tab[aria-selected="true"] { border-bottom-color: var(--accent); background: var(--bg-2); }
.ds-247420 .ov-emoji-tab:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: -2px; }
.ds-247420 .ov-emoji-grid { overflow: auto; max-height: 220px; padding: var(--space-2, 8px); }
.ds-247420 .ov-emoji-grid-inner { display: grid; grid-template-columns: repeat(8, 1fr); gap: var(--space-1, 4px); }
.ds-247420 .ov-emoji-cell {
    appearance: none; border: 0; background: transparent;
    aspect-ratio: 1; font-size: var(--fs-lg, 18px);
    border-radius: var(--r-1, 8px); cursor: pointer;
    display: inline-flex; align-items: center; justify-content: center;
}
.ds-247420 .ov-emoji-cell:hover {
    background: color-mix(in oklab, var(--accent) 18%, transparent);
}
.ds-247420 .ov-emoji-cell:focus-visible {
    background: color-mix(in oklab, var(--accent) 18%, transparent);
    outline: var(--focus-w) solid var(--focus-color); outline-offset: 1px;
}

/* ---------------------------------------------------------------
   BootOverlay (ov-boot)
   --------------------------------------------------------------- */
.ds-247420 .ov-boot {
    position: fixed; inset: 0;
    z-index: var(--z-top);
    background: var(--bg);
    display: flex; align-items: center; justify-content: center;
    color: var(--fg);
}
.ds-247420 .ov-boot-inner {
    display: flex; flex-direction: column; align-items: center;
    gap: var(--space-3, 16px); width: min(320px, 80vw);
}
.ds-247420 .ov-boot-spinner {
    width: 40px; height: 40px; border-radius: 50%;
    border: 3px solid var(--rule);
    border-top-color: var(--accent);
    animation: ov-boot-spin 0.8s linear infinite;
}
@keyframes ov-boot-spin { to { transform: rotate(360deg); } }
.ds-247420 .ov-boot-mark { font-size: 40px; line-height: 1; }
.ds-247420 .ov-boot-mark-error { color: var(--danger); }
.ds-247420 .ov-boot-bar {
    width: 100%; height: 6px;
    background: var(--bg-3); border-radius: var(--r-pill, 999px);
    overflow: hidden;
}
.ds-247420 .ov-boot-bar-fill {
    height: 100%; background: var(--accent);
    border-radius: var(--r-pill, 999px);
    transition: width var(--dur-base, 160ms) var(--ease, ease);
}
.ds-247420 .ov-boot-phase {
    font: var(--fs-sm, 14px)/1.4 var(--ff-mono, monospace);
    color: var(--fg-2); text-align: center;
}
.ds-247420 .ov-boot.is-error .ov-boot-phase { color: var(--danger); }
@media (prefers-reduced-motion: reduce) {
    .ds-247420 .ov-boot-spinner { animation: none; }
    .ds-247420 .ov-boot-bar-fill { transition: none; }
}

/* ---------------------------------------------------------------
   SettingsPopover (ov-set)
   --------------------------------------------------------------- */
.ds-247420 .ov-set-root {
    position: fixed; left: 0; top: 0;
    /* Anchored to a trigger, not a blocking surface — dropdown rung. */
    z-index: var(--z-dropdown);
    width: 300px; max-height: 70vh;
    display: flex; flex-direction: column;
    background: var(--bg-2); color: var(--fg);
    border: 1px solid var(--rule);
    border-radius: var(--r-2, 14px);
    box-shadow: var(--shadow-3);
    outline: none; overflow: hidden;
}
.ds-247420 .ov-set-head {
    flex-shrink: 0;
    padding: var(--space-3, 16px);
    border-bottom: 1px solid var(--rule);
    font: var(--fs-sm, 14px)/1 var(--ff-mono, monospace);
    text-transform: uppercase; letter-spacing: var(--tr-label, 0.1em);
    color: var(--fg);
}
.ds-247420 .ov-set-body { overflow: auto; padding: var(--space-2, 8px); }
.ds-247420 .ov-set-section + .ov-set-section { margin-top: var(--space-2, 8px); }
.ds-247420 .ov-set-section-head {
    padding: var(--space-2, 8px) var(--space-2, 8px) var(--space-1, 4px);
    font: var(--fs-micro, 11px)/1 var(--ff-mono, monospace);
    text-transform: uppercase; letter-spacing: var(--tr-label, 0.1em);
    color: var(--fg-3);
}
.ds-247420 .ov-set-row {
    display: flex; align-items: center; gap: var(--space-2, 8px);
    min-height: 40px; padding: var(--space-1, 4px) var(--space-2, 8px);
}
.ds-247420 .ov-set-row-label { flex: 1; min-width: 0; font-size: var(--fs-sm, 14px); color: var(--fg-2); }
.ds-247420 .ov-set-row-value { color: var(--fg); font: var(--fs-sm, 14px)/1 var(--ff-mono, monospace); }
.ds-247420 .ov-set-control {
    appearance: none; flex-shrink: 0; max-width: 55%;
    background: var(--bg-3); color: var(--fg);
    border: 1px solid var(--rule); border-radius: var(--r-1, 8px);
    padding: var(--space-1, 4px) var(--space-2, 8px); font: inherit;
}
.ds-247420 .ov-set-control:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: 1px; border-color: var(--accent-ink); }
.ds-247420 input.ov-set-control[type="range"] { padding: 0; accent-color: var(--accent); }
.ds-247420 .ov-set-toggle { width: 18px; height: 18px; accent-color: var(--accent); flex-shrink: 0; cursor: pointer; }
@media (pointer: coarse) {
    .ds-247420 .ov-set-toggle { width: 24px; height: 24px; }
}
.ds-247420 .ov-set-btn {
    width: 100%; appearance: none; cursor: pointer;
    background: var(--accent); color: var(--accent-fg);
    border: 0; border-radius: var(--r-1, 8px);
    padding: var(--space-2, 8px); font: inherit;
}

/* AuthModal — ov-auth-* */
.ds-247420 .ov-auth-backdrop {
    position: fixed; inset: 0; z-index: var(--z-modal);
    display: flex; align-items: center; justify-content: center;
    background: var(--scrim-strong);
}
.ds-247420 .ov-auth-panel {
    width: min(420px, 92vw);
    display: flex; flex-direction: column;
    background: var(--bg-2); color: var(--fg);
    border: 1px solid var(--rule); border-radius: var(--r-2, 8px);
    box-shadow: var(--shadow-overlay);
    overflow: hidden;
}
.ds-247420 .ov-auth-head { display: flex; align-items: center; justify-content: space-between; padding: var(--space-3); border-bottom: 1px solid var(--rule); }
.ds-247420 .ov-auth-title { margin: 0; font-size: var(--fs-lg); font-weight: 700; }
.ds-247420 .ov-auth-x { border: none; background: transparent; color: var(--fg-2, var(--fg)); font-size: 22px; line-height: 1; cursor: pointer; }
.ds-247420 .ov-auth-x:hover { color: var(--fg); }
.ds-247420 .ov-auth-tabs { display: flex; gap: var(--space-1); padding: var(--space-2) var(--space-3) 0; }
.ds-247420 .ov-auth-tab { flex: 1; padding: var(--space-1) var(--space-2); border: 1px solid var(--rule); border-bottom: none; border-radius: var(--r-1) var(--r-1) 0 0; background: var(--bg-3); color: var(--fg-2, var(--fg)); cursor: pointer; }
.ds-247420 .ov-auth-tab:hover { color: var(--fg); }
.ds-247420 .ov-auth-tab.is-active { background: var(--bg-2); color: var(--fg); font-weight: 700; }
.ds-247420 .ov-auth-body { display: flex; flex-direction: column; gap: var(--space-2); padding: var(--space-3); }
.ds-247420 .ov-auth-hint { margin: 0; font-size: var(--fs-tiny); color: var(--fg-2, var(--fg)); }
.ds-247420 .ov-auth-input { padding: var(--space-2); border: 1px solid var(--rule-strong); border-radius: var(--r-1); background: var(--bg-3); color: var(--fg); }
.ds-247420 .ov-auth-primary { padding: var(--space-2); border: none; border-radius: var(--r-1); background: var(--accent); color: var(--on-accent); font-weight: 700; cursor: pointer; }
.ds-247420 .ov-auth-primary:disabled { opacity: .6; cursor: progress; }
.ds-247420 .ov-auth-error { margin: 0 var(--space-3) var(--space-3); padding: var(--space-2); border-radius: var(--r-1); background: color-mix(in oklab, var(--danger) 15%, transparent); color: var(--danger); font-size: var(--fs-tiny); }

/* VideoLightbox — ov-lightbox-* */
.ds-247420 .ov-lightbox-backdrop {
    position: fixed; inset: 0; z-index: var(--z-modal);
    display: flex; align-items: center; justify-content: center;
    background: var(--scrim-media);
}
.ds-247420 .ov-lightbox-x { position: absolute; top: 16px; right: 16px; width: 40px; height: 40px; border: none; border-radius: 50%; background: color-mix(in oklab, var(--on-color) 12%, transparent); color: var(--on-color); font-size: 24px; line-height: 1; cursor: pointer; }
.ds-247420 .ov-lightbox-x:hover { background: color-mix(in oklab, var(--on-color) 24%, transparent); }
.ds-247420 .ov-lightbox-stage { display: flex; flex-direction: column; align-items: center; gap: var(--space-2); max-width: 92vw; max-height: 90vh; }
.ds-247420 .ov-lightbox-video { max-width: 92vw; max-height: 82vh; border-radius: var(--r-1); background: var(--media-letterbox); /* true black media letterbox, intentional */ }
.ds-247420 .ov-lightbox-label { color: var(--on-color); font-size: var(--fs-tiny); opacity: .85; }

/* ---------------------------------------------------------------
   Editor-primitive scrollbars — thin, dark, themed so docked panels
   don't show a fat light OS scrollbar over the dark engine chrome.
   --------------------------------------------------------------- */
.ds-247420 .ds-ep-panel,
.ds-247420 .ds-ep-panel-body,
.ds-247420 .ds-ep-tree,
.ds-247420 .ds-ep-eventlog,
.ds-247420 .ds-ep-drawer { scrollbar-width: thin; scrollbar-color: var(--rule) transparent; }
.ds-247420 .ds-ep-panel ::-webkit-scrollbar,
.ds-247420 .ds-ep-panel-body::-webkit-scrollbar,
.ds-247420 .ds-ep-tree::-webkit-scrollbar,
.ds-247420 .ds-ep-eventlog::-webkit-scrollbar { width: 8px; height: 8px; }
.ds-247420 .ds-ep-panel ::-webkit-scrollbar-track,
.ds-247420 .ds-ep-panel-body::-webkit-scrollbar-track,
.ds-247420 .ds-ep-tree::-webkit-scrollbar-track,
.ds-247420 .ds-ep-eventlog::-webkit-scrollbar-track { background: transparent; }
.ds-247420 .ds-ep-panel ::-webkit-scrollbar-thumb,
.ds-247420 .ds-ep-panel-body::-webkit-scrollbar-thumb,
.ds-247420 .ds-ep-tree::-webkit-scrollbar-thumb,
.ds-247420 .ds-ep-eventlog::-webkit-scrollbar-thumb {
    background: var(--rule); border-radius: var(--r-pill, 999px);
    border: 2px solid transparent; background-clip: padding-box;
}
.ds-247420 .ds-ep-panel ::-webkit-scrollbar-thumb:hover,
.ds-247420 .ds-ep-panel-body::-webkit-scrollbar-thumb:hover {
    background: var(--panel-text-3, var(--rule)); background-clip: padding-box;
}

/* ---------------------------------------------------------------
   Dense app-prop inputs — color swatch + checkbox sized to match the
   engine-editor density (raw browser controls otherwise read as jank).
   --------------------------------------------------------------- */
/* Specificity bump (.ds-ep-propfield-value prefix) so the sized swatch/check win
   over the generic `.ds-ep-propfield-value input { width:100% }` rule — otherwise
   a color/checkbox app-prop stretches to the full field width and reads as jank. */
.ds-247420 .ds-ep-propfield-value input.ds-input-color { width: 28px; flex: 0 0 auto; }
.ds-247420 .ds-ep-propfield-value input.ds-input-check { width: 16px; flex: 0 0 auto; }
.ds-247420 .ds-input-color {
    appearance: none; -webkit-appearance: none;
    width: 28px; height: 22px; padding: 0;
    border: 1px solid var(--rule); border-radius: var(--r-1, 8px);
    background: var(--panel-2); cursor: pointer; flex-shrink: 0;
}
.ds-247420 .ds-input-color::-webkit-color-swatch-wrapper { padding: var(--space-hair); }
.ds-247420 .ds-input-color::-webkit-color-swatch { border: none; border-radius: var(--r-hair); }
.ds-247420 .ds-input-color:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
.ds-247420 .ds-input-check {
    appearance: none; -webkit-appearance: none;
    width: 16px; height: 16px; flex-shrink: 0;
    border: 1px solid var(--rule); border-radius: var(--r-1, 8px);
    background: var(--panel-2); cursor: pointer; position: relative;
}
.ds-247420 .ds-input-check:checked { background: var(--accent); border-color: var(--accent); }
.ds-247420 .ds-input-check:checked::after {
    content: ''; position: absolute; left: 4px; top: 1px;
    width: 4px; height: 8px; border: solid var(--accent-fg);
    border-width: 0 2px 2px 0; transform: rotate(45deg);
}
.ds-247420 .ds-input-check:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }

/* ---------------------------------------------------------------
   Tree drag-reparent affordance — clear drop target + dragging row.
   useDropTarget marks the active row with [data-drop-target-active]; the
   dragged source carries [data-dragging]. Both surfaced as visible state so a
   reparent drag shows exactly where it will land.
   --------------------------------------------------------------- */
.ds-247420 .ds-ep-tree-item[data-drop-target-active] > .ds-ep-tree-row,
.ds-247420 .ds-ep-tree-row[data-drop-target-active],
.ds-247420 .ds-ep-tree-row.drag-over {
    background: color-mix(in oklab, var(--accent) 22%, transparent);
    box-shadow: inset 0 0 0 1px var(--accent);
}
.ds-247420 .ds-ep-panel-body[data-drop-target-active] {
    box-shadow: inset 0 0 0 2px color-mix(in oklab, var(--accent) 50%, transparent);
}
.ds-247420 .ds-ep-tree-item[data-dragging] > .ds-ep-tree-row,
.ds-247420 .ds-ep-tree-item.dragging > .ds-ep-tree-row { opacity: 0.5; }

/* ---------------------------------------------------------------
   Floating editor docks — the non-occluding idiom. The game/3D
   viewport fills the whole editor stage; the hierarchy + inspector
   ride over it as translucent, backdrop-blurred, collapsible cards
   so the play area stays visible (and clickable) behind them. The
   dock layer is pointer-events:none; only the dock cards capture
   input, so the exposed viewport gutter clicks through to the canvas.
   --------------------------------------------------------------- */
.ds-247420 .ds-ep-stage { position: relative; flex: 1; min-height: 0; overflow: hidden; }
.ds-247420 .ds-ep-dock-layer { display: block; }
.ds-247420 .ds-ep-dock {
    position: absolute; top: 8px; bottom: 8px;
    display: flex; flex-direction: column;
    width: 280px; max-width: calc(50% - 12px);
    pointer-events: all;
    background: color-mix(in oklab, var(--panel-1) 82%, transparent);
    -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
    border: 1px solid var(--rule);
    border-radius: var(--r-2, 8px);
    box-shadow: var(--shadow-3);
    color: var(--panel-text);
    overflow: hidden;
}
.ds-247420 .ds-ep-dock-left { left: 8px; }
.ds-247420 .ds-ep-dock-right { right: 8px; }
/* Collapsed: shrink to just the header strip, freeing the viewport. */
.ds-247420 .ds-ep-dock.collapsed { bottom: auto; height: auto; }
.ds-247420 .ds-ep-dock-head {
    display: flex; align-items: center; gap: var(--space-1-75);
    flex: 0 0 auto; padding: var(--space-1) var(--space-1-75) var(--space-1) var(--space-2-5);
    border-bottom: 1px solid var(--rule);
    background: color-mix(in oklab, var(--panel-1) 60%, transparent);
}
.ds-247420 .ds-ep-dock.collapsed .ds-ep-dock-head { border-bottom: 0; }
.ds-247420 .ds-ep-dock-title {
    flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    font: 8px/1 var(--ff-mono, monospace); text-transform: uppercase; letter-spacing: 0.12em;
    color: var(--panel-text-3);
}
.ds-247420 .ds-ep-dock-body { flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden; background: transparent; }
.ds-247420 .ds-ep-dock-body.hidden { display: none; }
/* The right dock's body reuses .ds-ep-panel but must stay transparent so the
   dock's own translucency/blur shows the game view through. */
.ds-247420 .ds-ep-dock-body.ds-ep-panel { background: transparent; }

/* Mobile: docks become full-width bottom-anchored sheets so they don't crowd
   the narrow viewport side-by-side; the viewport still fills the stage behind. */
@media (max-width: 760px) {
    .ds-247420 .ds-ep-dock { width: auto; max-width: none; left: 6px; right: 6px; top: auto; bottom: 6px; max-height: 46vh; }
    .ds-247420 .ds-ep-dock-left { bottom: calc(46vh + 12px); max-height: 40vh; }
    .ds-247420 .ds-ep-dock.collapsed { bottom: auto; }
    .ds-247420 .ds-ep-dock-left.collapsed { top: 6px; bottom: auto; }
}

/* Collapse / CollapseGroup — progressive-disclosure toggle panel. */
.ds-247420 .ds-ep-collapse {
    border: 1px solid var(--rule);
    border-radius: var(--r-1, 8px);
    background: var(--panel-2, var(--bg-2));
}
.ds-247420 .ds-ep-collapse + .ds-ep-collapse { margin-top: var(--space-1, 4px); }
.ds-247420 .ds-ep-collapse-head {
    appearance: none; cursor: pointer; width: 100%;
    display: flex; align-items: center; gap: var(--space-1-5, 5px);
    background: transparent; border: none;
    padding: var(--space-1-5, 5px) var(--space-2, 8px);
    font: inherit; color: var(--panel-text, var(--fg));
    text-align: left;
}
.ds-247420 .ds-ep-collapse-head:hover { background: color-mix(in oklab, var(--accent) 6%, transparent); }
.ds-247420 .ds-ep-collapse-chevron {
    font-family: var(--ff-mono, monospace);
    width: 1em; flex-shrink: 0;
    color: var(--panel-text-3, var(--fg-3));
}
.ds-247420 .ds-ep-collapse-title { flex: 1; font-weight: 600; }
.ds-247420 .ds-ep-collapse-body {
    padding: var(--space-2, 8px);
    border-top: 1px solid var(--rule);
}
.ds-247420 .ds-ep-collapse-group { display: flex; flex-direction: column; }

/* Divider — plain rule / labeled rule / vertical separator. */
.ds-247420 .ds-ep-divider {
    border: none; border-top: 1px solid var(--rule);
    margin: var(--space-2, 8px) 0;
}
.ds-247420 .ds-ep-divider-labeled {
    display: flex; align-items: center; gap: var(--space-1-5, 5px);
    margin: var(--space-2, 8px) 0;
}
.ds-247420 .ds-ep-divider-labeled::before,
.ds-247420 .ds-ep-divider-labeled::after {
    content: '';
    flex: 1;
    border-top: 1px solid var(--rule);
}
.ds-247420 .ds-ep-divider-label {
    font: var(--fs-tiny, 12px)/1 var(--ff-mono, monospace);
    color: var(--panel-text-3, var(--fg-3));
    white-space: nowrap;
    /* Sits on the ::before/::after rule line, which otherwise draws straight
       through the label text. Background must match the label's containing
       card (.panel uses --panel-1, falling back to --bg-2) so the line reads
       as visually broken around the label rather than crossing it. */
    background: var(--panel-1, var(--bg-2));
    padding: 0 var(--space-1-5, 5px);
    position: relative;
    z-index: var(--z-raised);
}
/* PermissionMenu (ov-perm) — roving-tabindex menuitemcheckbox rows +
   approve-all/revoke-all actions, styled to match the sibling Dropdown menu. */
.ds-247420 .ov-perm-menu { min-width: 220px; }
.ds-247420 .ov-perm-list { display: flex; flex-direction: column; padding: var(--space-1, 4px); }
.ds-247420 .ov-perm-item {
    display: flex; align-items: center; gap: var(--space-2, 8px);
    width: 100%; min-height: 36px;
    padding: var(--space-2, 8px) var(--space-3, 16px);
    background: transparent; border: 0;
    color: var(--fg); font: inherit; text-align: left;
    border-radius: var(--r-1, 8px); cursor: pointer;
}
.ds-247420 .ov-perm-item:hover { background: var(--panel-2, var(--rule)); }
.ds-247420 .ov-perm-item[aria-checked="true"] { color: var(--accent-ink); }
.ds-247420 .ov-perm-item[aria-checked="true"]::before { content: '[x] '; font-family: var(--ff-mono, monospace); }
.ds-247420 .ov-perm-item[aria-checked="false"]::before { content: '[ ] '; font-family: var(--ff-mono, monospace); color: var(--fg-3); }
.ds-247420 .ov-perm-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ds-247420 .ov-perm-actions {
    display: flex; gap: var(--space-2, 8px);
    padding: var(--space-2, 8px) var(--space-3, 16px);
    border-top: 1px solid var(--rule); margin-top: var(--space-1, 4px);
}
.ds-247420 .ov-perm-action {
    flex: 1; appearance: none; border: 1px solid var(--rule); background: transparent;
    color: var(--fg-2); font: inherit; font-size: var(--fs-tiny, 12px);
    padding: var(--space-1) var(--space-2); border-radius: var(--r-1, 8px); cursor: pointer;
}
.ds-247420 .ov-perm-action:hover { background: var(--panel-2, var(--rule)); }

/* ApprovalPrompt — inline in-thread tool-permission card: name/category
   head, optional args preview, an optional free-text note, four resolution
   actions. Distinct from PermissionMenu (a settings dropdown); this renders
   directly in the chat flow like a message. */
.ds-247420 .ov-approval {
    display: flex; flex-direction: column; gap: var(--space-2, 8px);
    max-width: var(--measure, 640px);
    padding: var(--space-4, 16px);
    border: var(--bw-hair) solid var(--rule);
    border-radius: var(--r-2, 12px);
    background: var(--panel, var(--bg-2));
    box-shadow: var(--shadow-1);
}
.ds-247420 .ov-approval-head { display: flex; align-items: center; gap: var(--space-2, 8px); color: var(--accent-ink); }
.ds-247420 .ov-approval-icon { display: inline-flex; flex: 0 0 auto; }
.ds-247420 .ov-approval-tool { color: var(--fg); }
.ds-247420 .ov-approval-cat { color: var(--fg-3); font-weight: 400; }
.ds-247420 .ov-approval-args {
    margin: 0; padding: var(--space-2, 8px);
    background: var(--panel-2, var(--rule)); border-radius: var(--r-1, 8px);
    font: var(--fs-tiny, 12px)/1.4 var(--ff-mono, monospace);
    color: var(--fg-3); max-height: 100px; overflow: auto; white-space: pre-wrap; word-break: break-all;
}
.ds-247420 .ov-approval-note {
    width: 100%; min-height: 48px; resize: vertical;
    padding: var(--space-2, 8px) var(--space-2-75, 12px);
    border: var(--bw-hair) solid var(--rule); border-radius: var(--r-1, 8px);
    background: var(--bg); color: var(--fg); font: inherit; box-sizing: border-box;
}
.ds-247420 .ov-approval-note:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 2px; }
.ds-247420 .ov-approval-actions { display: flex; gap: var(--space-2, 8px); flex-wrap: wrap; align-items: center; }
.ds-247420 .ov-approval-btn {
    appearance: none; border: var(--bw-hair) solid var(--rule); background: transparent;
    color: var(--fg); font: inherit; font-size: var(--fs-tiny, 13px); font-weight: 500;
    padding: var(--space-1-75, 6px) var(--space-3, 16px); border-radius: var(--r-pill, 999px); cursor: pointer;
}
.ds-247420 .ov-approval-btn:hover { background: var(--panel-2, var(--rule)); }
.ds-247420 .ov-approval-btn-primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }
.ds-247420 .ov-approval-btn-primary:hover { filter: brightness(1.05); }
.ds-247420 .ov-approval-btn-soft { background: var(--panel-2, var(--rule)); border-color: transparent; }
.ds-247420 .ov-approval-btn-deny { margin-left: auto; border-color: var(--danger); color: var(--danger-ink, var(--danger)); }
.ds-247420 .ov-approval-btn-deny:hover { background: var(--danger); color: var(--bg); }

/* CountdownDialog's ticking status line: tabular-nums so the number swap
   each second doesn't jitter the modal layout. */
.ds-247420 .ds-countdown-status {
    font-variant-numeric: tabular-nums; font-feature-settings: 'tnum';
    color: var(--fg-2);
}

.ds-247420 .ds-ep-divider-vertical {
    display: inline-block;
    align-self: stretch;
    width: 0;
    border-left: 1px solid var(--rule);
    margin: 0 var(--space-1-5, 5px);
}

/* community-app.css */
/* community-app.css — layout glue for mountCommunityApp (the full chat/community
   app GUI). Reuses the canonical app/body/main classes from app-shell.css and
   the community + voice component classes from community.css; this part only adds
   the app-composition wrapper, the server+channel rail pills, the voice view grid,
   and the category-color tokens the consumer avatars use. All scoped under
   .ds-247420. Additive — defines only the ca- prefix, group, rail-empty, vx-view
   classes plus the cat color tokens.

   Relationship to community.css: this is NOT a fork of community.css's content
   -- it is a small (~150-line), genuinely distinct app-shell WRAPPER around the
   canonical, complete community surface. community.css (~1650 lines) owns every
   .cm-* / .vx-* component (server rail, channel sidebar, chat header, member list,
   voice PTT/VAD/webcam, thread panel, forum, page view) and is the canonical,
   actively-referenced stylesheet -- it is the one listed in package.json's
   `exports`/`files`, the one scripts/build.mjs and scripts/lint-tokens.mjs treat
   as a first-class component sheet on its own, and the one THEME.md/
   COMPONENT_API.md document as the load-order anchor
   (colors_and_type.css -> app-shell.css -> community.css -> chat.css ->
   editor-primitives.css -> community-app.css -> app-surfaces.css). This file
   loads strictly AFTER community.css (see ui_kits/community-app/index.html) and
   only adds the .ca-* composition shell + category-color tokens that
   community.css deliberately has no opinion about -- it never redefines a
   .cm-* / .vx-* rule community.css already owns. Canonical tokens/rules live in
   community.css; only overrides/additions specific to mountCommunityApp's own
   composition live here. */

.ds-247420.ds-247420 .ca-app {
  display: flex;
  flex-direction: column;
  height: 100vh;
  height: 100dvh;
  min-height: 0;
  overflow: hidden;
}
.ds-247420.ds-247420 .ca-app > .app-body {
  display: flex;
  flex-direction: row;
  flex: 1;
  min-height: 0;
  overflow: hidden;
}
.ds-247420.ds-247420 .ca-app > .app-body > .app-main {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  padding: 0;
}

/* app topbar — mountCommunityApp's own sole chrome above the chat-head; had
   no rules anywhere (relying on raw flex defaults), which read as unaligned
   default spacing rather than a designed header row.
   Scoped under .ca-app: a bare `.app-topbar` selector here collided with the
   main AppShell's own `.app-topbar` (src/css/app-shell/topbar.css) — same
   class name, unrelated component. The main site's merged-chrome reset rule
   (`.app-chrome > .app-topbar { padding: 0; background: none; }`) is
   specificity (0,3,0); this block's un-scoped `.app-topbar` at (0,2,0)
   shouldn't have beaten it, but the mirrored dist build re-scopes every
   selector here to `.ds-247420.ds-247420` (0,4,0) to survive minification/
   dedup, which DID beat it — showing up live as the homepage topbar
   floating inset with a `--bg-2` card fill instead of flush. `.ca-app`
   confines every rule in this file to the community-app widget's own DOM
   subtree, the fix for both the source and its dist mirror. */
.ds-247420.ds-247420 .ca-app .app-topbar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  height: 48px;
  flex-shrink: 0;
  padding: 0 var(--space-3);
  background: var(--bg-2);
  border-bottom: 1px solid var(--panel-3, color-mix(in oklab, var(--fg) 10%, transparent));
}
.ds-247420.ds-247420 .ca-app .app-topbar .brand {
  display: flex;
  align-items: baseline;
  gap: var(--space-1);
  min-width: 0;
  font-weight: var(--fw-medium, 500);
  font-size: var(--fs-sm);
  color: var(--fg);
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.ds-247420.ds-247420 .ca-app .app-topbar .brand .slash { color: var(--fg-3); }
.ds-247420.ds-247420 .ca-app .app-topbar nav {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.ds-247420.ds-247420 .ca-app .app-topbar nav a {
  color: var(--fg-2);
  font-size: var(--fs-tiny);
  text-decoration: none;
  padding: var(--space-1) var(--space-1-5);
  border-radius: var(--r-1);
  transition: background var(--dur-snap, .12s) var(--ease, ease), color var(--dur-snap, .12s) var(--ease, ease);
}
.ds-247420.ds-247420 .ca-app .app-topbar nav a:hover { color: var(--fg); background: color-mix(in oklab, var(--fg) 8%, transparent); }
.ds-247420.ds-247420 .ca-app .app-topbar nav a:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: 2px; }

/* server + channel rail */
.ds-247420.ds-247420 .ca-rail {
  display: flex;
  flex-direction: column;
  gap: 2px;
  width: 220px;
  min-width: 220px;
  padding: 8px;
  overflow-y: auto;
  overflow-x: hidden;
  background: var(--bg-2, color-mix(in oklab, var(--bg) 92%, var(--fg)));
  scrollbar-width: thin;
}
.ds-247420.ds-247420 .ca-rail .group {
  font-size: 11px;
  font-weight: var(--fw-semibold, 600);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--fg-3, color-mix(in oklab, var(--fg) 55%, transparent));
  padding: 10px 8px 4px;
}
.ds-247420.ds-247420 .ca-rail .group-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-right: var(--space-1, 4px);
}
.ds-247420.ds-247420 .ca-rail .group-add-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  padding: 0;
  border: none;
  border-radius: var(--r-0);
  background: transparent;
  color: var(--fg-3, color-mix(in oklab, var(--fg) 55%, transparent));
  cursor: pointer;
}
.ds-247420.ds-247420 .ca-rail .group-add-btn:hover {
  background: var(--bg-3, color-mix(in oklab, var(--fg) 8%, transparent));
  color: var(--fg-2, color-mix(in oklab, var(--fg) 78%, transparent));
}
.ds-247420.ds-247420 .ca-rail a {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  border-radius: var(--r-0);
  color: var(--fg-2, color-mix(in oklab, var(--fg) 78%, transparent));
  text-decoration: none;
  font-size: var(--fs-sm, 14px);
  line-height: 1.2;
  transition: background 0.1s, color 0.1s;
}
.ds-247420.ds-247420 .ca-rail a:hover { background: color-mix(in oklab, var(--fg) 8%, transparent); color: var(--fg); }
.ds-247420.ds-247420 .ca-rail a.active { background: color-mix(in oklab, var(--accent) 22%, transparent); color: var(--fg); }
.ds-247420.ds-247420 .ca-rail a .glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  flex-shrink: 0;
  color: var(--fg-3, currentColor);
}
.ds-247420.ds-247420 .ca-rail a > span:nth-child(2) { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ds-247420.ds-247420 .ca-rail a .count {
  flex-shrink: 0;
  min-width: 18px;
  padding: 0 5px;
  border-radius: var(--r-1);
  background: var(--accent);
  color: var(--on-accent);
  font-size: 11px;
  font-weight: var(--fw-semibold, 600);
  text-align: center;
}
.ds-247420.ds-247420 .ca-rail .rail-empty {
  padding: 8px;
  color: var(--fg-3, color-mix(in oklab, var(--fg) 50%, transparent));
  font-size: var(--fs-sm, 14px);
  font-style: italic;
}

/* voice channel view */
.ds-247420.ds-247420 .vx-view {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
  overflow: hidden;
}
.ds-247420.ds-247420 .vx-view .vx-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 12px;
  flex: 1;
  min-height: 0;
  padding: 16px;
  overflow-y: auto;
  align-content: start;
}

/* ============================================================
   Responsive — collapse the server+channel rail on narrow viewports.
   community-app builds its own hand-rolled .ca-app/.app-body/.ca-rail tree
   (not AppShell()), so it does not inherit AppShell's @container drawer CSS
   (keyed off .app/.app-side-shell, which .ca-app/.ca-rail never establish).
   Mirrors the .cm-channel-sidebar off-canvas idiom in community.css 1:1: same
   breakpoint, same translateX(-110%) + .open reveal, same :has() scrim — so
   the .ca-rail drawer driven by MobileHeader's hamburger (mobileMenuOpen in
   the adapter snapshot) behaves identically to the community kit's sidebar.
   ============================================================ */
@media (max-width: 900px) {
  .ds-247420.ds-247420 .ca-rail {
    position: absolute;
    z-index: calc(var(--z-drawer) + 1);
    top: 0; left: 0; bottom: 0;
    width: 240px; min-width: 240px;
    transform: translateX(-110%);
    transition: transform var(--dur-base, 240ms) var(--ease, ease);
    box-shadow: 2px 0 16px color-mix(in oklab, var(--ink, #000) 30%, transparent);
  }
  .ds-247420.ds-247420 .ca-rail.open { transform: translateX(0); }
  .ds-247420.ds-247420 .ca-app:has(.ca-rail.open) .app-main {
    position: relative;
  }
  .ds-247420.ds-247420 .ca-app:has(.ca-rail.open) .app-main::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: var(--z-drawer);
    background: color-mix(in oklab, var(--ink, #000) 40%, transparent);
    pointer-events: none;
  }
}

/* app-surfaces.css */
/* app-surfaces.css — agentgui application-surface styling.
 *
 * Owns every design decision for the agentgui consumer app's own surfaces
 * (the bits the app composes around the flagship kit components): the project
 * filter pills, the working-directory bar, the resume banner, the backend
 * health chips, the settings two-column grid, the history empty state, the
 * boot splash, the chat control cluster, and the cross-cutting base (scrollbar
 * theming, focus rings, print). These previously lived as a hand-rolled inline
 * \3c style> block in agentgui's index.html; the kit now owns them so NO design
 * content lives in the consumer app.
 *
 * Every selector is written pre-scoped under `.ds-247420` (the consumer mounts
 * on <html class="ds-247420">), so the build's selector-prefixer leaves them
 * untouched rather than mis-compounding the `[attr]` / `*` / bare-element
 * selectors. All colour/space/radius/font values read from kit tokens
 * (colors_and_type.css) — there are no app-local colour fallbacks. */

/* Visually-hidden but screen-reader-available (aria-live announcer, labels). */
.ds-247420 .sr-only {
  position: absolute !important; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0);
  white-space: nowrap; border: 0;
}

/* Base: full-height app root reading kit tokens. */
.ds-247420, .ds-247420 body { margin: 0; height: 100%; }
.ds-247420 body {
  background: var(--bg);
  color: var(--fg);
  font-family: var(--ff-display, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif);
}
/* #root, not #app: every kit in this repo mounts to <div id="root">
   (every ui_kits index.html) -- #app never matches anything here and this rule
   was silently inert, which is why WorkspaceShell's height:100% chain broke
   (#root had no definite height to resolve against, so .ws-shell grew to
   content height instead of the viewport, pushing the composer off-screen
   on short viewports and reading as chat/composer overlap). */
.ds-247420 #root { height: 100vh; height: 100dvh; }
.ds-247420 #root > * { height: 100%; }

/* Themed thin scrollbars — the native chrome scrollbar is chunky/light and
   clashes with the dark theme on the history sidebar and settings column. */
.ds-247420 * {
  scrollbar-width: thin;
  scrollbar-color: color-mix(in srgb, var(--fg) 22%, transparent) transparent;
}
.ds-247420 *::-webkit-scrollbar { width: 9px; height: 9px; }
.ds-247420 *::-webkit-scrollbar-track { background: transparent; }
.ds-247420 *::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--fg) 20%, transparent);
  border-radius: 999px;
  border: 2px solid transparent;
  background-clip: padding-box;
}
.ds-247420 *::-webkit-scrollbar-thumb:hover {
  background: color-mix(in srgb, var(--fg) 34%, transparent);
  background-clip: padding-box;
}

/* We move focus to the page heading on tab change for AT users; suppress the
   resulting outline box on those programmatically-focused elements. */
.ds-247420 [data-prog-focus]:focus,
.ds-247420 [data-prog-focus]:focus-visible { outline: none !important; box-shadow: none !important; }

/* The connection-dot disc + pulse are the kit's canonical .status-dot-disc;
   only the inline-flex wrapper layout stays here. */
.ds-247420 .status-dot { display: inline-flex; align-items: center; gap: .4em; white-space: nowrap; }

/* Resume banner. */
.ds-247420 .resume-banner {
  padding: .6em .8em;
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  border-radius: var(--r-1, 8px);
  display: flex; justify-content: space-between; align-items: center; gap: .75em;
  margin-bottom: .75em;
}

/* History action buttons. */
.ds-247420 .history-actions { display: flex; gap: .5em; padding: 0 0 .75em 0; flex-wrap: wrap; }

/* Empty / loading state. */
.ds-247420 .empty-state { display: flex; gap: 8px; align-items: center; padding: .5em 0; white-space: normal; }
.ds-247420 p.empty-state { text-align: left; padding: .6em 0; color: var(--fg-3); }

/* Project filter pills. */
.ds-247420 .pill-row { display: flex; flex-wrap: wrap; gap: .35em; padding: .35em 0; }
.ds-247420 .pill {
  cursor: pointer; padding: .25em .7em; border-radius: 999px;
  border: 1px solid transparent; background: transparent;
  font: inherit; color: inherit; line-height: 1.4;
  transition: background-color .15s ease, border-color .15s ease;
  min-height: 28px;
}
.ds-247420 .pill:hover { background: color-mix(in srgb, var(--accent) 10%, transparent); }
.ds-247420 .pill.pill-active {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  border-color: color-mix(in srgb, var(--accent) 40%, transparent);
}
.ds-247420 .pill:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }

/* Subagent toggle (the control itself is the kit Checkbox). */
.ds-247420 .subagent-toggle { padding: .4em 0; min-height: 32px; }

.ds-247420 .field-error { color: var(--warn); }

/* Boot splash: static text painted with the HTML, replaced by the first mount. */
.ds-247420 .boot-splash { height: 100%; min-height: 60vh; display: flex; align-items: center; justify-content: center; color: var(--fg-3); font-size: .9rem; }

/* Chat control cluster in the crumb bar: model picker, +new, status. */
.ds-247420 .chat-controls { display: flex; align-items: center; gap: .6em; flex-wrap: wrap; }
.ds-247420 .chat-controls > * { flex: 0 0 auto; }
.ds-247420 .chat-controls select,
.ds-247420 .chat-controls .select,
.ds-247420 .chat-controls [role="combobox"] { min-width: 130px; max-width: 220px; }
.ds-247420 .chat-controls .status-dot { white-space: nowrap; }

/* Main content region layout: every tab is a min-height:0 scroll host; the
   chat tab additionally flexes as a column so the thread fills the viewport. */
.ds-247420 .agentgui-main { min-height: 0; }
.ds-247420 .agentgui-main-chat { display: flex; flex-direction: column; flex: 1; }
.ds-247420 .agentgui-main .ds-section { margin: 0 0 var(--space-4, 16px); }

/* Tab-switch entrance: navTo() sets a per-tab modifier class
   (agentgui-main-<tab>) on the SAME #agentgui-main DOM node every switch
   (webjsx patches props on the existing element rather than replacing it).
   A single shared animation-name bound to the constant .agentgui-main base
   class would only ever fire once (on first mount) since a CSS animation
   restarts only when the animation-name value itself changes or the
   element is detached/reattached - the base class never changes. Each tab
   gets its OWN animation-name (functionally identical keyframes, distinct
   names) so switching from e.g. agentgui-main-chat to agentgui-main-history
   genuinely changes which animation-name applies, which is what makes the
   browser replay the entrance on every switch. Matches the existing motion
   craft level of ds-alert-in/ds-card-in/agentchat-jump.show instead of every
   full-page tab swap happening with zero transition. */
@media (prefers-reduced-motion: no-preference) {
  .ds-247420 .agentgui-main-chat { animation: ds-main-in-chat var(--dur-base) var(--ease); }
  .ds-247420 .agentgui-main-history { animation: ds-main-in-history var(--dur-base) var(--ease); }
  .ds-247420 .agentgui-main-files { animation: ds-main-in-files var(--dur-base) var(--ease); }
  .ds-247420 .agentgui-main-live { animation: ds-main-in-live var(--dur-base) var(--ease); }
  .ds-247420 .agentgui-main-settings { animation: ds-main-in-settings var(--dur-base) var(--ease); }
}
@keyframes ds-main-in-chat { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
@keyframes ds-main-in-history { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
@keyframes ds-main-in-files { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
@keyframes ds-main-in-live { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
@keyframes ds-main-in-settings { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }

/* Readable backend health summary (replaces raw JSON dump). */
.ds-247420 .health-summary { display: flex; flex-wrap: wrap; gap: .4em; margin: .6em 0; }
.ds-247420 .health-chip {
  font-family: var(--ff-mono, ui-monospace, monospace);
  font-size: .8rem; padding: .15em .55em; border-radius: var(--r-1, 8px);
  background: color-mix(in srgb, var(--fg) 8%, transparent);
  color: var(--fg-2);
  border: var(--bw-hair, 1px) solid color-mix(in srgb, var(--fg) 12%, transparent);
}
.ds-247420 .health-summary.health-ok .health-chip:first-child {
  color: var(--accent-ink);
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  border-color: color-mix(in srgb, var(--accent) 40%, transparent);
}

/* Standalone connection-status chip — same visual vocabulary as
   .health-chip so a form's own connecting/connected/failed feedback reads
   consistently with the healthSummary row beneath it, instead of one being
   a colored chip and the other bare <p class="t-meta"> text. */
.ds-247420 .ds-status-chip {
  display: inline-flex; align-items: center; gap: .35em;
  font-family: var(--ff-mono, ui-monospace, monospace);
  font-size: .8rem; padding: .15em .55em; border-radius: var(--r-1, 8px);
  background: color-mix(in srgb, var(--fg) 8%, transparent);
  color: var(--fg-2);
  border: var(--bw-hair, 1px) solid color-mix(in srgb, var(--fg) 12%, transparent);
  margin: .4em 0;
}
.ds-247420 .ds-status-chip-ok {
  color: var(--accent-ink);
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  border-color: color-mix(in srgb, var(--accent) 40%, transparent);
}
.ds-247420 .ds-status-chip-error {
  color: var(--danger);
  background: color-mix(in srgb, var(--danger) 12%, transparent);
  border-color: color-mix(in srgb, var(--danger) 35%, transparent);
}

/* Compact working-directory bar. */
.ds-247420 .cwd-bar {
  display: flex; align-items: center; gap: .5em; flex-wrap: wrap;
  padding: .15em 0 .5em; font-size: .85rem;
}
.ds-247420 .cwd-bar-text {
  font-family: var(--ff-mono, ui-monospace, monospace);
  color: var(--fg-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 60vw;
}
.ds-247420 .cwd-bar-btn {
  cursor: pointer; font: inherit; line-height: 1.3;
  padding: .15em .55em; border-radius: var(--r-1, 8px);
  background: color-mix(in srgb, var(--fg) 8%, transparent);
  border: var(--bw-hair, 1px) solid color-mix(in srgb, var(--fg) 14%, transparent);
  color: var(--fg-2);
}
.ds-247420 .cwd-bar-btn:hover { background: color-mix(in srgb, var(--fg) 14%, transparent); }
.ds-247420 .cwd-bar-btn:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }

/* History no-session empty state: natural height, no leftover void below. */
.ds-247420 .history-empty {
  display: flex; flex-direction: column; align-items: center; justify-content: flex-start;
  gap: .4em; text-align: center; padding: var(--space-6, 40px) 0 var(--space-4, 16px); color: var(--fg-3);
}
.ds-247420 .history-empty-title { margin: 0; font-size: var(--fs-lg); color: var(--fg-2); }
.ds-247420 .history-empty-sub { margin: 0; max-width: 42ch; }
.ds-247420 .history-empty-hints { margin: var(--space-5, 24px) auto 0; max-width: 46ch; text-align: left; }

/* Settings: two-column on wide screens (backend + agents). Grid gap is the
   single source of rhythm between stacked Panels here - .panel's own
   margin-bottom would stack on top of the gap (32px + 24px = 56px, a break
   used nowhere else), so it's zeroed for direct grid children. */
.ds-247420 .settings-grid {
  display: grid; gap: var(--space-4, 16px);
  grid-template-columns: minmax(0, 1fr);
  align-items: start;
}
.ds-247420 .settings-grid > .panel { margin-bottom: 0; }
.ds-247420 .agentgui-main-settings .settings-grid { margin-top: 0; }

/* The kit Chat head computes its own zero-padded count and ignores our sub
   prop; hide it (streaming state shows via the title + busy banner). */
.ds-247420 .chat-head .sub { display: none; }

/* Event-list row hover + search-result flash. */
.ds-247420 .ds-event-list .row[role="button"]:hover { background: color-mix(in srgb, var(--fg) 5%, transparent); }
/* Opt-in like every other animation in the kit: the flash is declared only
   under no-preference, so a reduced-motion user gets the row without a 2s
   pulsing highlight. It animates background rather than transform, so the
   vestibular risk is low, but a repeating attention pull is still exactly the
   kind of thing the preference exists to suppress. The row is located by the
   scroll-into-view that accompanies it, so nothing is lost when it is absent. */
@media (prefers-reduced-motion: no-preference) {
  .ds-247420 .ds-event-list .row.event-flash { animation: agentgui-event-flash 2s ease-out; }
}


/* Generic interactive focus ring for app-emitted controls. */
.ds-247420 button:focus-visible,
.ds-247420 [tabindex]:focus-visible,
.ds-247420 a:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }

/* Settings-panel field spacing utilities (replace app.js inline style= margins). */
.ds-247420 .agentgui-field-mb { margin: 0 0 .5em; }
.ds-247420 .agentgui-field-my { margin: .5em 0; }

@keyframes agentgui-event-flash {
  0%   { background: color-mix(in srgb, var(--accent) 40%, transparent); }
  100% { background: transparent; }
}

@media (min-width: 900px) {
  .ds-247420 .settings-grid { grid-template-columns: minmax(0, 420px) minmax(0, 1fr); }
}

/* Tablet: keep the chat control cluster from wrapping the status dot alone. */
@media (max-width: 900px) {
  .ds-247420 .chat-controls select,
  .ds-247420 .chat-controls .select,
  .ds-247420 .chat-controls [role="combobox"] { min-width: 110px; max-width: 170px; }
}

/* Touch targets on small screens. */
@media (max-width: 640px) {
  .ds-247420 .pill { min-height: 36px; padding: .4em .8em; }
  .ds-247420 .history-actions { gap: .75em; }
  .ds-247420 .history-actions .btn, .ds-247420 .history-actions button { min-height: 44px; }
  .ds-247420 .cwd-bar-text { max-width: 42vw; }
  .ds-247420 .cwd-bar { gap: .3em; }
}

@media (prefers-reduced-motion: reduce) {
  .ds-247420 .ds-event-list .row.event-flash { animation: none; outline: 2px solid var(--accent-ink); }
}

@media print {
  /* Re-assert the paper-tuned signal tokens so token-derived foregrounds
     (status discs, error text, tool cards) print with light-theme colours
     rather than the dark-theme-derived values inherited under auto-dark. */
  .ds-247420 { --flame:#C53E00; --amber:#7C570F; --warn:#C41C0C; --sky:#3A6EFF; --bg:var(--paper); --fg:var(--ink); }
  .ds-247420 #app { min-height: auto; display: block; height: auto; }
  .ds-247420 .skip-link, .ds-247420 .status-dot, .ds-247420 .history-actions, .ds-247420 .chat-composer { display: none !important; }
  .ds-247420 .app, .ds-247420 .app-main, .ds-247420 .panel, .ds-247420 .chat, .ds-247420 .chat-thread {
    background: var(--paper) !important; color: var(--ink) !important; box-shadow: none !important;
  }
  /* Community surface: unclamp the fixed-height shell and hide the chrome so a
     transcript prints as flowing pages instead of one clipped viewport. */
  .ds-247420 .cm-shell { height: auto; overflow: visible; display: block; }
  .ds-247420 .cm-server-rail, .ds-247420 .cm-channel-sidebar, .ds-247420 .cm-member-list, .ds-247420 .cm-mobile-header, .ds-247420 .cm-voice-strip { display: none !important; }
  .ds-247420 .cm-main, .ds-247420 .cm-channel-list { overflow: visible; height: auto; }
}
/* Freddie dashboard surface utilities — freddie src/web/index.html inline
   styles migrated here so NO design content lives in the consumer app. */
/* Cap raw-JSON/code dumps so unbounded payloads scroll instead of blowing
   the page layout. */
.ds-247420 .fd-pre { max-height: 60vh; overflow: auto; }
/* Topbar leaf layout (sampler pill + theme toggle). */
.ds-247420 .fd-topbar-leaf { display: inline-flex; gap: var(--space-2, 8px); align-items: center; }
/* Ctrl+K search hint in the topbar. */
.ds-247420 .fd-search-hint {
  font-size: var(--fs-pico, 11px); opacity: 0.5;
  padding: var(--space-1, 4px) var(--space-2, 8px);
  border: 1px solid var(--rule, currentColor); border-radius: var(--r-0, 4px);
  pointer-events: none;
}
/* Status bar items — inline flex with gap. */
.ds-247420 .fd-status-item { display: inline-flex; align-items: center; gap: var(--space-1, 4px); }
/* Page error display — scrollable pre block for stack traces. */
.ds-247420 .fd-page-error { max-height: 60vh; overflow: auto; white-space: pre-wrap; word-break: break-all; }

/* gm-prose.css */
.ds-247420 .ds-prose { line-height: 1.65; }
.ds-247420 .ds-prose a { color: var(--panel-accent); }
.ds-247420 .ds-prose a:hover { background: none; color: var(--panel-accent); text-decoration-color: transparent; }
.ds-247420 .ds-prose pre, .ds-247420 .ds-prose table { overflow-x: auto; max-width: 100%; }
.ds-247420 .ds-prose .mermaid { max-width: 100%; }
.ds-247420 .ds-prose .mermaid svg { max-width: 100%; height: auto; cursor: zoom-in; }
.ds-247420 .ds-prose canvas {
  display: block;
  width: 100%;
  max-width: 100%;
  height: 300px;
  max-height: 300px;
}

.ds-247420 #ds-article-host, .ds-247420 .ds-prose {
  width: 100%;
  max-width: 100%;
  overflow: visible;
  min-height: 0;
}

.ds-247420 {
  --article-toc-rail-w: 256px;
  --article-toc-gutter-nonrhythm: 44px;
  --article-toc-header-gap: 16px;
}
.ds-247420 #ds-article-host.has-toc {
  display: flex;
  align-items: flex-start;
  column-gap: var(--article-toc-gutter-nonrhythm);
  gap: var(--article-toc-gutter-nonrhythm);
  max-width: calc(var(--article-toc-rail-w) + var(--article-toc-gutter-nonrhythm) + var(--measure));
  margin-left: auto;
  margin-right: auto;
}
.ds-247420 #ds-article-host.has-toc > .article-col {
  flex: 1 1 0;
  min-width: 0;
  max-width: var(--measure);
}
.ds-247420 #ds-article-host.has-toc > .toc {
  flex: 0 0 var(--article-toc-rail-w);
  align-self: flex-start;
  position: sticky;
  top: calc(var(--article-header-h, calc(var(--app-topbar-h, 0px) + var(--app-crumb-h, 0px))) + var(--article-toc-header-gap));
  align-self: start;
  max-height: calc(100vh - var(--article-header-h, calc(var(--app-topbar-h, 0px) + var(--app-crumb-h, 0px))) - 32px);
  max-height: calc(100dvh - var(--article-header-h, calc(var(--app-topbar-h, 0px) + var(--app-crumb-h, 0px))) - 32px);
  overflow-y: auto;
  overscroll-behavior: contain;
  margin: 0;
  max-width: 100%;
  padding: var(--space-3-5);
  background: var(--panel-1);
  border-radius: var(--r-1);
  box-shadow: var(--panel-shadow);
  scrollbar-width: thin;
  scrollbar-color: var(--panel-3) transparent;
}
.ds-247420 #ds-article-host.has-toc > .toc::-webkit-scrollbar { width: 8px; }
.ds-247420 #ds-article-host.has-toc > .toc::-webkit-scrollbar-track { background: transparent; }
.ds-247420 #ds-article-host.has-toc > .toc::-webkit-scrollbar-thumb {
  background: var(--panel-3);
  border-radius: var(--r-0);
  border: 2px solid transparent;
  background-clip: padding-box;
}
.ds-247420 #ds-article-host.has-toc > .toc::-webkit-scrollbar-thumb:hover {
  background: var(--panel-text-3);
  background-clip: padding-box;
}
.ds-247420 #ds-article-host .toc .toc-title,
.ds-247420 .ds-prose .gm-callout .who {
  font-family: var(--ff-mono);
  font-size: var(--fs-nano);
  text-transform: uppercase;
  letter-spacing: var(--tr-label);
  color: var(--panel-text-2);
}

.ds-247420 #ds-article-host .toc .toc-title {
  margin: 0 0 var(--space-3) 0;
}
.ds-247420 #ds-article-host .toc ol { margin: 0; padding-left: var(--space-3-5); }
.ds-247420 #ds-article-host .toc li {
  font-size: var(--fs-tiny);
  line-height: 1.85;
  color: var(--panel-text);
  min-height: 1.85em;
}
.ds-247420 #ds-article-host .toc li:nth-child(5n) { margin-bottom: var(--space-2); }
.ds-247420 #ds-article-host .toc li::marker { color: var(--panel-text-3); font-variant-numeric: tabular-nums; }
.ds-247420 #ds-article-host .toc a { color: var(--panel-text-2); text-decoration: none; transition: color .12s ease; }
.ds-247420 #ds-article-host .toc a:hover { color: var(--panel-accent); }
.ds-247420 #ds-article-host .toc li.toc-group {
  margin-top: var(--space-3);
  padding-top: var(--space-2);
  border-top: 1px solid var(--panel-3);
  list-style: none;
  font-family: var(--ff-mono);
  font-size: var(--fs-nano);
  text-transform: uppercase;
  letter-spacing: var(--tr-label);
  color: var(--panel-text-3);
}
.ds-247420 #ds-article-host .toc li.toc-group > ol {
  margin-top: var(--space-1-5);
  padding-left: var(--space-3);
}
.ds-247420 #ds-article-host .toc li.toc-group li {
  font-size: var(--fs-nano);
  line-height: 1.7;
  min-height: 1.7em;
}
.ds-247420 #ds-article-host .toc li.toc-group li:nth-child(5n) { margin-bottom: 0; }
.ds-247420 #ds-article-host.has-toc > .article-col > * { min-width: 0; }
@media (max-width: 900px) {
  .ds-247420 #ds-article-host.has-toc { display: block; }
  .ds-247420 #ds-article-host.has-toc > .article-col { max-width: 100%; }
  .ds-247420 #ds-article-host.has-toc > .toc {
    position: static;
    max-height: none;
    overflow: visible;
    margin: 0 0 var(--space-5) 0;
  }
}

.ds-247420 .ds-cli-block {
  --ds-cli-block-fs-mono-halfrung: 12.5px;
  display: block;
  background: var(--panel-1);
  border-radius: var(--r-0);
  padding: var(--space-3) var(--space-3-5);
  margin: var(--space-3) 0 var(--space-5) 0;
  font-family: var(--ff-mono);
  font-size: var(--ds-cli-block-fs-mono-halfrung);
  line-height: 1.6;
  color: var(--panel-text);
  box-shadow: var(--panel-shadow);
  overflow-x: auto;
  white-space: pre-wrap;
  word-break: break-word;
}
.ds-247420 .ds-cli-block .ds-cli-comment {
  color: var(--panel-text-3);
  white-space: pre-wrap;
  word-break: break-word;
  min-height: 1.4em;
  padding: var(--space-half) 0;
  line-height: 1.6;
}
.ds-247420 .ds-cli-block .ds-cli-comment:empty::before { content: '\00a0'; }
.ds-247420 .ds-cli-block .ds-cli-row {
  display: flex;
  gap: var(--space-2-5);
  padding: var(--space-half) 0;
  white-space: pre-wrap;
  word-break: break-word;
}
.ds-247420 .ds-cli-block .ds-cli-row .prompt {
  color: var(--panel-accent);
  flex: 0 0 auto;
  user-select: none;
}
.ds-247420 .ds-cli-block .ds-cli-row .cmd {
  color: var(--panel-text);
  flex: 1 1 auto;
  white-space: pre-wrap;
  word-break: break-word;
}

.ds-247420 .panel .row { align-items: flex-start; padding: var(--space-3) var(--space-3-5); gap: var(--space-3); }
.ds-247420 .panel .row + .row { box-shadow: inset 0 1px 0 var(--rule); }
.ds-247420 .panel .row .code { padding-top: var(--space-hair); min-width: 28px; }
.ds-247420 .panel .row .meta { padding-top: var(--space-hair); opacity: 0.4; }
.ds-247420 .panel .row .title { display: block; line-height: 1.4; }
.ds-247420 .panel .row .title .sub {
  display: block;
  margin-top: var(--space-1-75);
  font-weight: 400;
  color: var(--panel-text-3);
  font-size: var(--fs-tiny);
  line-height: 1.6;
}

.ds-247420 .ds-prose .card {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-2-75) var(--space-3-5);
  margin: var(--space-2) 0;
  background: var(--panel-1);
  border-radius: var(--r-0);
  font-size: var(--fs-tiny);
  box-shadow: var(--panel-shadow);
  text-decoration: none;
}
.ds-247420 .ds-prose .card:hover { background: var(--panel-hover); }
.ds-247420 .ds-prose .card .card-top { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-2-75); }
.ds-247420 .ds-prose .card .repo-name { font-weight: 600; color: var(--panel-text); }
.ds-247420 .ds-prose .card .stars { color: var(--panel-text-3); font-family: var(--ff-mono); font-size: var(--fs-micro); flex: 0 0 auto; }
.ds-247420 .ds-prose .card .desc { color: var(--panel-text-2); font-size: var(--fs-micro); line-height: 1.5; margin: 0; }
.ds-247420 .ds-prose .tags { display: flex; gap: var(--space-1-75); flex-wrap: wrap; }
.ds-247420 .ds-prose .tag {
  background: var(--panel-2);
  color: var(--panel-text-2);
  border: 1px solid var(--panel-3);
  font-size: 0.7rem;
  --tag-inset-rem-coupled-to-root-fontsize: 0.15rem 0.5rem;
  padding: var(--tag-inset-rem-coupled-to-root-fontsize);
  border-radius: var(--r-hair);
  font-family: var(--ff-mono);
}
.ds-247420 .ds-prose .gm-callout {
  position: relative;
  margin: var(--space-3-5) 0;
  padding: var(--space-2-75) var(--space-3-5);
  padding-left: calc(var(--space-3-5) + var(--bw-chunk));
  background: var(--panel-1);
  border-radius: 0 var(--r-1) var(--r-1) 0;
  color: var(--panel-text);
}
.ds-247420 .ds-prose .gm-callout::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0;
  width: var(--bw-chunk); background: var(--panel-accent);
  border-radius: var(--bw-chunk) 0 0 var(--bw-chunk);
}
.ds-247420 .ds-prose .gm-callout .who {
  display: block;
  margin-bottom: var(--space-1);
}

.ds-247420 .ds-prose .ds-prose-endnote,
.ds-247420 .ds-prose-endnote {
  margin-top: var(--space-5);
  margin-bottom: 0;
  font-family: var(--ff-mono);
  font-size: var(--fs-xs);
  line-height: 1.6;
  color: var(--panel-text-2);
}
.ds-247420 .ds-prose-endnote a { color: var(--panel-accent); text-decoration: none; }
.ds-247420 .ds-prose-endnote a:hover { text-decoration: underline; }

/* marketing.css */
/* marketing.css — landing / docs site surface family (the agentgui flatspace
 * site renderer composes these instead of carrying inline style= props). Every
 * selector is written pre-scoped under `.ds-247420` so the build's selector-
 * prefixer leaves it untouched (consumers mount on <html class="ds-247420">).
 * All values read kit tokens — no hardcoded hex, no inline design in the app. */

.ds-247420 .site-panel { margin: var(--space-2); max-width: 1100px; padding: var(--space-3, 16px); }

/* Hero block */
.ds-247420 .site-hero { padding: var(--space-4); }
.ds-247420 .site-hero-h { margin: 0 0 var(--space-2); }
.ds-247420 .site-hero-body { margin: var(--space-2) 0 var(--space-3); color: var(--fg-2); max-width: 64ch; }
.ds-247420 .site-chip-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin: 0 0 var(--space-3); }
.ds-247420 .site-cta-row { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }

/* Quickstart CLI block — these .cli/.prompt/.cmd nodes were previously unstyled. */
.ds-247420 .site-cli { padding: var(--space-3); background: var(--bg); border: var(--bw-hair, 1px) solid var(--rule); border-radius: var(--r-1); }
.ds-247420 .site-cli .cli { display: flex; align-items: baseline; gap: var(--space-2); font-family: var(--ff-mono); font-size: var(--fs-sm); padding: 2px 0; }
.ds-247420 .site-cli .prompt { color: var(--fg-3); user-select: none; flex: 0 0 auto; }
.ds-247420 .site-cli .cmd { color: var(--fg); white-space: pre-wrap; word-break: break-word; }

/* Embedded legacy doc (iframe wrapper). 180px = the page chrome (topbar +
 * crumb + footer) sitting above the embed; dvh keeps it mobile-safe. */
.ds-247420 .site-embed { width: 100%; height: calc(100dvh - 180px); min-height: 320px; border: 0; border-radius: var(--r-1); background: var(--bg-2); display: block; }

/* Marketing footer family (the in-app .app-status strip suppresses content
 * below 1100px; the site footer always shows its credits row). */
.ds-247420 .site-footer { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-3); width: 100%; padding: var(--space-3) var(--space-4); font-family: var(--ff-body); font-size: var(--fs-sm); line-height: 1.4; color: var(--fg-3); border-top: 1px solid var(--rule); }
.ds-247420 .site-footer .item { color: inherit; }
.ds-247420 .site-footer .item:first-of-type { color: var(--accent); }
.ds-247420 .site-footer .spread { flex: 1; }

/* spoint/loading-screen.css */
/* Loading-screen kit styles. Scoped under .ds-247420 at build time.
   All colors reference design tokens (no raw literals) per the token-lint gate. */
.ds-247420 .sp-loading {
  position: fixed;
  inset: 0;
  z-index: var(--z-top);
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--ff-mono);
  transition: opacity var(--dur-slow) var(--ease);
}
.ds-247420 .sp-loading-fade { opacity: 0; pointer-events: none; }
.ds-247420 .sp-loading-container {
  width: min(420px, 86vw);
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.ds-247420 .sp-loading-header h1 {
  margin: 0;
  font-family: var(--ff-display);
  font-size: var(--fs-h2);
  letter-spacing: 0.04em;
}
.ds-247420 .sp-loading-label {
  margin: 4px 0 0;
  color: var(--fg-2);
  font-size: var(--fs-body);
}
.ds-247420 .sp-loading-bars { display: flex; flex-direction: column; gap: 10px; }
.ds-247420 .sp-loading-bar-row {
  display: grid;
  grid-template-columns: 88px 1fr 44px;
  align-items: center;
  gap: 10px;
}
.ds-247420 .sp-loading-bar-name {
  color: var(--fg-3);
  font-size: var(--fs-small, 11px);
  text-transform: uppercase;
  letter-spacing: 0.1em;
}
.ds-247420 .sp-loading-bar-track {
  height: 6px;
  border-radius: var(--r-hair);
  background: var(--bg-3);
  overflow: hidden;
}
.ds-247420 .sp-loading-bar-fill {
  height: 100%;
  width: 0%;
  background: var(--accent);
  transition: width var(--dur-base) var(--ease);
}
.ds-247420 .sp-loading-bar-pct {
  color: var(--fg-2);
  font-size: var(--fs-small, 11px);
  text-align: right;
}
.ds-247420 .sp-loading-detail {
  min-height: 16px;
  color: var(--fg-3);
  font-size: var(--fs-small, 11px);
}

/* spoint/game-hud.css */
/* Game-HUD kit styles. Scoped under .ds-247420 at build time.
   Colors from semantic tokens (no raw literals) per the token-lint gate. */
.ds-247420 .sp-hud { pointer-events: none; font-family: var(--ff-mono); }
.ds-247420 .sp-hud-crosshair {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  font-size: 24px;
  color: var(--fg);
  text-shadow: 0 0 2px var(--bg);
}
.ds-247420 .sp-hud-ammo {
  position: fixed;
  /* Fixed chrome clears notch/home-indicator on cutout devices. */
  bottom: calc(50px + env(safe-area-inset-bottom, 0px));
  right: calc(20px + env(safe-area-inset-right, 0px));
  font-size: 24px;
  font-weight: bold;
  color: var(--fg);
  text-shadow: 0 0 4px var(--bg);
}
.ds-247420 .sp-hud-ammo-reloading { color: var(--warn); }
.ds-247420 .sp-hud-health {
  position: fixed;
  bottom: calc(20px + env(safe-area-inset-bottom, 0px));
  left: 50%;
  transform: translateX(-50%);
  width: 200px;
  height: 20px;
  background: var(--bg-3);
  border-radius: 4px;
  overflow: hidden;
}
.ds-247420 .sp-hud-health-fill {
  height: 100%;
  transition: width var(--dur-snap) var(--ease);
}
.ds-247420 .sp-hud-hp-high { background: var(--success); }
.ds-247420 .sp-hud-hp-mid { background: var(--warn); }
.ds-247420 .sp-hud-hp-low { background: var(--danger); }
.ds-247420 .sp-hud-health-num {
  position: absolute;
  width: 100%;
  text-align: center;
  color: var(--fg);
  font-size: 12px;
  line-height: 20px;
}
.ds-247420 .sp-hud-boost {
  position: fixed;
  top: 80px;
  right: calc(20px + env(safe-area-inset-right, 0px));
  padding: 8px 16px;
  background: var(--accent);
  color: var(--accent-fg);
  font-weight: bold;
  border-radius: 6px;
  font-size: 14px;
}

/* spoint/host-join-lobby.css */
/* Host/join lobby kit styles. Scoped under .ds-247420 at build time.
   Token-only colors (no raw literals) per the token-lint gate. */
.ds-247420 .sp-lobby {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: flex;
  align-items: center;
  justify-content: center;
  background: color-mix(in srgb, var(--bg) 70%, transparent);
  font-family: var(--ff-mono);
}
.ds-247420 .sp-lobby-card {
  position: relative;
  width: min(360px, 88vw);
  display: flex;
  flex-direction: column;
  gap: var(--space-2-75);
  padding: var(--space-4);
  background: var(--bg-2);
  border: 1px solid var(--bw-rule, var(--rule));
  border-radius: var(--r-2, 10px);
  color: var(--fg);
  /* Short landscape phones: keep lower controls reachable inside the card. */
  max-height: calc(100vh - 32px);
  max-height: calc(100dvh - 32px);
  overflow-y: auto;
  overscroll-behavior: contain;
}
.ds-247420 .sp-lobby-title {
  margin: 0;
  font-family: var(--ff-display);
  font-size: var(--fs-h3);
}
.ds-247420 .sp-lobby-sub {
  margin: 0;
  color: var(--fg-3);
  font-size: var(--fs-small, 12px);
}
.ds-247420 .sp-lobby-btn {
  appearance: none;
  border: 1px solid var(--bw-rule, var(--rule));
  background: var(--bg-3);
  color: var(--fg);
  font-family: var(--ff-mono);
  font-size: var(--fs-body);
  padding: 10px 14px;
  border-radius: var(--r-1, 8px);
  cursor: pointer;
}
.ds-247420 .sp-lobby-btn:hover { background: var(--bg-2); }
.ds-247420 .sp-lobby-btn:disabled { opacity: 0.6; cursor: default; }
.ds-247420 .sp-lobby-btn-primary {
  background: var(--accent);
  color: var(--accent-fg);
  border-color: var(--accent);
}
.ds-247420 .sp-lobby-join { display: flex; gap: 8px; }
.ds-247420 .sp-lobby-input {
  flex: 1;
  min-width: 0;
  background: var(--bg);
  color: var(--fg);
  border: 1px solid var(--bw-rule, var(--rule));
  border-radius: var(--r-1, 8px);
  padding: var(--space-2) var(--space-2-5);
  font: var(--fs-body) var(--ff-mono);
  outline: none;
}
.ds-247420 .sp-lobby-input:focus-visible { border-color: var(--accent); box-shadow: var(--focus-ring-inset); }
.ds-247420 .sp-lobby-link { font-size: var(--fs-small, 11px); }
.ds-247420 .sp-lobby-code {
  font-family: var(--ff-display);
  font-size: var(--fs-h2);
  letter-spacing: 0.12em;
  text-align: center;
  color: var(--accent);
  padding: 8px;
  background: var(--bg);
  border-radius: var(--r-1, 8px);
}
.ds-247420 .sp-lobby-err { min-height: 16px; color: var(--danger); font-size: var(--fs-small, 11px); }
.ds-247420 .sp-lobby-close {
  position: absolute;
  top: 10px;
  right: 12px;
  appearance: none;
  background: none;
  border: none;
  color: var(--fg-3);
  font-size: 16px;
  cursor: pointer;
  line-height: 1;
}
