/**
 * Diannara Component Layer
 * Import after variables.css (or tailwind/theme.css) in any Tailwind v4 project.
 *
 * Usage:
 *   @import "diannara/theme.css";
 *   @import "diannara/components.css";
 *
 * Then: <button class="btn btn-primary">Click</button>
 */

@layer base {
  /* color-scheme is set on :root by variables.css (dark by default,
     light under [data-theme="light"]) so it tracks the active theme. */

  body {
    background: var(--color-bg-deepest);
    color: var(--color-foreground-body);
    font-family: var(--font-family-body);
    font-size: var(--text-base);
    line-height: var(--font-line-height-normal);
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
  }

  /* Canonical page-level surface treatment — documented in specs/elevation.md.
     Apply to <body> (or a full-viewport wrapper) for the faint grain texture
     that breaks up digital perfection. Uses isolation + ::after so fixed
     headers and sidebars are unaffected. */
  .bg-grain {
    position: relative;
    isolation: isolate;
  }
  .bg-grain::after {
    content: '';
    position: absolute;
    inset: 0;
    opacity: 0.03;
    pointer-events: none;
    background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
    background-repeat: repeat;
    background-size: 256px 256px;
    z-index: -1;
  }

  .display {
    font-family: var(--font-family-display);
    font-weight: var(--font-weight-extrabold);
    letter-spacing: var(--font-letter-spacing-tight);
    line-height: var(--font-line-height-tight);
    color: var(--color-foreground);
  }

  /* Swap heading font to the display stack (Fraunces) on opt-in.
     Single-purpose alias for var(--font-heading) — keeps consumer markup
     terse for the recommended pattern (specs/typography.md). */
  .font-heading { font-family: var(--font-heading); }

  /* Heading defaults live in @layer components, NOT here — see the block at the
     top of that layer for why. Moving them was the fix for #183. */
}

@layer components {

  /* ===== HEADING DEFAULTS (specs/typography.md) =====
     These restore the Diannara type scale so <h1>–<h4> render with a visible
     hierarchy out of the box, without consumers applying utility classes.

     They live in @layer components rather than @layer base, and that is
     load-order-critical rather than cosmetic. Tailwind v4's preflight carries
     `h1,…,h6 { font-size: inherit }` in @layer base at the same specificity as
     these element selectors, so when both sat in @layer base the winner was
     decided by which stylesheet the browser parsed last. README.md tells
     consumers to link components.css FIRST, which put preflight second and made
     it win — every documented consumer shipped body-sized page titles, silently,
     with correct-looking CSS in devtools (#183).

     Tailwind declares `@layer theme, base, components, utilities` and this file
     declares base before components, so `components` sorts after `base` under
     either load order — these now beat preflight without depending on link order.

     Do NOT move them out of a layer entirely. Unlayered rules beat every layer,
     including @layer utilities, which would break `<h1 class="text-xl">` for
     every consumer. Being in a LATER layer is the fix; being in NO layer is a
     different and worse bug. They must also stay ahead of the component classes
     below, so a component's own heading styles still win inside this layer. */
  h1, h2, h3, h4 {
    font-family: var(--font-family-body);
    font-weight: var(--font-weight-extrabold);
    letter-spacing: var(--font-letter-spacing-tight);
    color: var(--color-foreground);
    margin: 0;
  }
  h1 { font-size: var(--text-3xl); line-height: var(--font-line-height-tight); }
  h2 { font-size: var(--text-2xl); line-height: var(--font-line-height-snug); }
  h3 { font-size: var(--text-xl);  line-height: var(--font-line-height-snug); }
  h4 { font-size: var(--text-lg);  font-weight: var(--font-weight-bold); line-height: var(--font-line-height-normal); }

  /* ===== SHARED MOTION =====
     Animations referenced by --animate-* tokens from dist/tailwind/theme.css.
     Tailwind v4 generates `animate-accent-pulse` utilities from those tokens,
     but the @keyframes themselves are plain CSS and live here. Used by status
     indicators (online dots, live markers, timeline cursors). */
  @keyframes accent-pulse {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.5; }
  }

  /* ===== LAYOUT PRIMITIVES =====
     Two classes that arrange children, and nothing else.

     They exist because of the class of bug #171 is an instance of: a container
     that decides how its children arrange can only ever hold ONE composition.
     .sidebar-footer shipped as a flex row because the avatar + name + role strip
     wanted a row, so a rail wanting two stacked ACTIONS had nowhere to go — the
     position (bottom-pinned, divided, non-shrinking) was welded to one layout.
     Every slot in this system had the same latent defect; the sidebar footer is
     just where a consumer hit it first.

     The rule going forward: a container owns POSITION — where it sits, what
     bounds it, what divides it. A layout primitive owns ARRANGEMENT. A consumer
     composes the two. Five stacked rows, three across, seven icons wrapping, or
     one arbitrary element — all the same slot, no new modifier per case.

     Bounded on purpose (pillar 5, pillar 6). This is not a utility layer: two
     directions, no width/spacing/colour escape hatches, no arbitrary values. A
     system where anything can be arranged anywhere stops looking like one system.

     Parameters ride on data-* rather than modifier classes because gap × align ×
     justify is combinatorial — `.stack-gap-4.stack-align-center` is exactly the
     class explosion this avoids — and because data-accent / data-tint /
     data-theme already establish attribute-as-parameter as the system's shape. */
  .stack,
  .cluster {
    display: flex;
    /* --layout-gap is set by the data-gap rules below and by nothing else, so it
       carries its default in the fallback rather than as a declaration. One
       property, one place, and the rule still renders if data-gap is absent —
       which is the case the fallback exists for. */
    gap: var(--layout-gap, var(--spacing-3));
  }
  .stack {
    flex-direction: column;
    /* Rail rows, form fields, card bodies: a stacked child almost always wants
       the container's full width. align-items:center is the opt-in, not this. */
    align-items: stretch;
  }
  .cluster {
    flex-direction: row;
    /* Wrapping is the default because the alternative is overflow. A row of
       seven icons in a 256px rail must go to a second line, not out of it. */
    flex-wrap: wrap;
    align-items: center;
  }

  /* Gap scale — the spacing steps from specs/spacing.md, no intermediate values.
     Scoped to the two primitives: a bare [data-gap] would collide with any
     consumer attribute of that name, which is pillar 7. */
  .stack[data-gap="0"],  .cluster[data-gap="0"]  { --layout-gap: 0; }
  .stack[data-gap="1"],  .cluster[data-gap="1"]  { --layout-gap: var(--spacing-1); }
  .stack[data-gap="2"],  .cluster[data-gap="2"]  { --layout-gap: var(--spacing-2); }
  .stack[data-gap="3"],  .cluster[data-gap="3"]  { --layout-gap: var(--spacing-3); }
  .stack[data-gap="4"],  .cluster[data-gap="4"]  { --layout-gap: var(--spacing-4); }
  .stack[data-gap="6"],  .cluster[data-gap="6"]  { --layout-gap: var(--spacing-6); }
  .stack[data-gap="8"],  .cluster[data-gap="8"]  { --layout-gap: var(--spacing-8); }
  .stack[data-gap="12"], .cluster[data-gap="12"] { --layout-gap: var(--spacing-12); }
  .stack[data-gap="16"], .cluster[data-gap="16"] { --layout-gap: var(--spacing-16); }

  /* Cross axis — align-items, passed straight through, so the axis it acts on
     flips with the direction. In a .cluster (row) the cross axis is vertical, so
     "start" is the TOP. In a .stack (column) it is horizontal, so "start" is the
     inline-start EDGE. Reach for data-justify, below, when you want position
     along the direction the children actually flow in. */
  .stack[data-align="start"],    .cluster[data-align="start"]    { align-items: flex-start; }
  .stack[data-align="center"],   .cluster[data-align="center"]   { align-items: center; }
  .stack[data-align="end"],      .cluster[data-align="end"]      { align-items: flex-end; }
  .stack[data-align="stretch"],  .cluster[data-align="stretch"]  { align-items: stretch; }
  .stack[data-align="baseline"], .cluster[data-align="baseline"] { align-items: baseline; }

  /* Main axis — justify-content, which flips the same way: the horizontal axis
     in a .cluster, the vertical one in a .stack. */
  .stack[data-justify="start"],   .cluster[data-justify="start"]   { justify-content: flex-start; }
  .stack[data-justify="center"],  .cluster[data-justify="center"]  { justify-content: center; }
  .stack[data-justify="end"],     .cluster[data-justify="end"]     { justify-content: flex-end; }
  .stack[data-justify="between"], .cluster[data-justify="between"] { justify-content: space-between; }
  .stack[data-justify="around"],  .cluster[data-justify="around"]  { justify-content: space-around; }

  /* ===== BUTTONS ===== */
  .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    padding: var(--button-padding-y) var(--button-padding-x);
    border-radius: var(--button-radius);
    font-size: var(--button-font-size);
    font-weight: var(--button-font-weight);
    line-height: 1;
    border: 1px solid transparent;
    cursor: pointer;
    white-space: nowrap;
    transition: all var(--duration-normal) var(--easing-spring);
    text-decoration: none;
  }
  .btn:active { transform: scale(0.98); }
  .btn:focus-visible {
    outline: none;
    box-shadow: var(--shadow-accent-focus);
  }
  .btn:disabled {
    opacity: 0.45;
    cursor: not-allowed;
    pointer-events: none;
  }
  .btn-xs {
    padding: var(--button-padding-y-xs) var(--button-padding-x-xs);
    font-size: var(--button-font-size-xs);
  }
  .btn-sm {
    padding: var(--button-padding-y-sm) var(--button-padding-x-sm);
    font-size: var(--button-font-size-sm);
  }
  .btn-lg {
    padding: var(--button-padding-y-lg) var(--button-padding-x-lg);
    font-size: var(--button-font-size-lg);
  }
  .btn-icon {
    padding: var(--button-padding-y);
    aspect-ratio: 1;
  }
  .btn-icon.btn-xs { padding: var(--button-padding-y-xs); }
  .btn-icon.btn-sm { padding: var(--button-padding-y-sm); }
  .btn-icon.btn-lg { padding: var(--button-padding-y-lg); }

  .btn svg { width: 20px; height: 20px; flex-shrink: 0; }
  .btn-xs svg { width: 14px; height: 14px; }
  .btn-sm svg { width: 16px; height: 16px; }
  .btn-lg svg { width: 24px; height: 24px; }
  .btn-icon svg { width: 20px; height: 20px; }
  .btn-icon.btn-xs svg { width: 14px; height: 14px; }
  .btn-icon.btn-sm svg { width: 16px; height: 16px; }
  .btn-icon.btn-lg svg { width: 24px; height: 24px; }

  .btn-primary {
    background: var(--color-accent-500);
    color: var(--color-on-accent);
  }
  .btn-primary:hover { background: var(--color-accent-400); transform: scale(1.02); }

  .btn-secondary {
    background: var(--color-neutral-300);
    color: var(--color-foreground-secondary);
    /* Bordered so it reads as a distinct control in light mode, where the
       neutral-300 fill sits very close to the surface. The border token is a
       faint edge in dark and a defining outline in light — the .btn base
       already reserves 1px, so this adds no layout shift. */
    border-color: var(--color-border-default);
  }
  .btn-secondary:hover { background: var(--color-accent-a08); color: var(--color-foreground); }

  /* Transparent, neutral-bordered button. Distinct from secondary
     (filled neutral) and from ghost (accent-tinted with accent border).
     Use for "neutral but bordered" CTAs and decorative choice buttons. */
  .btn-outline {
    background: transparent;
    border-color: var(--color-border-strong);
    color: var(--color-foreground-body);
  }
  .btn-outline:hover {
    background: transparent;
    border-color: var(--color-border-emphasis);
    color: var(--color-foreground);
  }

  .btn-ghost {
    background: var(--color-accent-a12);
    color: var(--color-accent-300);
    border-color: var(--color-accent-a20);
  }
  .btn-ghost:hover { background: var(--color-accent-a20); border-color: var(--color-accent-a35); }

  .btn-danger {
    background: var(--color-crimson-500);
    color: var(--color-on-accent);
  }
  .btn-danger:hover { background: var(--color-crimson-400); }
  .btn-danger:focus-visible { box-shadow: var(--shadow-danger-focus); }

  .btn-warning {
    background: var(--color-amber-500);
    color: var(--color-on-warning);
  }
  .btn-warning:hover { background: var(--color-amber-400); }
  .btn-warning:focus-visible { box-shadow: var(--shadow-warning-focus); }

  /* Link variant — text-only, no chrome, underline-on-hover. Pairs well
     with asChild for anchor-styled actions ("View All", inline links). */
  .btn-link {
    background: transparent;
    border-color: transparent;
    padding: 0;
    color: var(--color-accent-300);
    text-decoration: none;
  }
  .btn-link:hover {
    background: transparent;
    border-color: transparent;
    text-decoration: underline;
    text-underline-offset: 3px;
  }
  .btn-link:active { transform: none; }

  /* ===== ICON TOGGLE ===== */
  /* Icon-only two-state button (favorite / pin / bookmark). The icon itself
     carries the on/off signal via color; no track or thumb. Backed by
     @radix-ui/react-toggle in the React layer — Radix sets data-state="on|off"
     and aria-pressed automatically. See specs/components/icon-toggle.md. */
  .icon-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    aspect-ratio: 1;
    width: 36px;
    height: 36px;
    padding: 0;
    background: transparent;
    border: 0;
    border-radius: var(--radius-md);
    color: var(--color-foreground-muted);
    cursor: pointer;
    transition: color var(--duration-fast), background var(--duration-fast);
  }

  .icon-toggle-sm {
    width: 28px;
    height: 28px;
  }

  .icon-toggle:hover:not(:disabled) {
    color: var(--color-accent-300);
    background: var(--color-accent-a08);
  }

  .icon-toggle[data-state="on"] {
    color: var(--color-accent-400);
  }

  .icon-toggle[data-state="on"]:hover:not(:disabled) {
    color: var(--color-accent-300);
  }

  .icon-toggle:focus-visible {
    outline: none;
    box-shadow: var(--shadow-accent-focus);
  }

  .icon-toggle:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }

  /* ===== CARDS ===== */
  .card {
    background: var(--card-bg);
    border: 1px solid var(--card-border);
    border-radius: var(--card-radius);
    padding: var(--card-padding);
    box-shadow: var(--shadow-sm);
  }
  .card-flush { padding: 0; }
  /* The Image Card's cover, documented in card.md since before the class existed —
     --card-image-height shipped naming a part no rule implemented (#163). Pairs with
     .card-flush, which removes the padding this image needs to sit against the edges. */
  .card-image {
    display: block;
    width: 100%;
    height: var(--card-image-height);
    object-fit: cover;
  }
  .card-accent-top { border-top: 4px solid var(--color-accent-400); }
  .card-accent-left { border-left: 4px solid var(--color-accent-400); }
  .card-accent-bottom { border-bottom: 4px solid var(--color-accent-400); }
  .card-accent-right { border-right: 4px solid var(--color-accent-400); }

  /* Tinted cards — colored background wash */
  .card-tinted-accent { background: var(--color-accent-a08); border-color: var(--color-accent-a15); }
  .card-tinted-accent-strong { background: var(--color-accent-a10); border-color: var(--color-accent-a20); }
  .card-tinted-success { background: color-mix(in srgb, var(--color-success) 8%, transparent); border-color: color-mix(in srgb, var(--color-success) 15%, transparent); }
  .card-tinted-success-strong { background: color-mix(in srgb, var(--color-success) 10%, transparent); border-color: color-mix(in srgb, var(--color-success) 20%, transparent); }
  .card-tinted-warning { background: color-mix(in srgb, var(--color-warning) 8%, transparent); border-color: color-mix(in srgb, var(--color-warning) 15%, transparent); }
  .card-tinted-warning-strong { background: color-mix(in srgb, var(--color-warning) 10%, transparent); border-color: color-mix(in srgb, var(--color-warning) 20%, transparent); }
  .card-tinted-error { background: color-mix(in srgb, var(--color-error) 8%, transparent); border-color: color-mix(in srgb, var(--color-error) 15%, transparent); }
  .card-tinted-error-strong { background: color-mix(in srgb, var(--color-error) 10%, transparent); border-color: color-mix(in srgb, var(--color-error) 20%, transparent); }
  .card-tinted-info { background: color-mix(in srgb, var(--color-info) 8%, transparent); border-color: color-mix(in srgb, var(--color-info) 15%, transparent); }
  .card-tinted-info-strong { background: color-mix(in srgb, var(--color-info) 10%, transparent); border-color: color-mix(in srgb, var(--color-info) 20%, transparent); }
  .card[data-tint] { background: color-mix(in srgb, var(--color-tint-500) 8%, transparent); border-color: color-mix(in srgb, var(--color-tint-500) 15%, transparent); }

  /* Structured slots — auto-flush card padding and flex-column when any slot is present.
     Flex-column + .card-body flex:1 lets the body grow to fill extra height when the
     card is stretched by a grid row, so the footer stays pinned to the bottom. */
  .card:has(.card-header, .card-body, .card-footer) {
    padding: 0;
    display: flex;
    flex-direction: column;
  }

  .card-header {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--spacing-3);
    padding: 16px 24px;
    border-bottom: 1px solid var(--color-border-default);
    flex-shrink: 0;
  }

  .card-body {
    padding: 24px;
    flex: 1;
  }

  .card-footer {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--spacing-3);
    padding: 16px 24px;
    border-top: 1px solid var(--color-border-default);
    flex-shrink: 0;
  }

  .card-title {
    font-size: var(--text-lg);
    font-weight: var(--font-weight-bold);
    color: var(--color-foreground);
    letter-spacing: -0.3px;
    margin: 0;
  }

  /* Muted secondary text — typically rendered after CardTitle or inside
     CardBody. flex-basis: 100% so when placed in CardHeader (a wrap row),
     it forces its own line below CardTitle without consumers having to
     wrap it in another flex column. */
  .card-description {
    flex-basis: 100%;
    font-size: var(--text-sm);
    color: var(--color-foreground-muted);
    margin: 0;
    line-height: 1.5;
  }

  .card-actions {
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: var(--spacing-2);
  }

  /* Compact size — reduce slot padding and gaps for dense list contexts.
     Title font stays unchanged for readability (--text-lg, 19px). */
  .card-sm { padding: var(--spacing-4); }
  .card-sm .card-header,
  .card-sm .card-footer { padding: var(--spacing-3) var(--spacing-4); gap: var(--spacing-2); }
  .card-sm .card-body { padding: var(--spacing-4); }

  /* Tinted cards — header/footer dividers harmonize with tint color */
  .card-tinted-accent .card-header,
  .card-tinted-accent-strong .card-header { border-bottom-color: var(--color-accent-a15); }
  .card-tinted-accent .card-footer,
  .card-tinted-accent-strong .card-footer { border-top-color: var(--color-accent-a15); }

  .card-tinted-success .card-header,
  .card-tinted-success-strong .card-header { border-bottom-color: color-mix(in srgb, var(--color-success) 15%, transparent); }
  .card-tinted-success .card-footer,
  .card-tinted-success-strong .card-footer { border-top-color: color-mix(in srgb, var(--color-success) 15%, transparent); }

  .card-tinted-warning .card-header,
  .card-tinted-warning-strong .card-header { border-bottom-color: color-mix(in srgb, var(--color-warning) 15%, transparent); }
  .card-tinted-warning .card-footer,
  .card-tinted-warning-strong .card-footer { border-top-color: color-mix(in srgb, var(--color-warning) 15%, transparent); }

  .card-tinted-error .card-header,
  .card-tinted-error-strong .card-header { border-bottom-color: color-mix(in srgb, var(--color-error) 15%, transparent); }
  .card-tinted-error .card-footer,
  .card-tinted-error-strong .card-footer { border-top-color: color-mix(in srgb, var(--color-error) 15%, transparent); }

  .card-tinted-info .card-header,
  .card-tinted-info-strong .card-header { border-bottom-color: color-mix(in srgb, var(--color-info) 15%, transparent); }
  .card-tinted-info .card-footer,
  .card-tinted-info-strong .card-footer { border-top-color: color-mix(in srgb, var(--color-info) 15%, transparent); }
  .card[data-tint] .card-header { border-bottom-color: color-mix(in srgb, var(--color-tint-500) 15%, transparent); }
  .card[data-tint] .card-footer { border-top-color: color-mix(in srgb, var(--color-tint-500) 15%, transparent); }

  /* Stat card — horizontal layout with icon, value, trend */
  .stat-card {
    background: var(--card-bg);
    border: 1px solid var(--card-border);
    border-radius: var(--card-radius);
    padding: var(--card-padding);
    display: flex;
    align-items: center;
    gap: var(--spacing-6);
  }
  /* Left accent (default) */
  .stat-card-accent { border-left: 4px solid var(--color-accent-400); }
  .stat-card-success { border-left: 4px solid var(--color-success); }
  .stat-card-warning { border-left: 4px solid var(--color-warning); }
  .stat-card-error { border-left: 4px solid var(--color-error); }
  .stat-card-info { border-left: 4px solid var(--color-info); }
  /* Top accent */
  .stat-card-accent-top { border-top: 4px solid var(--color-accent-400); }
  .stat-card-success-top { border-top: 4px solid var(--color-success); }
  .stat-card-warning-top { border-top: 4px solid var(--color-warning); }
  .stat-card-error-top { border-top: 4px solid var(--color-error); }
  .stat-card-info-top { border-top: 4px solid var(--color-info); }
  /* Bottom accent */
  .stat-card-accent-bottom { border-bottom: 4px solid var(--color-accent-400); }
  .stat-card-success-bottom { border-bottom: 4px solid var(--color-success); }
  .stat-card-warning-bottom { border-bottom: 4px solid var(--color-warning); }
  .stat-card-error-bottom { border-bottom: 4px solid var(--color-error); }
  .stat-card-info-bottom { border-bottom: 4px solid var(--color-info); }
  /* Right accent */
  .stat-card-accent-right { border-right: 4px solid var(--color-accent-400); }
  .stat-card-success-right { border-right: 4px solid var(--color-success); }
  .stat-card-warning-right { border-right: 4px solid var(--color-warning); }
  .stat-card-error-right { border-right: 4px solid var(--color-error); }
  .stat-card-info-right { border-right: 4px solid var(--color-info); }

  /* The icon tile. The box was always here — 48px, rounded, centred — but it
     carried no background and no tone, so every consumer hand-built a toned tile
     around their glyph and ended up declaring the tone twice: once as the card's
     `tone`, once as the tile's colours. Nothing kept the two in step, so a success
     card could render an info tile and neither TypeScript nor CSS objected (#177).
     The tile now takes its colours from the card's tone, which React plumbs
     through context, so `tone` has exactly one source.

     Sizes are unchanged at 48px/24px — that pairing is already shipped AND
     documented in specs/icons.md's container-sizes-its-SVG table, so existing
     call sites keep the glyph size they have. */
  .stat-card-icon {
    width: 48px;
    height: 48px;
    border-radius: var(--radius-lg);
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    /* Untoned default: the accent wash, matching a card with no tone set. */
    background: var(--color-accent-a10);
    color: var(--color-accent-300);
  }
  .stat-card-icon svg { width: 24px; height: 24px; }
  .stat-card-icon-accent  { background: var(--color-accent-a10);  color: var(--color-accent-300); }
  .stat-card-icon-success { background: var(--color-success-bg);  color: var(--color-success-text); }
  .stat-card-icon-warning { background: var(--color-warning-bg);  color: var(--color-warning-text); }
  .stat-card-icon-error   { background: var(--color-error-bg);    color: var(--color-error-text); }
  .stat-card-icon-info    { background: var(--color-info-bg);     color: var(--color-info-text); }

  .stat-card-body {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: var(--spacing-1);
  }
  .stat-card-label {
    display: block;
    font-size: var(--text-sm);
    color: var(--color-foreground-body);
    margin: 0;
  }
  .stat-card-value {
    display: block;
    font-size: var(--text-xl);
    font-weight: var(--font-weight-extrabold);
    color: var(--color-foreground);
    letter-spacing: var(--font-letter-spacing-tight);
    line-height: var(--font-line-height-tight);
    margin: 0;
  }
  .stat-card-trend {
    font-size: var(--text-sm);
    font-weight: var(--font-weight-semibold);
    display: flex;
    align-items: center;
    gap: 0.25rem;
  }
  .stat-card-trend svg { width: 16px; height: 16px; }
  .stat-card-trend-up { color: var(--color-success); }
  .stat-card-trend-down { color: var(--color-error); }

  /* Stat grid — the container-width companion to the .stat-card family.
     Column count follows the space the grid ACTUALLY has, not the viewport: a
     dashboard main area is narrowed by a docked sidebar and rail, so viewport
     breakpoints misreport it (at 1280 with both docked, main is ~556px — four
     viewport-chosen columns would be ~130px each). auto-fit drops a column before
     any cell goes under the 240px floor, and collapses empty tracks so N cards
     never make more than N columns. min(240px, 100%) keeps the floor from
     overflowing a host narrower than one card. This is the shipped example of
     specs/responsive.md's "add intermediate layout logic with Grid, don't invent
     breakpoints between the steps". */
  .stat-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
    gap: var(--spacing-4);
  }

  /* ===== HERO (specs/components/hero.md) =====
     A page-level banner. Per specs/layout.md, .hero owns POSITION ONLY — bounds,
     padding, radius, surface, and the accent signature. It sets no flex-direction,
     no gap, no align/justify, and has no opinion about whether it contains a title,
     buttons, or an aside. The consumer composes .stack / .cluster inside it.

     That is the whole design. Three separate hero mockups were built (boxed +
     top stripe, bare + vertical rule, boxed + accent glow) and every one of them
     is reachable here as a combination of independent attributes rather than as
     its own class — signature x edge x surface x size is combinatorial, and
     .hero-stripe-top-elevated-lg is the class explosion layout.md exists to avoid.

     Parameters are scoped to .hero in every selector, so a bare data-edge on
     consumer markup can never collide with these rules. */
  .hero {
    position: relative;
    padding: var(--hero-padding-y) var(--hero-padding-x);
    /* Contains the stripe and the glow, both of which are absolutely positioned
       and must be clipped to the radius. Without this the stripe squares off the
       top corners of an elevated hero — the detail most implementations miss. */
    overflow: hidden;
    /* Own stacking context, so the glow can sit at z-index -1: above .hero's own
       background but below its content, with NOTHING required of the children.
       The alternative — `.hero > * { position: relative; z-index: 1 }` — would
       have worked visually and broken any child that wanted position: absolute,
       which is precisely the "container dictating to its children" that
       specs/layout.md forbids. */
    isolation: isolate;
  }
  .hero[data-size="sm"] { padding-block: var(--hero-padding-y-sm); }
  .hero[data-size="lg"] { padding-block: var(--hero-padding-y-lg); }

  /* Surface. Default is bare: a plain .hero contributes rhythm and nothing else,
     so it inherits the page the way a section does. Opting IN to the box is less
     surprising than opting out of one you never asked for. */
  .hero[data-surface="elevated"] {
    background: var(--hero-bg);
    border: 1px solid var(--hero-border);
    border-radius: var(--hero-radius);
  }

  /* --- Signature: stripe -------------------------------------------------
     The .modal-stripe gradient, promoted to page scale and free to sit on any
     edge. data-edge="left" is not a different feature — it is this same gradient
     rotated, which is why both live under one attribute. Space-separated tokens
     (~=) rather than a single value, so data-signature="stripe glow" composes
     without needing a "both" keyword. */
  .hero[data-signature~="stripe"]::before {
    content: "";
    position: absolute;
    z-index: 1;
    /* Same reason as the glow. At z-index 1 the stripe paints above content, and
       on a left/right edge it is a full-height band — so without this it would
       swallow clicks from anything a consumer places against that edge. It is
       decoration; it should never be a hit target. */
    pointer-events: none;
    background: linear-gradient(
      var(--hero-stripe-angle, 90deg),
      var(--color-accent-500),
      transparent
    );
  }
  .hero[data-signature~="stripe"]::before,
  .hero[data-signature~="stripe"][data-edge="top"]::before {
    inset: 0 0 auto 0;
    height: var(--hero-stripe-height);
    --hero-stripe-angle: 90deg;
  }
  .hero[data-signature~="stripe"][data-edge="bottom"]::before {
    inset: auto 0 0 0;
    height: var(--hero-stripe-height);
    --hero-stripe-angle: 90deg;
  }
  .hero[data-signature~="stripe"][data-edge="left"]::before {
    inset: 0 auto 0 0;
    width: var(--hero-stripe-height);
    height: auto;
    --hero-stripe-angle: 180deg;
  }
  .hero[data-signature~="stripe"][data-edge="right"]::before {
    inset: 0 0 0 auto;
    width: var(--hero-stripe-height);
    height: auto;
    --hero-stripe-angle: 180deg;
  }

  /* --- Signature: glow ---------------------------------------------------
     The accent as a light source rather than a mark. Built from the shipped
     --color-accent-a* steps, peaking at 20% and falling to nothing before the
     far edge: a wash that reaches the opposite side has stopped being light and
     become a fill, which is the failure mode this is tuned against.

     Off-centre along the entry edge on purpose (--hero-glow-offset) — a centred
     radial reads as a stock gradient, a directional one reads as illumination. */
  .hero[data-signature~="glow"]::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    background: radial-gradient(
      var(--hero-glow-size) var(--hero-glow-size)
        at var(--hero-glow-x, var(--hero-glow-offset)) var(--hero-glow-y, 0%),
      var(--color-accent-a20),
      var(--color-accent-a08) 45%,
      transparent 70%
    );
  }
  .hero[data-signature~="glow"][data-edge="top"]::after {
    --hero-glow-x: var(--hero-glow-offset);
    --hero-glow-y: 0%;
  }
  .hero[data-signature~="glow"][data-edge="bottom"]::after {
    --hero-glow-x: var(--hero-glow-offset);
    --hero-glow-y: 100%;
  }
  .hero[data-signature~="glow"][data-edge="left"]::after {
    --hero-glow-x: 0%;
    --hero-glow-y: var(--hero-glow-offset);
  }
  .hero[data-signature~="glow"][data-edge="right"]::after {
    --hero-glow-x: 100%;
    --hero-glow-y: var(--hero-glow-offset);
  }

  /* --- Type roles --------------------------------------------------------
     Font, size and measure only — no margin, no display, no alignment. Same
     contract as .card-title / .card-description: a type role never arranges. */
  .hero-eyebrow {
    display: inline-block;
    font-size: var(--hero-eyebrow-size);
    font-weight: var(--font-weight-semibold);
    letter-spacing: var(--hero-eyebrow-tracking);
    text-transform: uppercase;
    color: var(--color-accent-300);
  }
  .hero-title {
    font-family: var(--font-family-display);
    font-size: var(--hero-title-size);
    font-weight: var(--font-weight-extrabold);
    line-height: 1.1;
    letter-spacing: -0.02em;
    color: var(--color-foreground);
    margin: 0;
  }
  .hero-subtitle {
    font-size: var(--hero-subtitle-size);
    /* A measure, not a layout width — caps line length so the paragraph reads as
       set text while the hero itself stays fluid. */
    max-width: var(--hero-subtitle-measure);
    color: var(--color-foreground-body);
    margin: 0;
  }

  /* ===== PAGE HEADER (specs/components/page-header.md) =====
     The quiet sibling of .hero: the title block at the top of an ordinary app
     screen. Same contract, and for the same reason — it owns POSITION ONLY (what
     bounds it below, what divides it) and arranges nothing.

     It is deliberately tiny. Title-left / actions-right is a .cluster, not a
     feature of this class: the moment a container commits to that row it can only
     ever hold that one composition, which is the .sidebar-footer defect
     specs/layout.md was written about. A page header with no actions, or with a
     tab bar under the title, or with a breadcrumb above it, all have to work.

     No accent signature here, unlike .hero. Every screen in an app carries one of
     these; a brand mark repeated on every screen stops reading as a signature and
     starts reading as chrome. The hero is the surface that gets to be loud. */
  .page-header {
    padding-block-end: var(--page-header-padding-block-end);
  }
  /* Opt-in. A rule under every page title is a strong default that fights any
     screen whose first content block already has a top border (a table, a card). */
  .page-header[data-divider="true"] {
    border-bottom: 1px solid var(--page-header-divider);
  }
  .page-header-title {
    font-family: var(--font-family-display);
    font-size: var(--page-header-title-size);
    font-weight: var(--font-weight-extrabold);
    line-height: 1.2;
    letter-spacing: -0.015em;
    color: var(--color-foreground);
    margin: 0;
  }
  .page-header-description {
    font-size: var(--page-header-description-size);
    max-width: var(--page-header-description-measure);
    color: var(--color-foreground-body);
    margin: 0;
  }

  /* ===== BADGES ===== */
  .badge {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    padding: var(--badge-padding-y) var(--badge-padding-x);
    border-radius: var(--badge-radius);
    font-size: var(--badge-font-size);
    font-weight: var(--font-weight-medium);
    line-height: 1;
    white-space: nowrap;
  }
  .badge svg { width: 14px; height: 14px; }
  .badge-accent { background: var(--color-accent-a15); color: var(--color-accent-300); }
  .badge-success { background: color-mix(in srgb, var(--color-success) 15%, transparent); color: var(--color-success-text); }
  .badge-warning { background: color-mix(in srgb, var(--color-warning) 15%, transparent); color: var(--color-warning-text); }
  .badge-error { background: color-mix(in srgb, var(--color-error) 15%, transparent); color: var(--color-error-text); }
  .badge-info { background: color-mix(in srgb, var(--color-info) 15%, transparent); color: var(--color-info-text); }
  .badge-neutral { background: var(--color-neutral-350); color: var(--color-foreground-muted); }
  /* Stylistic variants — orthogonal to tone, compose via class composition.
     .badge-outline uses inset box-shadow rather than `border` so the variant
     does not change the badge's rendered dimensions (a real 1px border would
     shift layout 2px in each direction relative to solid). */
  .badge-outline { background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, currentColor 50%, transparent); }
  .badge-ghost { background: transparent; }

  /* ===== ALERTS ===== */
  .alert {
    display: flex;
    align-items: center;
    gap: var(--spacing-3);
    padding: var(--spacing-4) var(--spacing-6);
    border-radius: var(--alert-radius);
    font-size: var(--text-sm);
    color: var(--color-foreground-secondary);
    border: 1px solid transparent;
    border-left: var(--alert-stripe-width) solid transparent;
  }
  .alert svg { width: 20px; height: 20px; flex-shrink: 0; }
  .alert-success {
    background: color-mix(in srgb, var(--color-success) 6%, transparent);
    border-color: var(--color-success-bg);
    border-left-color: var(--color-success);
  }
  .alert-success svg { color: var(--color-success); }
  .alert-warning {
    background: color-mix(in srgb, var(--color-warning) 6%, transparent);
    border-color: var(--color-warning-bg);
    border-left-color: var(--color-warning);
  }
  .alert-warning svg { color: var(--color-warning); }
  .alert-error {
    background: color-mix(in srgb, var(--color-error) 6%, transparent);
    border-color: var(--color-error-bg);
    border-left-color: var(--color-error);
  }
  .alert-error svg { color: var(--color-error); }
  .alert-info {
    background: color-mix(in srgb, var(--color-info) 6%, transparent);
    border-color: var(--color-info-bg);
    border-left-color: var(--color-info);
  }
  .alert-info svg { color: var(--color-info); }

  /* ===== TOASTS ===== */
  .toast {
    background: var(--toast-bg);
    border: 1px solid var(--toast-border);
    border-radius: var(--toast-radius);
    box-shadow: var(--shadow-sm);
    padding: var(--spacing-4);
    display: flex;
    align-items: flex-start;
    gap: var(--spacing-3);
  }
  .toast-success { border-left: var(--toast-stripe-width) solid var(--color-success); }
  .toast-error { border-left: var(--toast-stripe-width) solid var(--color-error); }
  .toast-warning { border-left: var(--toast-stripe-width) solid var(--color-warning); }
  .toast-info { border-left: var(--toast-stripe-width) solid var(--color-info); }
  .toast-accent { border-left: var(--toast-stripe-width) solid var(--color-accent-400); }

  .toast-icon {
    width: 24px;
    height: 24px;
    border-radius: var(--radius-full);
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
  }
  .toast-icon svg { width: 14px; height: 14px; }

  /* The tone has to reach the icon disc. .toast-icon above is geometry only, so a
     hand-written `<div class="toast-icon">` — exactly what toast.md's Code Example
     shows — rendered a transparent circle with a currentColor glyph, and every
     consumer patched it inline. .alert already ships the equivalent
     (`.alert-<tone> svg { color: … }`); these are its missing counterpart.
     Accent uses --color-accent-a10, the 10%-alpha accent tint, which is the same
     recipe --color-<tone>-bg-subtle uses for the status tones. */
  .toast-success .toast-icon { background: var(--color-success-bg-subtle); color: var(--color-success); }
  .toast-error   .toast-icon { background: var(--color-error-bg-subtle);   color: var(--color-error); }
  .toast-warning .toast-icon { background: var(--color-warning-bg-subtle); color: var(--color-warning); }
  .toast-info    .toast-icon { background: var(--color-info-bg-subtle);    color: var(--color-info); }
  .toast-accent  .toast-icon { background: var(--color-accent-a10);        color: var(--color-accent-400); }
  .toast-title { font-size: var(--text-xs); font-weight: var(--font-weight-bold); color: var(--color-foreground); }
  .toast-body { font-size: var(--text-xs); color: var(--color-foreground-subtle); }

  /* ===== SNACKBAR =====
     Floating transient notification. Distinct from .toast (which is a
     static layout chip). Container positions the stack; individual
     .snackbar slides in when .open is applied, slides out when removed.
     Auto-dismiss timing is a consumer concern (JS); CSS only covers
     entrance/exit transitions. */
  .snackbar-container {
    position: fixed;
    bottom: var(--snackbar-container-offset);
    right: var(--snackbar-container-offset);
    z-index: var(--snackbar-z-index);
    display: flex;
    flex-direction: column;
    gap: var(--snackbar-container-gap);
    pointer-events: none;
  }
  .snackbar-container-center {
    right: auto;
    left: 50%;
    transform: translateX(-50%);
    align-items: center;
  }

  .snackbar {
    pointer-events: auto;
    min-width: 280px;
    max-width: var(--snackbar-width);
    background: var(--snackbar-bg);
    border: 1px solid var(--snackbar-border);
    border-radius: var(--snackbar-radius);
    box-shadow: var(--snackbar-shadow);
    padding: var(--snackbar-padding-y) var(--snackbar-padding-x);
    display: flex;
    align-items: flex-start;
    gap: var(--snackbar-gap);
    opacity: 0;
    transform: translateX(110%);
    transition:
      opacity var(--duration-normal) var(--easing-spring),
      transform var(--duration-normal) var(--easing-spring);
  }
  .snackbar.open {
    opacity: 1;
    transform: translateX(0);
  }
  .snackbar-container-center .snackbar {
    transform: translateY(110%);
  }
  .snackbar-container-center .snackbar.open {
    transform: translateY(0);
  }

  .snackbar-success { border-left: var(--snackbar-stripe-width) solid var(--color-success); }
  .snackbar-error   { border-left: var(--snackbar-stripe-width) solid var(--color-error); }
  .snackbar-warning { border-left: var(--snackbar-stripe-width) solid var(--color-warning); }
  .snackbar-info    { border-left: var(--snackbar-stripe-width) solid var(--color-info); }
  .snackbar-accent  { border-left: var(--snackbar-stripe-width) solid var(--color-accent-400); }

  /* Tone tints the icon disc, mirroring .toast-<tone> .toast-icon and
     .alert-<tone> svg. Without these the tone stops at the left stripe and the
     icon circle renders transparent, forcing every consumer to hand-write the
     tint inline (as snackbar.md's example did). Accent uses --color-accent-a10,
     the 10%-alpha accent tint, matching the --color-<tone>-bg-subtle recipe. */
  .snackbar-success .snackbar-icon { background: var(--color-success-bg-subtle); color: var(--color-success); }
  .snackbar-error   .snackbar-icon { background: var(--color-error-bg-subtle);   color: var(--color-error); }
  .snackbar-warning .snackbar-icon { background: var(--color-warning-bg-subtle); color: var(--color-warning); }
  .snackbar-info    .snackbar-icon { background: var(--color-info-bg-subtle);    color: var(--color-info); }
  .snackbar-accent  .snackbar-icon { background: var(--color-accent-a10);        color: var(--color-accent-400); }

  .snackbar-icon {
    flex-shrink: 0;
    width: 24px;
    height: 24px;
    border-radius: var(--radius-full);
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .snackbar-icon svg { width: 14px; height: 14px; }

  .snackbar-content {
    flex: 1;
    min-width: 0;
  }
  .snackbar-title {
    font-size: var(--text-sm);
    font-weight: var(--font-weight-bold);
    color: var(--color-foreground);
  }
  .snackbar-body {
    font-size: var(--text-sm);
    color: var(--color-foreground-subtle);
    margin-top: 2px;
  }

  .snackbar-action {
    background: transparent;
    border: none;
    color: var(--color-accent-300);
    font-size: var(--text-sm);
    font-weight: var(--font-weight-semibold);
    cursor: pointer;
    padding: 0;
    margin-top: var(--spacing-2);
  }
  .snackbar-action:hover { color: var(--color-accent-400); }

  .snackbar-dismiss {
    flex-shrink: 0;
    background: transparent;
    border: none;
    color: var(--color-foreground-subtle);
    cursor: pointer;
    padding: 2px;
    border-radius: var(--radius-sm);
    transition: color var(--duration-fast);
  }
  .snackbar-dismiss:hover { color: var(--color-foreground); }
  .snackbar-dismiss svg { width: 16px; height: 16px; }

  /* ===== TABLE ===== */
  .table-wrap {
    border: 1px solid var(--color-border-subtle);
    border-radius: var(--table-radius);
    overflow: hidden;
    overflow-x: auto;
  }
  .table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
  .table thead { background: var(--table-header-bg); }
  .table th {
    text-align: left;
    padding: var(--table-cell-padding-y) var(--table-cell-padding-x);
    font-size: var(--table-header-font-size);
    font-weight: var(--font-weight-semibold);
    color: var(--color-foreground-disabled);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    border-bottom: 1px solid var(--color-border-subtle);
  }
  .table td {
    padding: var(--table-cell-padding-y) var(--table-cell-padding-x);
  }
  .table tbody tr {
    border-top: 1px solid var(--color-border-subtle);
    transition: background var(--duration-fast);
  }
  .table tbody tr:first-child { border-top: none; }
  .table tbody tr:hover { background: var(--color-accent-a04); }

  /* Frozen first column — opt in on the table, mark the cell to pin.
     NN/g treats locking the left column as inseparable from horizontal scroll,
     not an enhancement: without it, swiping right to read a wide row loses the
     label that says which row you're reading. We shipped the scroll without it.

     No breakpoint on purpose. Pinning is useful on a wide desktop table too, and
     an opt-in class can't surprise anyone — every existing table is untouched
     until it asks. It also avoids a new media width the guard would reject.

     Two things this exists to solve once instead of eight times badly:

     1. The pinned cell needs its OWN opaque background. Cells paint above the
        row and thead backgrounds, so a transparent pinned cell shows the
        scrolling columns straight through itself. The background has to know
        what surface the table sits on, which the framework cannot: hence a
        fallback-only hook, NOT a declared token. Declaring --table-pinned-bg at
        :root would bake in :root's value and beat any override an ancestor set.
     2. .table tbody tr:hover paints its tint on the ROW, which the pinned cell's
        opaque background then covers — the pinned column stays cold while the
        rest of the row lights up. The tint has to be recomposited over the same
        base, which is why the hover rule below repeats the color-mix rather than
        reusing --color-accent-a04 (that's 4% over *transparent*).

     border-collapse: collapse keeps sticky borders intact — verified in Chrome
     against the shipped .table, so no forced switch to `separate`. */
  .table-pin-first .table-cell-pin {
    position: sticky;
    left: 0;
    z-index: 1;
    background: var(--table-pinned-bg, var(--color-bg-base));
    /* Not border-right: under border-collapse the neighbour's border can win the
       conflict resolution and the pinned edge disappears. A shadow always draws. */
    box-shadow: 1px 0 0 var(--color-border-subtle);
  }
  .table-pin-first thead .table-cell-pin {
    z-index: 2;
    background: var(--table-header-bg);
  }
  .table-pin-first tbody tr:hover .table-cell-pin {
    background: color-mix(in srgb, var(--color-accent-400) 4%, var(--table-pinned-bg, var(--color-bg-base)));
  }

  /* Scroll affordance — NN/g: the need to scroll must be *clearly indicated*
     ("don't use dots alone"). Goes ON the wrap; no extra element.

     The hard part isn't drawing an edge, it's knowing whether there IS more to the
     right — CSS can't ask "does this overflow?". The answer is background-attachment,
     which has known it since 2012:

       - the SHADOW layer is `scroll`: pinned to the element's own right edge.
       - the COVER layer is `local`: pinned to the CONTENT's right edge, so it travels
         with the scroll and parks exactly over the shadow once you reach the end.

     Order matters — cover is listed first, so it paints above the shadow.

     That falls out correctly in all three states with no JS and no measurement:
       more to the right  -> content's right edge is off-screen, cover with it, shadow shows
       scrolled to the end -> the two right edges coincide, cover hides the shadow
       nothing to scroll   -> they ALWAYS coincide, so the shadow never appears at all

     That last line is the whole reason this isn't a fade-out animation. A scroll-driven
     fade handles the scroll-end case but not the no-overflow one: a ScrollTimeline whose
     source has no scrollable overflow is INACTIVE, the effect never applies, and a table
     that fits keeps a permanent gradient claiming there's more. The affordance would lie
     in exactly the case it has nothing to say.

     Backgrounds paint behind cell content, so this reads as a shadow at the edge rather
     than a fade of the text — which is the affordance NN/g asks for anyway, and what
     every shipped data table does. --table-pinned-bg does double duty: the cover has to
     match the surface the table sits on, same as the pinned cell.

     The shadow is mixed from --color-foreground, not from black. Black on a near-black
     surface is invisible — the edge has to go LIGHTER on dark and DARKER on light, which
     is exactly what the foreground does for free, in both themes, with no light/ override.

     The mix is written here rather than declared as a :root token on purpose. A custom
     property substitutes its var()s at the scope it is DECLARED, so a :root-level
     color-mix would bake in :root's foreground and keep painting a light scrim inside a
     data-theme="light" island. Declared on the element, it resolves against that
     element's foreground. Same trap that nearly shipped on --heatmap-ramp-color. */
  .table-scroll-hint {
    background:
      linear-gradient(to left,
        var(--table-pinned-bg, var(--color-bg-base)),
        var(--table-pinned-bg, var(--color-bg-base)))
        right center / var(--table-scroll-hint-width, 40px) 100% no-repeat local,
      linear-gradient(to left,
        var(--table-scroll-hint-shadow,
          color-mix(in srgb, var(--color-foreground) 16%, transparent)),
        transparent)
        right center / var(--table-scroll-hint-width, 40px) 100% no-repeat scroll;
  }

  /* Expandable rows — the visible row is a summary; the row under it is the rest of
     the record. Master-detail: right when the job is "read one record", wrong when
     it's "compare rows" (you can't see two expanded records side by side).

     The detail row belongs to the record above it, not to the table as a peer row:
       - no border-top, so it reads as continuous with its summary rather than as
         the next row down.
       - no hover tint. It isn't a row you act on, and .table tbody tr:hover would
         only half-apply anyway — the cell's own background paints over the row's,
         exactly like the pinned cell above.
     Its cell drops the top padding because the summary row already supplied it.

     Unlike card view, this needs no data-label trick: the consumer writes the <dl>,
     so the label is real markup at the call site instead of a duplicate of a <th>
     that CSS is not allowed to read. */
  .table tbody tr.table-row-detail {
    border-top: none;
    background: transparent;
  }
  .table tbody tr.table-row-detail:hover { background: transparent; }
  .table-row-detail > td {
    padding-top: 0;
    background: var(--table-detail-bg, var(--color-bg-elevated-1));
  }
  .table-detail-list {
    margin: 0;
    display: grid;
    grid-template-columns: auto 1fr;
    gap: var(--spacing-2) var(--spacing-4);
    font-size: var(--text-sm);
  }
  .table-detail-list dt {
    font-size: var(--text-xs);
    font-weight: var(--font-weight-semibold);
    color: var(--color-foreground-disabled);
    text-transform: uppercase;
    letter-spacing: 0.05em;
  }
  .table-detail-list dd {
    margin: 0;
    color: var(--color-foreground-secondary);
  }
  /* Chevron rotation, mirroring .sidebar-group-trigger — the state lives on
     aria-expanded, so the affordance and the announcement can never disagree. */
  .table-row-toggle svg { transition: transform var(--duration-fast); }
  .table-row-toggle[aria-expanded="true"] svg { transform: rotate(90deg); }

  /* Card view — every row becomes a stacked card. Read the whole comment before
     reaching for this; it is the only pattern here that is not free.

     Right for: a short, read-one-at-a-time record set.
     Wrong for: comparison (nothing lines up), and anything past a few dozen rows —
     you get one record per screenful and no way to scan. Every record table in the
     consuming apps is already past that, which is why the frozen column above, not
     this, is the answer to nfd.

     TWO things the call site must supply, and neither can be done from CSS:

     1. data-label on every <td>. CSS cannot read a <th>'s text from a <td> — there
        is no selector for "my column's header". React's Table.Cell takes a `label`
        prop and writes this for you; vanilla writes it by hand, and it silently
        drifts the day someone renames a column.

     2. ARIA roles. Changing `display` has historically dropped a table's implicit
        semantics in browsers, which would leave a screen reader announcing
        "Marcus Vane Green 18 71%" as one undifferentiated run with no column names.
        React's `cards` prop stamps role="table"/"rowgroup"/"row"/"cell" for you.
        We restore them unconditionally rather than testing for it: a redundant role
        is a no-op, a missing one is silent and invisible, and the two failure modes
        are not remotely symmetric.

     If you are on plain HTML and that markup burden isn't worth it — render cards.
     `.card` ships already and owes you no attributes. This class exists for the
     one-markup-two-renderings case, and nothing else. */
  .table-cards,
  .table-cards thead,
  .table-cards tbody,
  .table-cards tr,
  .table-cards th,
  .table-cards td { display: block; }
  .table-cards thead { display: none; }
  .table-cards tbody {
    display: flex;
    flex-direction: column;
    gap: var(--spacing-3);
  }
  .table-cards tbody tr {
    border: 1px solid var(--color-border-subtle);
    border-radius: var(--radius-lg);
    background: var(--table-cards-bg, var(--color-bg-elevated-1));
    padding: var(--spacing-2) 0;
  }
  /* .table tbody tr:first-child kills the top border so the header's own bottom
     border can separate the first row. There is no header here — put it back. */
  .table-cards tbody tr:first-child { border-top: 1px solid var(--color-border-subtle); }
  /* Same trap as the pinned cell: .table tbody tr:hover paints --color-accent-a04,
     which is 4% over TRANSPARENT. Left alone it would replace the card's opaque
     surface with a tint and the card would dissolve on hover. Recomposite instead. */
  .table-cards tbody tr:hover {
    background: color-mix(in srgb, var(--color-accent-400) 4%, var(--table-cards-bg, var(--color-bg-elevated-1)));
  }
  .table-cards td {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--spacing-4);
  }
  .table-cards td::before {
    content: attr(data-label);
    flex: 0 0 auto;
    font-size: var(--text-xs);
    font-weight: var(--font-weight-semibold);
    color: var(--color-foreground-disabled);
    text-transform: uppercase;
    letter-spacing: 0.05em;
  }
  /* An unlabelled cell is the card's title, not a broken row — no empty ::before. */
  .table-cards td:not([data-label])::before,
  .table-cards td[data-label=""]::before { display: none; }
  .table-cards .table-cell-title {
    display: block;
    font-size: var(--text-base);
    font-weight: var(--font-weight-semibold);
    color: var(--color-foreground);
    border-bottom: 1px solid var(--color-border-subtle);
    padding-bottom: var(--spacing-3);
  }

  /* ===== MODAL ===== */
  .modal-backdrop {
    position: fixed;
    inset: 0;
    background: var(--modal-backdrop);
    backdrop-filter: blur(var(--modal-blur));
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: var(--z-modal);
  }
  .modal {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    z-index: var(--z-modal-content);
    display: flex;
    flex-direction: column;
    background: var(--modal-bg);
    border: 1px solid var(--modal-border);
    border-radius: var(--modal-radius);
    overflow: hidden;
    box-shadow: var(--shadow-lg);
    max-width: 90vw;
    /* dvh, not vh: vh measures the LARGE viewport — the page as if a mobile
       address bar were retracted — so a 90vh modal is taller than the visible
       area whenever that bar is showing. The overflow lands off-screen above and
       below, and an open modal locks page scroll, so it cannot be reached: the
       header's close button can sit under the browser chrome (#115). dvh tracks
       the live viewport. The vh line stays as a fallback for engines without
       dvh, where the old behavior is still the best available. */
    max-height: 90vh;
    max-height: 90dvh;
  }
  .modal-description {
    flex-shrink: 0;
    padding: 0 var(--spacing-6) var(--spacing-3);
    margin: 0;
    color: var(--color-foreground-subtle);
    font-size: var(--text-sm);
    line-height: var(--font-line-height-loose);
  }
  .modal-sm { width: 360px; }
  .modal-md { width: 500px; }
  .modal-lg { width: 680px; }
  .modal-stripe {
    flex-shrink: 0;
    height: var(--modal-stripe-height);
    background: linear-gradient(90deg, var(--color-accent-500), transparent);
  }
  .modal-header {
    display: flex;
    flex-shrink: 0;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--spacing-2);
    padding: var(--spacing-4) var(--spacing-6);
    border-bottom: 1px solid var(--color-border-subtle);
  }
  .modal-title {
    font-size: var(--text-lg);
    font-weight: var(--font-weight-extrabold);
    color: var(--color-foreground);
    letter-spacing: var(--font-letter-spacing-tight);
    margin: 0;
  }
  /* When DialogDescription is nested inside DialogHeader (the React Dialog
     composition pattern), force it to a new line below the title via
     flex-basis: 100%, and strip its standalone padding since the header
     already provides horizontal padding. Mirrors the .card-header pattern
     documented in specs/components/card.md. */
  .modal-header .modal-description {
    flex-basis: 100%;
    padding: 0;
    margin: 0;
  }
  .modal-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    /* Stop a touch scroll that reaches the end of the body from chaining to the
       page behind the modal (#115). */
    overscroll-behavior: contain;
    padding: var(--spacing-6);
  }
  .modal-footer {
    display: flex;
    flex-shrink: 0;
    justify-content: flex-end;
    gap: var(--spacing-3);
    padding: var(--spacing-4) var(--spacing-6);
    border-top: 1px solid var(--color-border-subtle);
  }

  /* ===== FOCUS MODE =====
     Regional immersive view — fills its parent container (NOT viewport).
     Not a dialog: surrounding app stays visible & interactive. See
     specs/components/focus-mode.md for the full rationale (ARIA APG). */
  .focus-mode-host {
    position: relative;
  }
  .focus-mode {
    position: absolute;
    inset: 0;
    z-index: var(--z-focus-mode);
    display: flex;
    flex-direction: column;
    background: var(--color-bg-base);
    view-transition-name: focus-mode;
  }
  .focus-mode-header,
  .focus-mode-footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--spacing-3);
    padding: var(--spacing-3) var(--spacing-6);
  }
  .focus-mode-header {
    border-bottom: 1px solid var(--color-border-subtle);
  }
  .focus-mode-footer {
    border-top: 1px solid var(--color-border-subtle);
  }
  .focus-mode-title {
    font-size: var(--text-base);
    font-weight: var(--font-weight-semibold);
    color: var(--color-foreground);
    margin: 0;
  }
  .focus-mode-body {
    flex: 1;
    min-height: 0;
    padding: var(--spacing-4) var(--spacing-6);
    overflow-y: auto;
  }
  .focus-mode-footer-left,
  .focus-mode-footer-right {
    display: flex;
    align-items: center;
    gap: var(--spacing-2);
  }
  .focus-mode-exit {
    display: inline-flex;
    align-items: center;
    gap: var(--spacing-2);
    padding: var(--spacing-2) var(--spacing-3);
    border-radius: var(--radius-md);
    font-size: var(--text-base);
    font-weight: var(--font-weight-medium);
    color: var(--color-foreground-muted);
    background: transparent;
    border: 0;
    cursor: pointer;
    transition: color var(--duration-fast), background var(--duration-fast);
  }
  .focus-mode-exit:hover {
    color: var(--color-foreground-secondary);
    background: var(--color-accent-a08);
  }
  .focus-mode-exit:focus-visible {
    outline: 0;
    box-shadow: var(--shadow-accent-focus);
  }
  /* Tinted dividers — mirror .card[data-tint] convention, same 15% mix. */
  .focus-mode-host[data-tint] .focus-mode-header,
  .focus-mode[data-tint] .focus-mode-header {
    border-bottom-color: color-mix(in srgb, var(--color-tint-500) 15%, transparent);
  }
  .focus-mode-host[data-tint] .focus-mode-footer,
  .focus-mode[data-tint] .focus-mode-footer {
    border-top-color: color-mix(in srgb, var(--color-tint-500) 15%, transparent);
  }

  /* ===== TOOLTIP ===== */
  .tooltip {
    background: var(--tooltip-bg);
    border: 1px solid var(--tooltip-border);
    border-top: var(--tooltip-stripe-height) solid var(--color-accent-400);
    border-radius: var(--tooltip-radius);
    padding: 0.5rem 0.875rem;
    font-size: var(--tooltip-font-size);
    color: var(--color-foreground-secondary);
    box-shadow: var(--shadow-md);
    white-space: nowrap;
    z-index: var(--z-floating);
  }

  /* ===== DROPDOWN ===== */
  .dropdown {
    background: var(--dropdown-bg);
    border: 1px solid var(--dropdown-border);
    border-radius: var(--dropdown-radius);
    padding: 6px;
    box-shadow: var(--shadow-md);
    overflow: hidden;
    z-index: var(--z-floating);
  }
  /* Leading-icon row is the default: the documented markup is a flat
     `<svg/> Edit`, so the icon must sit next to its label (the gap supplies the
     spacing). This used to be `space-between`, which is only correct when the
     item has a *second* child to push to the far edge — it pinned the icon left
     and the label right for every item the spec actually documents (#97).
     Trailing affordances (a ⌘E shortcut, a selected-state checkmark) opt in via
     .dropdown-item-trailing below. */
  .dropdown-item {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    gap: var(--spacing-3);
    width: 100%;
    padding: var(--dropdown-item-padding-y) var(--dropdown-item-padding-x);
    border-radius: var(--dropdown-item-radius);
    font-size: var(--text-sm);
    color: var(--color-foreground-secondary);
    background: none;
    border: none;
    cursor: pointer;
    transition: background var(--duration-fast);
    text-align: left;
  }
  .dropdown-item:hover, .dropdown-item-active { background: var(--color-accent-a08); }
  .dropdown-item-active { color: var(--color-foreground); }
  .dropdown-item-danger { color: var(--color-crimson-300); }
  .dropdown-item-danger:hover { background: color-mix(in srgb, var(--color-error) 8%, transparent); }
  .dropdown-item svg { width: 16px; height: 16px; flex-shrink: 0; }
  /* Trailing affordance — a keyboard shortcut (.kbd) or a selected-state
     checkmark, pushed to the item's far edge. Positioning lives here, in the
     parent context, rather than on .kbd itself — the same doctrine
     `.search-pill .kbd` follows, so .kbd stays reusable wherever it appears. */
  .dropdown-item-trailing { margin-left: auto; }
  /* Group label — a section heading inside the menu (#101). DropdownMenu.Label used to
     hand-roll this in Tailwind utilities (px-2 py-1 text-xs uppercase opacity-60), which
     bypassed the token system AND — worse — meant no .dropdown-label class shipped at all,
     leaving vanilla (PHP/Python) consumers no supported way to render a group label. Now a
     real class: uppercase micro-label, muted via a foreground token (not raw opacity), its
     horizontal padding matched to the item's so the label aligns with the rows below it. */
  .dropdown-label {
    padding: var(--spacing-1) var(--dropdown-item-padding-x);
    font-size: var(--text-xs);
    font-weight: var(--font-weight-semibold);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--color-foreground-subtle);
  }
  .dropdown-divider {
    height: 1px;
    background: var(--color-border-subtle);
    margin: 6px var(--spacing-3);
  }

  /* ===== POPOVER =====
     Trigger-anchored floating panel for arbitrary interactive content (forms,
     search + checkbox lists) — the interactive sibling of .dropdown (menu
     items) and .tooltip (non-interactive). Reuses the --dropdown-* family but
     --shadow-lg so it reads as sitting above menus. */
  .popover {
    background: var(--dropdown-bg);
    border: 1px solid var(--dropdown-border);
    border-radius: var(--dropdown-radius);
    padding: var(--spacing-3);
    box-shadow: var(--shadow-lg);
    z-index: var(--z-floating);
  }

  /* ===== SCROLL AREA =====
     Bounded scroll region with a Diannara-styled scrollbar. The browser does
     the scrolling; only the bar's appearance is ours — themed via
     ::-webkit-scrollbar (Chromium, Safari) and scrollbar-width /
     scrollbar-color (Firefox). Same approach as .side-panel-body and
     .list-sidebar-list, at the larger 10px track ScrollArea wants because
     here the bar is meant to be seen and grabbed.

     Because scrolling is native, .scroll-area needs no JS and works in every
     consumption stack — including plain HTML, where the previous Radix-backed
     version could not scroll at all. `max-height` also works, which it did
     not before (see #146).

     Sizing: give the root a definite height, a max-height, or `flex: 1`
     inside a bounded flex column. Any of the three bounds the box, which is
     all a scroll context needs. */
  .scroll-area {
    overflow: auto;
    overscroll-behavior: contain;
    /* Firefox */
    scrollbar-width: thin;
    scrollbar-color: transparent transparent;
  }
  .scroll-area[data-orientation="vertical"] {
    overflow-x: hidden;
    overflow-y: auto;
  }
  .scroll-area[data-orientation="horizontal"] {
    overflow-x: auto;
    overflow-y: hidden;
  }
  .scroll-area[data-orientation="both"] {
    overflow: auto;
  }

  .scroll-area::-webkit-scrollbar {
    width: 10px;
    height: 10px;
  }
  .scroll-area::-webkit-scrollbar-track,
  .scroll-area::-webkit-scrollbar-corner {
    background: transparent;
  }
  /* The transparent border inset by background-clip keeps the visible thumb
     slim inside the wider track, so the grab target stays a comfortable 10px
     while the bar reads as 6px. */
  .scroll-area::-webkit-scrollbar-thumb {
    background: transparent;
    border: 2px solid transparent;
    background-clip: content-box;
    border-radius: var(--radius-full);
    transition: background var(--duration-fast);
  }

  /* Default is quiet — the bar appears on pointer intent, and on focus so
     keyboard scrolling isn't silent. */
  .scroll-area:hover,
  .scroll-area:focus-within {
    scrollbar-color: var(--color-neutral-800) transparent;
  }
  .scroll-area:hover::-webkit-scrollbar-thumb,
  .scroll-area:focus-within::-webkit-scrollbar-thumb {
    background: var(--color-neutral-800);
    background-clip: content-box;
  }
  .scroll-area::-webkit-scrollbar-thumb:hover {
    background: var(--color-neutral-900);
    background-clip: content-box;
  }

  /* Always visible — for modal-internal regions and long config panels where
     the user needs to know there's more. */
  .scroll-area[data-reveal="always"] {
    scrollbar-color: var(--color-neutral-800) transparent;
  }
  .scroll-area[data-reveal="always"]::-webkit-scrollbar-thumb {
    background: var(--color-neutral-800);
    background-clip: content-box;
  }
  .scroll-area[data-reveal="always"]::-webkit-scrollbar-thumb:hover {
    background: var(--color-neutral-900);
    background-clip: content-box;
  }

  /* ===== AVATAR ===== */
  /* Fill and ink both route through custom properties (#173). They used to be
     asymmetric: `background` was reachable from an inline style, `color` was
     hard-wired to --color-on-accent. That asymmetry is not a small thing — it
     invites a consumer to recolour the circle and then gives them no way to
     recolour what sits on it, so the white ink follows a fill it was never
     chosen for. Dojo Tracker shipped a neutral surface gradient under white
     initials that measured 1.00:1 in light theme, on 21 of 51 records.
     These two properties are the MECHANISM, not the consumer-facing answer.
     Reaching --avatar-fg only makes a correct override possible; it still lets
     one get set without the other, which lands in the same place (verified in
     Chrome: --avatar-bg alone, light theme, 1.00:1 — the original bug through
     the new door). So the two classes below own the pairing instead:
     .avatar-neutral for "no data yet", .avatar-color for a fill that IS data. */
  .avatar {
    border-radius: var(--avatar-radius);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-weight: var(--font-weight-bold);
    color: var(--avatar-fg, var(--color-on-accent));
    flex-shrink: 0;
    position: relative;                       /* anchor for AvatarBadge */
    overflow: visible;                        /* let badges escape the circle */
    background: var(--avatar-bg, linear-gradient(135deg, var(--color-accent-500), var(--color-accent-300)));
    vertical-align: middle;
    /* Size flows through --avatar-size rather than each size class setting width
       directly, so a group can size its whole stack at once (.avatar-group-sm)
       while an individual child that declares its own size still wins — a custom
       property set ON an element always beats one inherited from its parent, so
       this needs no specificity juggling and no !important.
       Falls back to md, matching the React component's defaultVariants, so a bare
       <div class="avatar"> in hand-written HTML renders the size React gives you
       instead of collapsing to zero. */
    width: var(--avatar-size, var(--avatar-size-md));
    height: var(--avatar-size, var(--avatar-size-md));
    font-size: var(--avatar-font-size, 15px);
  }
  /* Square avatar (#96). Avatar is the only component in the system that is not a
     rounded rectangle — everything else is 8px or 12px. A tenant's brand mark is a
     rounded square, so it gets one. The circle stays the default, and stays right
     for PEOPLE, which is what circles are for.
     .avatar-image / .avatar-fallback use border-radius: inherit, so the image and
     the initials both clip to these corners automatically. */
  .avatar-square { border-radius: var(--avatar-radius-square); }

  /* Neutral tone (#173) — the "no data yet" avatar: no photo, no tenant colour,
     no rank to derive one from. Both halves come off the theme ramp, so it flips
     with light/dark instead of pinning ink that only works over accent. Measured
     15.43:1 dark, 17.17:1 light.
     Deliberately the same surface as .avatar-group-count: the "+3" overflow bubble
     and a rankless person are the same neutral circle, and only the emphasis of
     what sits on them differs. A consumer reaching for a hand-rolled neutral
     gradient wants this instead — that hand-rolled version is what shipped the
     contrast defect. */
  .avatar-neutral {
    --avatar-bg: var(--color-neutral-300);
    --avatar-fg: var(--color-foreground);
  }

  /* Arbitrary user-colour avatar (#173) — a tenant's brand colour, a belt rank, a
     team colour. DATA from a database, not a tone from the closed vocabulary, which
     is the same distinction .chip-color draws (#111, #112) — and Dojo Tracker's belt
     colour is literally the case that produced it, so this is Avatar catching up to
     a pattern the system already has rather than a new idea.
     The fill is --avatar-color; the ink is DERIVED from that fill's own OKLCH
     lightness with the same step function .chip-color uses: lightness below the
     ~0.62 threshold → white text, above → black, the ×1000 making it a hard switch
     and chroma/hue 0 making it a pure grey. No app-side luminance math, and
     plain-HTML consumers get it too (style="--avatar-color: #7c3aed").
     Deriving it is the point. A consumer cannot get the contrast wrong by omission,
     which is the failure mode #173 actually reported — the app HAD picked a fill and
     simply had no way to say what should sit on it.
     Limitation, shared with .chip-color and measured rather than assumed: a
     black-or-white step is weakest at its own flip point. Worst case over the grey
     ramp is #858585 at 3.69:1, where black would have scored 5.69:1 — the 0.62
     threshold flips slightly late. Retuning it moves three components at once and
     belongs in its own ticket; matching the shipped formula is the right call here,
     because two thresholds would render the same hex differently on a chip and on
     the avatar next to it.
     An explicit --avatar-fg set on the element still wins over this (inline beats a
     class-level declaration), so the escape hatch survives for a fill whose derived
     ink is right but not wanted. */
  .avatar-color {
    --avatar-bg: var(--avatar-color, var(--color-neutral-500));
    --avatar-fg: oklch(from var(--avatar-color, var(--color-neutral-500)) clamp(0, (0.62 - l) * 1000, 1) 0 0);
    /* Hairline ring so the fill has an EDGE against the surface. Without it a
       white or near-white `color` on a light surface loses its shape entirely and
       the initials float unattached — the derived ink is behaving correctly in
       exactly that case (white fill, dark ink), so the step function cannot help.
       This is a different axis from the known mid-grey ink issue (#178).

       Drawn unconditionally whenever `color` is in use, not only for fills close
       to the surface luminance. Conditional would mean an avatar's silhouette
       appearing and disappearing according to user data — belt colour, tenant
       brand — which is the inconsistency pillar 2 exists to prevent. Against a
       dark fill the hairline is simply not perceptible, so it costs nothing.

       inset box-shadow, not a border: the avatar's width/height are the token
       size, and a border would either shrink the content box or push the whole
       circle 2px wider. Layout is untouched here. */
    box-shadow: inset 0 0 0 1px var(--color-border-default);
  }
  .avatar-xs { --avatar-size: var(--avatar-size-xs); --avatar-font-size: 10px; }
  .avatar-sm { --avatar-size: var(--avatar-size-sm); --avatar-font-size: 12px; }
  .avatar-md { --avatar-size: var(--avatar-size-md); --avatar-font-size: 15px; }
  .avatar-lg { --avatar-size: var(--avatar-size-lg); --avatar-font-size: 20px; }
  .avatar-xl { --avatar-size: var(--avatar-size-xl); --avatar-font-size: 26px; }

  /* Inner image + fallback (Radix Avatar sub-elements). The image and
     fallback both fill the avatar circle and clip to its radius. Radix
     mounts only one at a time — image when loaded, fallback otherwise. */
  .avatar-image,
  .avatar-fallback {
    width: 100%;
    height: 100%;
    border-radius: inherit;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .avatar-image { object-fit: cover; }
  .avatar-fallback {
    text-transform: uppercase;
    letter-spacing: 0.5px;
    line-height: 1;
  }

  /* Avatar group — overlapping stack with optional "+N" overflow count.
     Each child gets a ring matching the page surface so circles read as
     separate even when they overlap. Z-index descends left-to-right so
     the leftmost circle is on top, which reads as "first speaker / first
     member" in story scene displays. */
  .avatar-group {
    display: inline-flex;
    align-items: center;
  }
  .avatar-group > * + * { margin-left: calc(-1 * var(--spacing-2)); }
  .avatar-group > * {
    box-shadow: 0 0 0 2px var(--color-bg-base);
  }
  .avatar-group > :nth-child(1) { z-index: 8; }
  .avatar-group > :nth-child(2) { z-index: 7; }
  .avatar-group > :nth-child(3) { z-index: 6; }
  .avatar-group > :nth-child(4) { z-index: 5; }
  .avatar-group > :nth-child(5) { z-index: 4; }
  .avatar-group > :nth-child(6) { z-index: 3; }
  .avatar-group > :nth-child(7) { z-index: 2; }
  .avatar-group > :nth-child(8) { z-index: 1; }

  /* Size the whole stack from the container. These classes shipped as no-ops —
     avatar.md called them "only meaningful as a typed hint" — which meant
     <Avatar.Group size="sm"> silently did nothing and every child had to repeat
     the size itself. Setting --avatar-size here inherits down to each .avatar
     (including .avatar-group-count, which composes .avatar), while any child that
     declares its own size class still overrides it. */
  .avatar-group-xs { --avatar-size: var(--avatar-size-xs); --avatar-font-size: 10px; }
  .avatar-group-sm { --avatar-size: var(--avatar-size-sm); --avatar-font-size: 12px; }
  .avatar-group-md { --avatar-size: var(--avatar-size-md); --avatar-font-size: 15px; }
  .avatar-group-lg { --avatar-size: var(--avatar-size-lg); --avatar-font-size: 20px; }
  .avatar-group-xl { --avatar-size: var(--avatar-size-xl); --avatar-font-size: 26px; }

  /* Overflow count bubble — neutral background, muted text, same circle
     as a regular avatar so the stack reads as one continuous row. */
  .avatar-group-count {
    --avatar-bg: var(--color-neutral-300);
    --avatar-fg: var(--color-foreground-muted);
    font-weight: var(--font-weight-semibold);
  }

  /* Status / notification pip — anchored bottom-right of the parent
     avatar, scales with parent size, ringed against the page surface
     for a "cutout" look. Hidden on xs (too small to render legibly). */
  .avatar-badge {
    position: absolute;
    bottom: 0;
    right: 0;
    box-sizing: content-box;
    border-radius: var(--radius-full);
    background: var(--color-accent-500);
    color: var(--color-on-accent);
    font-size: 9px;
    font-weight: var(--font-weight-bold);
    line-height: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 2px solid var(--color-bg-base);
    min-width: 10px;
    min-height: 10px;
    padding: 0 3px;
  }
  /* Size-matched dot dimensions when used as a status indicator (empty body) */
  .avatar-xs .avatar-badge { display: none; }
  .avatar-sm .avatar-badge { min-width: 8px;  min-height: 8px;  font-size: 8px;  }
  .avatar-md .avatar-badge { min-width: 10px; min-height: 10px; font-size: 9px;  }
  .avatar-lg .avatar-badge { min-width: 12px; min-height: 12px; font-size: 10px; }
  .avatar-xl .avatar-badge { min-width: 14px; min-height: 14px; font-size: 11px; }

  /* Status presets */
  .avatar-badge-online  { background: var(--color-success); }
  .avatar-badge-away    { background: var(--color-warning); }
  .avatar-badge-busy    { background: var(--color-error); }
  .avatar-badge-offline { background: var(--color-foreground-ghost); }

  /* ===== CHIP ===== */
  .chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: var(--chip-padding-y) var(--chip-padding-x);
    border-radius: var(--chip-radius);
    font-size: var(--chip-font-size);
    font-weight: var(--font-weight-medium);
  }
  .chip-accent { background: var(--color-accent-a15); color: var(--color-accent-300); }
  .chip-neutral { background: var(--color-neutral-350); color: var(--color-foreground-muted); }

  /* Arbitrary user-colour chip (#112) — a belt/group hex from the DB, not a tone.
     The fill is `--chip-color`; the FOREGROUND is derived from the fill's own OKLCH
     lightness with relative-colour syntax, so black text lands on pale fills and
     white on dark ones with zero app-side luminance math — and plain-HTML/PHP
     consumers get it too (`style="--chip-color:#f97316"`). The step function:
     lightness L below the ~0.62 threshold → white text (fg L=1), above → black (fg
     L=0); the ×1000 makes it a hard switch, chroma/hue 0 make it a pure grey. */
  .chip-color {
    background: var(--chip-color, var(--color-neutral-500));
    color: oklch(from var(--chip-color, var(--color-neutral-500)) clamp(0, (0.62 - l) * 1000, 1) 0 0);
    /* Same edge-against-the-surface fix as .avatar-color (#178). This path shares
       the ink derivation with Avatar and shared its blind spot: a white or
       near-white user colour on a light surface left the pill with no boundary.
       inset, not a border — .chip declares none, so a real border would add 2px
       to every coloured chip and break alignment in a row of mixed chips. */
    box-shadow: inset 0 0 0 1px var(--color-border-default);
  }
  /* Tint: a low-alpha fill with the full colour as border + text — for lighter-weight
     uses (a filter toggle) where a solid pill would be too loud. */
  .chip-color-tint {
    background: color-mix(in srgb, var(--chip-color, var(--color-neutral-500)) 12%, transparent);
    border: 1px solid var(--chip-color, var(--color-neutral-500));
    color: var(--chip-color, var(--color-neutral-500));
  }
  .chip-close {
    width: var(--chip-close-size);
    height: var(--chip-close-size);
    border-radius: var(--radius-full);
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    border: none;
    background: none;
    color: inherit;
  }
  .chip-close:hover { background: var(--color-accent-a25); }
  .chip-close svg { width: 12px; height: 12px; }

  /* Chip Input (Tag Editor) — bordered container that holds chips
     alongside a plain inline text input. Floating label sits at top-left
     when present. Per specs/components/chips.md. Powers <TagInput>. */
  .chip-input-wrap {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
    background: var(--color-bg-elevated-1);
    border: 1px solid var(--color-border-default);
    border-radius: var(--radius-lg);
    padding: var(--spacing-2) var(--spacing-3);
    min-height: 48px;
    cursor: text;
    transition: border-color var(--duration-fast), box-shadow var(--duration-fast);
  }

  .chip-input-wrap:focus-within {
    border-color: var(--color-accent-400);
    box-shadow: var(--shadow-accent-focus);
  }

  .chip-input-label {
    flex-basis: 100%;
    font-size: var(--text-xs);
    font-weight: var(--font-weight-semibold);
    color: var(--color-accent-300);
    letter-spacing: 0.3px;
    margin-bottom: 2px;
  }

  .chip-inline-input {
    border: none;
    background: transparent;
    outline: none;
    font-size: var(--text-sm);
    color: var(--color-foreground);
    min-width: 80px;
    flex: 1 1 auto;
    padding: 0;
  }

  .chip-inline-input::placeholder {
    color: var(--color-foreground-ghost);
  }

  /* ===== INPUT ===== */
  .input {
    width: 100%;
    background: var(--input-bg);
    border: 1px solid var(--input-border);
    border-radius: var(--input-radius);
    padding: var(--spacing-3) var(--spacing-4);
    font-size: var(--input-font-size);
    color: var(--color-foreground);
    outline: none;
    transition: border-color var(--duration-fast), box-shadow var(--duration-fast);
  }
  .input:focus {
    border-color: var(--color-accent-400);
    box-shadow: 0 0 0 3px var(--color-accent-a15);
  }
  .input::placeholder { color: var(--color-foreground-disabled); }

  /* Floating label variant — use with the .input-float wrapper.
     Markup order is INPUT then LABEL (the label is a following sibling), and the
     input MUST carry placeholder=" " (a single space): the raised state keys off
     :not(:placeholder-shown), which is only false when the field has a value OR a
     non-empty placeholder. A real placeholder would sit under the resting label and
     defeat it, so the space is the whole trick — React's <Field float> supplies it.

     Two states, not one. The old rule shipped ONLY the raised position (label pinned
     up forever), so the label was a static caption and never floated (#123). */
  .input-float { position: relative; }
  /* Room at the top for the raised label to land without overlapping the value. */
  .input-float .input {
    padding-top: var(--spacing-6);
    padding-bottom: var(--spacing-2);
  }
  /* Resting: the label sits over the value like a placeholder — same size as the
     input text, vertically centred, muted. */
  .input-float label {
    position: absolute;
    left: calc(var(--spacing-4) + 1px); /* input border (1px) + padding-left, so it aligns with the value */
    top: 50%;
    transform: translateY(-50%);
    font-size: var(--input-label-size-rest);
    font-weight: var(--font-weight-medium);
    color: var(--color-foreground-disabled);
    pointer-events: none;
    /* Specific props, not `all` — `all` would also transition layout and colour in
       ways that stutter, and the old rule transitioned to nothing anyway. */
    transition: top var(--duration-fast), transform var(--duration-fast),
                font-size var(--duration-fast), color var(--duration-fast);
  }
  /* A textarea is multi-line, so its resting label belongs at the FIRST line, not the
     vertical centre a single-line input uses — centred, it would float in the middle of
     a tall box. :has() tests the control on the wrapper because the label is a following
     sibling and can't look back at the control directly. Raised position is shared. */
  .input-float:has(textarea.input) label {
    top: var(--spacing-6);
    transform: none;
  }
  /* Raised: on focus, or whenever the field has a value. */
  .input-float .input:focus + label,
  .input-float .input:not(:placeholder-shown) + label {
    top: var(--spacing-2);
    transform: translateY(0);
    font-size: var(--input-label-size-float);
  }
  /* Focus tints the raised label accent; a filled-but-blurred field leaves it muted. */
  .input-float .input:focus + label { color: var(--color-accent-400); }
  /* Error mirrors the input's own invalid styling onto the label (spec: label turns
     crimson). Keyed off aria-invalid so the affordance and the a11y state agree. */
  .input-float .input[aria-invalid="true"] + label { color: var(--color-error); }

  /* ===== FIELD =====
     The wrapper React's <Field> renders: label, control, and a message region
     carrying a hint, an error, or both. Lives here rather than as Tailwind
     utilities on the component so the plain-HTML stack gets the same field, and
     so the spacing comes off the scale instead of an arbitrary step. */
  .field { display: flex; flex-direction: column; gap: var(--spacing-2); }
  .field-label {
    font-size: var(--text-sm);
    font-weight: var(--font-weight-medium);
    color: var(--color-foreground);
  }

  /* Two columns: a reserved icon track and a text column. The track is an
     EXPLICIT width, not `auto` — an auto track collapses to zero when the row
     has no icon, which would land a lone hint 8px from the edge instead of
     24px and quietly break the alignment this grid exists to create. */
  .field-messages {
    display: grid;
    grid-template-columns: var(--field-message-icon-size) 1fr;
    column-gap: var(--spacing-2);
    row-gap: var(--spacing-1);
    align-items: start;
  }
  /* Both marks: sized by the container per specs/icons.md, coloured by the row
     they sit in via currentColor. Never sized or coloured at the call site. */
  .field-message-icon {
    grid-column: 1;
    width: var(--field-message-icon-size);
    height: calc(var(--text-xs) * var(--font-line-height-snug));
    display: flex;
    align-items: center;
    flex-shrink: 0;
  }
  .field-message-icon svg {
    width: var(--field-message-icon-size);
    height: var(--field-message-icon-size);
  }
  .field-error {
    grid-column: 2;
    font-size: var(--text-xs);
    line-height: var(--font-line-height-snug);
    font-weight: var(--font-weight-medium);
    color: var(--color-error-text);
  }
  .field-hint {
    grid-column: 2;
    font-size: var(--text-xs);
    line-height: var(--font-line-height-snug);
    color: var(--color-foreground-muted);
  }
  /* Paired with an error, the hint recedes — dimmer, same size. It never shrinks:
     --text-xs is the 14px floor and subordination is carried by colour, by the
     mark it does NOT have, and by sitting second. */
  .field-hint-paired { color: var(--color-foreground-subtle); }
  /* The control's own invalid affordance, so the field reads as wrong before the
     message is read. Keyed off aria-invalid so state and appearance can't diverge. */
  .field .input[aria-invalid="true"],
  .input-float .input[aria-invalid="true"] { border-color: var(--color-error); }
  /* The float wrapper is position:relative, not a flex column — it can't own the
     gap the way .field does, so the message region spaces itself there. */
  .input-float > .field-messages { margin-top: var(--spacing-2); }

  /* ===== APP SHELL =====
     The frame that holds a rail beside the page it navigates. Sidebar always
     shipped as a complete rail and nothing in the system ever placed it, so every
     consumer hand-wrote this same row — and specs/components/navigation.md
     claimed the rail "composes as a full app shell with no hand-applied CSS
     classes", which was not true (#184).

     Position only, matching the doctrine .sidebar-footer already follows: it
     owns arrangement, never the look of what sits inside it. No padding here —
     page inset is genuinely the consumer's call per specs/responsive.md, and
     baking one in would make every app fight it. */
  .app-shell {
    display: flex;
    /* row and nowrap are the flex defaults and are still written out, because they
       ARE the contract rather than incidental styling — and because a guard can
       only assert a property that is present. Checking merely for the absence of
       `flex-wrap: wrap` let `flex-direction: column` through, which puts the page
       BELOW the rail: the same bug from the other axis.

       nowrap is also why Cluster cannot be used here. It wraps by design — correct
       for a row of controls, fatal for a shell, since below ~1024px the content
       column drops below the rail. The rail's own narrow-width behaviour is a
       drawer, a different mechanism entirely. */
    flex-direction: row;
    flex-wrap: nowrap;
    min-height: 100dvh;
  }
  /* The rail keeps its width. Flex items shrink by default, so without this a
     wide main squeezes the rail narrower than --sidebar-width instead of
     scrolling itself. */
  .app-shell > .sidebar { flex-shrink: 0; }
  .app-main {
    flex: 1;
    /* The trap this component exists to own. A flex item's default min-width is
       auto, meaning it refuses to shrink below its content's intrinsic width — so
       ONE wide table anywhere on the page pushes the entire shell sideways and
       the whole layout scrolls horizontally, rail included. With min-width: 0 the
       column shrinks and the table scrolls inside its own box, which is what
       everybody actually wants and almost nobody writes. */
    min-width: 0;
  }

  /* ===== SIDEBAR ===== */
  .sidebar {
    width: var(--sidebar-width);
    background: var(--sidebar-bg);
    border-right: 1px solid var(--sidebar-border);
    overflow-y: auto;
    padding: var(--spacing-3);
    /* Column layout is what lets .sidebar-footer pin to the bottom via
       margin-top:auto. The collapsed, icon-only, and (desktop) drawer states
       were already flex columns — the default rail was the odd one out. */
    display: flex;
    flex-direction: column;
  }
  .sidebar-app-header {
    display: flex;
    align-items: center;
    gap: var(--sidebar-app-header-gap);
    padding-bottom: var(--sidebar-app-header-padding-bottom);
    margin-bottom: var(--sidebar-app-header-margin-bottom);
    border-bottom: 1px solid var(--color-border-subtle);
  }
  .sidebar-app-mark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: var(--sidebar-app-mark-size);
    height: var(--sidebar-app-mark-size);
    border-radius: var(--radius-md);
    color: var(--color-accent-400);
    background: transparent;
    border: 0;
    padding: 0;
    cursor: inherit;
  }
  .sidebar-app-mark svg { width: 100%; height: 100%; }
  .sidebar-app-name,
  .sidebar-app-name-plain {
    font-family: var(--font-family-display);
    font-size: var(--text-xl);
    font-weight: var(--font-weight-extrabold);
    letter-spacing: var(--font-letter-spacing-tight);
    line-height: 1;
  }
  .sidebar-app-name {
    background: linear-gradient(135deg, var(--color-accent-300), var(--color-accent-600));
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    color: transparent;
  }
  .sidebar-app-name-plain { color: var(--color-foreground); }
  .sidebar-collapsed .sidebar-app-name,
  .sidebar-collapsed .sidebar-app-name-plain,
  .sidebar-icon-only .sidebar-app-name,
  .sidebar-icon-only .sidebar-app-name-plain { display: none; }

  /* ===== SIDEBAR IDENTITY (#96) =====
     The TENANT's identity — avatar, name, account type — in a card at the top of
     the rail. Distinct from .sidebar-app-header / -mark / -name (#93), which is the
     APP's fixed wordmark: this one is data, and it differs per user and per
     business. Both may exist; an app picks whichever its design calls for. */
  .sidebar-identity {
    display: flex;
    align-items: center;
    gap: var(--sidebar-identity-gap);
    padding: var(--sidebar-identity-padding-y) var(--sidebar-identity-padding-x);
    margin-bottom: var(--sidebar-identity-margin-bottom);
    background: var(--sidebar-identity-bg);
    border: 1px solid var(--sidebar-identity-border);
    border-radius: var(--sidebar-identity-radius);
    /* Rendered as a link/button via asChild, it must not look like one. */
    color: inherit;
    text-decoration: none;
    width: 100%;
    text-align: left;
  }
  /* min-width: 0 is THE rule that makes the name ellipse. A flex item defaults to
     min-width: auto — it refuses to shrink below its content — so without this the
     name pushes the card wider instead of truncating, and text-overflow never fires
     no matter how it is declared below. */
  .sidebar-identity-text {
    display: flex;
    flex-direction: column;
    min-width: 0;
    flex: 1;
  }
  .sidebar-identity-name {
    font-size: var(--sidebar-identity-name-font-size);
    font-weight: var(--font-weight-semibold);
    color: var(--color-foreground);
    line-height: 1.3;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .sidebar-identity-meta {
    font-size: var(--sidebar-identity-meta-font-size);
    color: var(--color-foreground-subtle);
    line-height: 1.3;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  /* Collapsed / icon-only: the avatar survives, the text does not — the same
     treatment .sidebar-app-name already gets. The card sheds its chrome so a lone
     avatar doesn't sit in a box inside a 64px rail. */
  .sidebar-collapsed .sidebar-identity-text,
  .sidebar-icon-only .sidebar-identity-text { display: none; }
  .sidebar-collapsed .sidebar-identity,
  .sidebar-icon-only .sidebar-identity {
    justify-content: center;
    padding: 0;
    background: none;
    border: 0;
  }
  /* Bottom-pinned region — the mirror of .sidebar-app-header.
     margin-top:auto consumes the rail's free space, so the footer sits on the
     bottom edge when the nav is short and follows the content when it overflows.
     Its border-top doubles as the divider, so no top margin is needed.

     POSITION ONLY. It pins, divides, and refuses to shrink; it has no opinion
     about how its children arrange. It used to: `flex-direction: row` (the flex
     default) plus align-items:center and a gap, authored for the avatar + name +
     role strip named in navigation.md's "User area (bottom)". That was one
     composition's layout written into the container, and it made every other
     composition impossible — a rail with two bottom-pinned ACTIONS and no avatar
     (Settings + Log out) got them side by side, splitting a 231px content box, and
     "Log out" wrapped to a second line at 58px against the 37px of every other row
     in the rail (#171, measured in bka-study-guide at 256px).
     The row direction was also redundant. .sidebar-identity — the class that
     actually holds that strip, and what the guide has always composed in here —
     is its own flex row with its own align-items, gap and width:100%. It never
     needed the footer's direction; it brings its own.
     So: column, matching the rail above it. A footer holding one .sidebar-identity
     renders identically (a single full-width child has no row to sit in), and a
     footer holding nav rows now stacks them flush like the nav rows above, which
     is what they are. No gap, for the same reason .sidebar itself sets none —
     footer rows and nav rows are the same kind of thing and must not read as two
     detached buttons. That retired --sidebar-footer-gap; see MIGRATION.md.

     Anything other than a stack composes a layout primitive inside it: .cluster
     for a row of icons or actions, .stack[data-gap] for deliberate vertical
     spacing. The footer does not need to know, which is the point. */
  .sidebar-footer {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    margin-top: auto;
    padding-top: var(--sidebar-footer-padding-top);
    border-top: 1px solid var(--color-border-subtle);
    /* Never let a long nav squeeze the footer's own height away. */
    flex-shrink: 0;
  }
  /* Collapsed rails have no horizontal padding and centre their children, so the
     footer spans the full width and centres its content — matching how
     .sidebar-app-name disappears and the mark stays. Centring is align-items now,
     not justify-content: in a column those axes swap, and justify-content would
     centre the children vertically in a box that is exactly their height — a
     no-op that silently left narrow rails uncentred. */
  .sidebar-collapsed .sidebar-footer,
  .sidebar-icon-only .sidebar-footer {
    width: 100%;
    align-items: center;
  }
  .sidebar-item {
    display: flex;
    align-items: center;
    gap: var(--spacing-3);
    padding: var(--sidebar-item-padding-y) var(--sidebar-item-padding-x);
    border-radius: var(--sidebar-item-radius);
    font-size: var(--sidebar-item-font-size);
    font-weight: var(--font-weight-medium);
    color: var(--color-foreground-subtle);
    text-decoration: none;
    cursor: pointer;
    position: relative;
    transition: color var(--duration-fast), background var(--duration-fast);
    /* Neutralise the <button> UA defaults, so a nav row that ACTS (a collapsible
       group's trigger, #119) can BE a .sidebar-item instead of restyling one. All five
       are no-ops on the <a> this class was written for.
       `background` belongs here rather than on .sidebar-group-trigger: an override
       there would tie with .sidebar-item-active (both 0,1,0) and kill the active fill
       on source order alone. Declared first, it loses to -active and :hover cleanly.
       `width` is the non-obvious one: a <button> sizes to fit-content even at
       display:flex, so without it a trigger hugs its label instead of filling the rail
       — and a trailing `margin-left: auto` chevron then sits beside the text rather than
       at the edge, which looks deliberate and is not. Safe on <a> (border-box is
       global), and both narrow rails re-set width at (0,2,0). */
    background: transparent;
    border: none;
    font-family: inherit;
    text-align: left;
    width: 100%;
    /* The rail is a flex column, so a row is a flex ITEM — and a flex item's default
       flex-shrink: 1 compresses it before the rail's own overflow-y: auto ever engages.
       A sidebar with more rows than fit therefore squashed them instead of scrolling,
       silently: measured on a 200px rail with six rows, each row rendered 29.3px against
       a natural 43.2px, with scrollHeight === clientHeight === 200 — no scrollbar, because
       there was nothing left to scroll. At tighter ratios it reaches 16px, well under the
       44px touch target accessibility.md requires, and the labels clip with it.
       .sidebar-footer already carried this for the same reason; nothing else in the rail
       needs it, because the app header, group label and identity all have content floors
       their boxes cannot compress below. A row's padding is the only slack in the column.
       Inert on .tree-row, which wears this class inside a <ul> rather than a flex column
       (measured 43.2px either way) — issue 168. */
    flex-shrink: 0;
    /* A nav row carrying user data is one long name from breaking, and it broke two
       ways depending on the label: multi-word labels wrapped and grew the row's height
       (37px to 63.6px at 17px, measured on a tenant name in bka-study-guide — THAT
       app's rail, not this one. Its padding differs, so the figure is not reproducible
       here; on the guide's 231px rail a wrapped row runs 43.2px at one line and 97.6px
       at three, +27.2px per extra line), single-word labels spilled out of the rail
       entirely (#159). `min-width: 0` + `overflow:
       hidden` is what contains BOTH: a long single word is now clipped at the rail
       edge instead of painting over the content beside it.

       That clip shipped with a second bug, found in CodeRabbit round-2 review of
       this same PR and fixed in the same round (see `overflow-wrap: anywhere`
       below): clipping the word did nothing for the row's trailing content. A bare
       single word is still an anonymous flex item with nowhere to shrink, so it
       still shoved `.sidebar-item-count` / `.sidebar-group-chevron` past the right
       edge — the clip just made that trailing content invisible instead of
       overpainted. `overflow-wrap: anywhere` closes it: see that declaration for
       why, and for the before/after measurement.

       `white-space: nowrap` is deliberately NOT here, and must not be added. The
       documented row carries its label as a BARE TEXT NODE, which becomes an
       ANONYMOUS flex item — an anonymous box cannot be selected, so it cannot be given
       `min-width: 0`, so its automatic minimum size stays `auto` (min-content). Under
       nowrap, min-content is the WHOLE STRING: the item refuses to shrink and shoves
       the row's `margin-left: auto` trailing content past the right edge, where this
       same `overflow: hidden` erases it.

       Measured against the guide's own regression fixture (guide/index.html,
       nav[aria-label="Long nav labels as bare text"], first row — label "Northeast
       Regional Operations Center", .sidebar-item-count "24", row content width
       231px), with white-space: nowrap temporarily reinstated here to reproduce this
       reverted draft: the count's right edge moves from 12px INSIDE the row's right
       edge to ~152px PAST it — its own width is unchanged at 31.8px, only its
       position moves — and did not render at all once overflow: hidden clips it, and
       a .sidebar-group-trigger lost its only disclosure affordance the same way.
       Expressed relative to the row's own right edge, not as absolute page
       coordinates, so the claim is reproducible regardless of where the row sits on
       the page in a given session — the plan's Task 5 correction note
       (docs/superpowers/plans/2026-07-31-survey-followups.md) cites this identical
       fixture and identical numbers rather than a second, independently-taken
       measurement.

       So nowrap lives on .sidebar-item-label instead, and the truncation guarantee is
       REQUIRED to be opted into — see that rule. A row whose label is a bare text node
       still wraps, exactly as it did before #159, and keeps its count and chevron. */
    min-width: 0;
    overflow: hidden;
    /* Round 2 fix for the single-word case above: `anywhere` (not `break-word`)
       introduces soft wrap opportunities that ARE counted when the UA computes an
       inline box's min-content intrinsic size — `break-word` only kicks in once
       normal layout already overflows, which never rescues an anonymous flex
       item's automatic minimum. With `anywhere` in place, a bare-text single word
       has a min-content size the row CAN shrink to, so it no longer refuses to
       shrink and no longer shoves the trailing count/chevron past the clip.
       Measured on the guide's own fixture (guide/index.html,
       nav[aria-label="Long single-word nav label as bare text"], label
       "Unteilbarkeitserklaerungsformular", .sidebar-item-count "3", row content
       width 231px, same technique as the multi-word measurement above): the
       count's right edge moved from ~80.6px PAST the row's right edge to 12px
       INSIDE it — matching the "clean" 12px-inside figure the multi-word and
       spanned rows already sit at, both unaffected by this rule (measured, not
       assumed: 918.5/950.4-style offsets on those rows are identical before and
       after). Confirmed inert on .sidebar-item-label (which sets
       white-space: nowrap — nowrap disables line breaking outright, so there is no
       soft-wrap opportunity for `anywhere` to introduce, and the ellipsis path is
       unchanged) and on .tree-row (which already carries `overflow-wrap: anywhere`
       directly on .tree-label, in agreement with this rule, and separately resets
       `overflow: visible` — see TREE below). */
    overflow-wrap: anywhere;
  }
  .sidebar-item:hover { color: var(--color-foreground-muted); background: var(--color-accent-a04); }
  .sidebar-item svg {
    width: var(--sidebar-item-icon-size);
    height: var(--sidebar-item-icon-size);
    flex-shrink: 0;
  }
  /* REQUIRED for one-line truncation, not optional polish. This span is the only thing
     that can carry `white-space: nowrap` safely: it is a real element, so `min-width: 0`
     reaches it and it can shrink below min-content. The row cannot — its label is a bare
     text node, an anonymous flex item that nowrap would freeze at full width, pushing the
     count and chevron out of the clip (see .sidebar-item above). A row without this span
     wraps; a row with it ellipses. `text-overflow` needs it too — an ellipsis applies to a
     block container's own inline content and cannot reach an anonymous box.

     Not rendered for you: Sidebar.Item passes children through and supports asChild, and
     wrapping children would hand Radix Slot a fragment — the failure Chip shipped in #154.
     Opt in by writing the span, the same contract .sidebar-item-count already has. */
  .sidebar-item-label {
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
  }
  .sidebar-item-active {
    color: var(--color-accent-300);
    background: var(--color-accent-a10);
    font-weight: var(--font-weight-semibold);
  }
  /* -count-accent shares the shape rather than restating it. navigation.md has always
     documented it as "same shape as .sidebar-item-count, but accent colours on inactive
     rows too", and its own examples write it standalone — but the rule below only ever
     gave it `background` and `color`, so a standalone one rendered as bare accent text:
     measured against a real pill, display block vs inline-flex, border-radius 0 vs 9999px,
     padding 0 vs 2px 8px, and no margin-left: auto, so it sat mid-row against the label
     instead of at the rail edge. Adding the selector here is what makes the documented
     contract true, for the spec's examples and for every consumer who followed them —
     the alternative was rewriting the promise in five places to say "compose both"
     (issue 168). */
  .sidebar-item-count,
  .sidebar-item-count-accent {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    margin-left: auto;
    padding: var(--sidebar-item-count-padding-y) var(--sidebar-item-count-padding-x);
    border-radius: var(--radius-full);
    background: var(--color-neutral-350);
    color: var(--color-foreground-muted);
    font-size: var(--sidebar-item-count-font-size);
    font-weight: var(--font-weight-semibold);
    line-height: 1;
  }
  .sidebar-item-count-accent,
  .sidebar-item-active .sidebar-item-count {
    background: var(--color-accent-a15);
    color: var(--color-accent-300);
  }
  /* Accent forced unconditionally — gray default reads as dirt at 22px bubble size. */
  .sidebar-collapsed .sidebar-item-count,
  .sidebar-icon-only .sidebar-item-count {
    position: absolute;
    top: 2px;
    right: 2px;
    margin-left: 0;
    min-width: var(--sidebar-item-count-bubble-size);
    height: var(--sidebar-item-count-bubble-size);
    padding: 0 var(--sidebar-item-count-bubble-padding-x);
    background: var(--color-accent-a15);
    color: var(--color-accent-300);
  }
  .sidebar-group-label {
    font-size: var(--sidebar-group-font-size);
    font-weight: var(--font-weight-bold);
    color: var(--color-foreground-subtle);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    padding: var(--spacing-2) var(--sidebar-item-padding-x);
  }

  /* Collapsible nav group (#119) — for nesting that IS the information architecture
     (a group per location/workspace, every child route scoped to it), not decoration.
     Deliberately NOT a new visual language: the trigger is a real .sidebar-item, so it
     inherits every chrome change the rail ever gets, for free.
     Reaching for .accordion-* here is the trap this exists to close — that's a CONTENT
     component, and against a nav row it renders each group as a card: 24px padding
     against the rail's 8/12, --text-base semibold against --text-sm medium, a border
     between every group and a radius wrapping the set, all inside a 256px rail. It
     reads as a FAQ block that wandered into the nav.

     Disclosure state is read from `aria-expanded`, not a modifier class or data-state:
     the a11y attribute and the rotation cannot drift apart, and Radix's Trigger and a
     hand-wired vanilla <button> both set it — so one selector serves both layers.

     Several groups open at once isn't a mode: each group is an independent disclosure,
     so it's simply what happens. Single-select would be the thing you'd have to build. */
  .sidebar-group-trigger .sidebar-group-chevron {
    /* Beats `.sidebar-item svg` (0,1,1) on class count — the leading icon is 20px, and a
       trailing chevron at that size dominates the row it's subordinate to. */
    width: var(--sidebar-group-chevron-size);
    height: var(--sidebar-group-chevron-size);
    margin-left: auto;
    transition: transform var(--duration-fast);
  }
  /* A count already claims the row's free space, so the chevron just trails it. Two
     `margin-left: auto` in one flex row split the space between them and strand the
     count mid-row. */
  .sidebar-item-count + .sidebar-group-chevron { margin-left: 0; }
  .sidebar-group-trigger[aria-expanded="true"] .sidebar-group-chevron {
    transform: rotate(90deg);
  }

  /* The entire nesting design is an indent — child rows are ordinary .sidebar-items.
     Note this is (0,1,0), so the (0,2,0) `padding` shorthands in .sidebar-collapsed /
     .sidebar-icon-only below beat it on specificity regardless of source order: a 44px
     indent can never land in a 64px rail. */
  .sidebar-item-nested { padding-left: var(--sidebar-item-nested-padding-x); }

  /* Sidebar collapsed — icon + label, narrow width */
  .sidebar-collapsed {
    width: var(--sidebar-width-collapsed);
    background: var(--sidebar-bg);
    border-right: 1px solid var(--sidebar-border);
    overflow-y: auto;
    padding: var(--spacing-3) 0;
    display: flex;
    flex-direction: column;
    align-items: center;
  }
  .sidebar-collapsed .sidebar-item {
    flex-direction: column;
    gap: 0.25rem;
    padding: var(--spacing-3) var(--spacing-2);
    /* Stays at --text-xs while the rest of nav moves to --text-base (#161): this is a
       40px content box (56px minus --spacing-2 either side) for a stacked
       icon-over-label cell, and "Settings" at 17px Geist is roughly 65-70px. An
       intentional exception, not an oversight. */
    font-size: var(--text-xs);
    text-align: center;
    border-radius: var(--radius-lg);
    width: 56px;
  }
  .sidebar-collapsed .sidebar-item-label {
    /* 40px content box (56px minus --spacing-2 either side) for a stacked
       icon-over-label cell — the label is MEANT to wrap here, so it opts out of the
       nowrap .sidebar-item-label carries. Same reason this cell keeps --text-xs while
       the rest of nav moves to --text-base (#161). (0,2,0) beats the base rule's
       (0,1,0), so it wins independent of source order.
       The row itself needs no counterpart: .sidebar-item sets no white-space at all, so
       a bare-text-node label in this cell already wraps (#159).

       Behaviour note, round 2 of #169: this rule sets no overflow-wrap of its own, so it
       INHERITS .sidebar-item's overflow-wrap: anywhere (added this round). That is a real
       change here, not a no-op: a single word wider than the 40px column — "Dashboard" at
       --text-xs measures ~69px unbroken — used to overflow the column and get clipped by
       .sidebar-item-label's own overflow: hidden, silently truncating it on one line
       (measured before this change: 69px wide, 22.4px tall, i.e. one line, text cut off
       mid-word with no visual indication). Inheriting `anywhere` lets it break instead:
       measured after, the same label wraps to two lines inside the 40px column (44.8px
       tall, exactly 2x the one-line height) with the full word visible. That is the
       CORRECT outcome for a rule whose own stated intent, above, is that this label is
       MEANT to wrap — before this round it could not actually do that for an unbreakable
       word, only for one with natural break points. Deliberately not overridden back to
       `normal` here.
       Rendered since #168: the guide's Narrow rails demo carries this exact label, and
       the wrap measures 44.78px across two line boxes (39.98px + 29px wide, summing to
       the ~69px unbroken word) against a 45px scrollHeight — unclipped, matching the
       figure above, which until then rested on arithmetic with nothing to load.
       Building that demo is also what surfaced the squash this label used to be a
       casualty of: a rail shorter than its content compressed its rows instead of
       scrolling, and at 300px this label rendered 23.7px with its second line cut off.
       That is fixed at the source — .sidebar-item now sets flex-shrink: 0 — so the
       height of the containing rail no longer decides whether this rule works. */
    white-space: normal;
  }
  .sidebar-collapsed .sidebar-item svg {
    width: var(--sidebar-item-icon-size);
    height: var(--sidebar-item-icon-size);
  }
  .sidebar-collapsed .sidebar-group-label { display: none; }

  /* Sidebar icon-only — just icons, no text */
  .sidebar-icon-only {
    width: var(--sidebar-width-collapsed);
    background: var(--sidebar-bg);
    border-right: 1px solid var(--sidebar-border);
    overflow-y: auto;
    padding: var(--spacing-3) 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--spacing-1);
  }
  .sidebar-icon-only .sidebar-item {
    justify-content: center;
    padding: var(--spacing-3);
    border-radius: var(--radius-lg);
    width: 44px;
    height: 44px;
    font-size: 0;
    gap: 0;
  }
  .sidebar-icon-only .sidebar-item svg {
    width: var(--sidebar-item-icon-size);
    height: var(--sidebar-item-icon-size);
  }
  .sidebar-icon-only .sidebar-group-label { display: none; }

  /* A collapsible group (#119) keeps collapsing in both narrow variants, and the trigger
     stays a row like any other. The alternatives were both worse: hiding the children
     outright is the "region disappears at a breakpoint with nothing to bring it back"
     failure this library keeps re-learning, and force-expanding defeats the point of a
     64px rail. Narrowing already means "you navigate by icon" — a group trigger is just
     another icon, and its children are one click away exactly as the affordance says.
     Only the chevron goes: no room for it once the row is a stacked 56px cell
     (.sidebar-collapsed) or a 44px square (.sidebar-icon-only). It needs an explicit
     rule because `font-size: 0` hides a label but not an SVG. */
  .sidebar-collapsed .sidebar-group-chevron,
  .sidebar-icon-only .sidebar-group-chevron { display: none; }

  /* Sidebar drawer — mobile overlay */
  .sidebar-drawer {
    position: fixed;
    left: 0;
    top: 0;
    bottom: 0;
    z-index: var(--z-drawer);
    width: 260px;
    background: var(--sidebar-bg);
    border-right: 1px solid var(--sidebar-border);
    overflow-y: auto;
    padding: var(--spacing-3);
    transform: translateX(-100%);
    /* visibility:hidden (defect 1, #95) removes the closed drawer from the tab order
       and the a11y tree — translateX alone hides it visually but leaves keyboard/SR
       users tabbing into invisible links. The transition delays the flip to hidden by
       --duration-slow, so the slide-out still plays before it leaves the a11y tree. */
    visibility: hidden;
    transition: transform var(--duration-slow) var(--easing-out),
                visibility 0s var(--duration-slow);
  }
  /* Vanilla (class-toggle) open state — for the plain-HTML / PHP / Python consumers. */
  .sidebar-drawer.open {
    transform: translateX(0);
    visibility: visible;
    transition: transform var(--duration-slow) var(--easing-out), visibility 0s 0s;
  }

  /* React (Radix) path — driven by data-state, not .open. Radix Presence defers
     unmount only while a CSS ANIMATION is running (it reads
     getComputedStyle().animationName); a transition alone unmounts immediately and
     the exit slide is never seen. So the open/close movement is keyframes here. The
     element is mounted only while open or animating out, so it is visible throughout. */
  .sidebar-drawer[data-state="open"],
  .sidebar-drawer[data-state="closed"] {
    visibility: visible;
    transition: none;
  }
  .sidebar-drawer[data-state="open"] {
    transform: translateX(0);
    animation: sidebar-drawer-in var(--duration-slow) var(--easing-out);
  }
  .sidebar-drawer[data-state="closed"] {
    animation: sidebar-drawer-out var(--duration-slow) var(--easing-out);
  }
  @keyframes sidebar-drawer-in {
    from { transform: translateX(-100%); }
    to   { transform: translateX(0); }
  }
  @keyframes sidebar-drawer-out {
    from { transform: translateX(0); }
    to   { transform: translateX(-100%); }
  }

  /* Sidebar backdrop — dims content behind drawer */
  .sidebar-backdrop {
    position: fixed;
    inset: 0;
    z-index: var(--z-backdrop);
    background: var(--sidebar-backdrop);
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--duration-slow) var(--easing-out);
  }
  .sidebar-backdrop.open { opacity: 1; pointer-events: auto; }

  /* React (Radix) path for the backdrop — same reasoning as the drawer: keyframes,
     not a transition, so Presence keeps it mounted through the fade-out. */
  .sidebar-backdrop[data-state="open"] {
    opacity: 1;
    pointer-events: auto;
    animation: sidebar-backdrop-in var(--duration-slow) var(--easing-out);
  }
  .sidebar-backdrop[data-state="closed"] {
    animation: sidebar-backdrop-out var(--duration-slow) var(--easing-out);
  }
  @keyframes sidebar-backdrop-in {
    from { opacity: 0; }
    to   { opacity: 1; }
  }
  @keyframes sidebar-backdrop-out {
    from { opacity: 1; }
    to   { opacity: 0; }
  }

  /* Reduced motion — kill the slide/fade, keep the state change instant. Mirrors the
     stepper and other animated components. */
  @media (prefers-reduced-motion: reduce) {
    .sidebar-drawer[data-state="open"],
    .sidebar-drawer[data-state="closed"],
    .sidebar-backdrop[data-state="open"],
    .sidebar-backdrop[data-state="closed"] {
      animation: none;
    }
  }

  /* Native <dialog> drawer (#99) — the RECOMMENDED vanilla (HTML / PHP / Python) drawer.
     showModal() gives focus containment, Esc-to-close, focus-restore-to-trigger, an inert
     background, and top-layer stacking for free — strictly LESS consumer JS than the
     .open class-toggle above, which stays supported for existing consumers. A closed
     <dialog> is display:none by UA default, so the invisible-but-tabbable defect the
     class-toggle path fixes with visibility:hidden structurally cannot occur here.
     Mobile-only: a modal <dialog> is role=dialog, so it is excluded from the ≥lg
     auto-static rule below — a permanent nav rail announced as "dialog" is wrong for
     every screen-reader user. The backdrop is the ::backdrop pseudo (no second element). */
  dialog.sidebar-drawer {
    /* Override the UA dialog box: a full-height left rail, not a centred auto-margin box.
       The .sidebar-drawer base already supplies bg, border-right, padding, width, and
       overflow; here we only undo the UA centring/clamps and re-home it to the left edge.
       height: 100% is load-bearing: the UA gives a dialog a fit-content height, which
       otherwise wins over inset's top/bottom:0 and shrinks the rail to its content height
       (a stubby floating box). 100% resolves against the viewport (the fixed/top-layer
       containing block), matching the full-height desktop column. */
    inset: 0 auto 0 0;
    height: 100%;
    max-width: none;
    max-height: none;
    margin: 0;
    border: none;
    border-right: 1px solid var(--sidebar-border);
    /* The class-toggle base parks the closed drawer off-screen (translateX(-100%)) and
       hides it via a delayed visibility flip. A <dialog> needs neither: it is display:none
       while closed, so it drops out of layout and the a11y tree on its own. Both are
       neutralised here — the dialog simply appears at the left edge when shown.
       We intentionally do NOT slide the <dialog> in. An @starting-style transform
       transition (and the @keyframes equivalent) can stick at its off-screen START value,
       leaving the rail invisible — an unacceptable failure mode for navigation. The native
       dialog appears instantly (the UA default), full-height, over a dimmed ::backdrop;
       consumers who want a slide use the always-rendered .open class-toggle path above. */
    visibility: visible;
    transform: none;
  }
  dialog.sidebar-drawer::backdrop {
    background: var(--sidebar-backdrop);
  }

  /* Responsive rail (#95) — the desktop half of the split. display:none below lg
     keeps it fully out of the tab order on mobile (where the drawer is the nav); an
     in-flow column at ≥lg. Paired with a Sidebar.Drawer for the mobile overlay. */
  .sidebar-responsive { display: none; }

  /* ===== MOBILE TOP BAR ===== */
  /* A fixed bar is out of flow, so the content beneath it must reserve its height:
     the height is tokenised (not content-driven) so consumers can offset with
     padding-top: var(--mobile-topbar-height). See specs/components/navigation.md. */
  .mobile-topbar {
    position: fixed;
    left: 0;
    right: 0;
    top: 0;
    z-index: var(--z-backdrop);
    height: var(--mobile-topbar-height);
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--spacing-3) var(--spacing-4);
    background: var(--sidebar-bg);
    border-bottom: 1px solid var(--sidebar-border);
  }

  /* ===== HAMBURGER BUTTON ===== */
  .hamburger {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--spacing-2);
    border-radius: var(--radius-md);
    color: var(--color-foreground-subtle);
    background: none;
    border: none;
    cursor: pointer;
    transition: color var(--duration-fast), background var(--duration-fast);
  }
  .hamburger:hover { color: var(--color-foreground-body); background: var(--color-accent-a04); }
  .hamburger svg { width: 20px; height: 20px; }

  /* ===== MOBILE BOTTOM NAV ===== */
  /* Tokenised height for the same reason as .mobile-topbar: a fixed bar is out of flow,
     so content can only reserve space beneath it against a height it can NAME.
     Consumers pair this with padding-bottom: var(--mobile-nav-height). */
  .mobile-nav {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: var(--z-backdrop);
    height: var(--mobile-nav-height);
    display: flex;
    align-items: center;
    justify-content: space-around;
    padding: var(--spacing-2) 0;
    background: var(--sidebar-bg);
    border-top: 1px solid var(--sidebar-border);
  }
  .mobile-nav-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.125rem;
    flex: 1;
    padding: var(--spacing-2) 0;
    min-height: 44px;
    color: var(--color-foreground-subtle);
    text-decoration: none;
    font-size: var(--text-xs);
    font-weight: var(--font-weight-medium);
    transition: color var(--duration-fast);
  }
  .mobile-nav-item:hover { color: var(--color-foreground-muted); }
  .mobile-nav-item-active { color: var(--color-accent-300); }
  .mobile-nav-item svg { width: 20px; height: 20px; }

  /* ===== NAVBAR (desktop header / top nav) ===== */
  .navbar {
    position: sticky;
    top: 0;
    z-index: var(--z-navbar);
    display: flex;
    align-items: center;
    gap: var(--navbar-gap);
    height: var(--navbar-height);
    padding: 0 var(--navbar-padding-x);
    background: var(--navbar-bg);
    border-bottom: 1px solid var(--navbar-border);
  }
  /* Brand slot — reuses the sidebar brand treatment (.sidebar-app-mark / -name(-plain)) */
  .navbar-brand {
    display: flex;
    align-items: center;
    gap: var(--navbar-brand-gap);
    flex-shrink: 0;
    margin-right: var(--spacing-2);
    text-decoration: none;
  }
  .navbar-nav {
    display: flex;
    align-items: center;
    gap: var(--navbar-gap);
  }
  .navbar-item {
    display: inline-flex;
    align-items: center;
    gap: var(--spacing-2);
    padding: var(--navbar-item-padding-y) var(--navbar-item-padding-x);
    border-radius: var(--navbar-item-radius);
    font-size: var(--navbar-item-font-size);
    font-weight: var(--font-weight-medium);
    color: var(--color-foreground-subtle);
    text-decoration: none;
    cursor: pointer;
    white-space: nowrap;
    transition: color var(--duration-fast), background var(--duration-fast);
  }
  .navbar-item:hover { color: var(--color-foreground-muted); background: var(--color-accent-a04); }
  .navbar-item svg { width: 18px; height: 18px; flex-shrink: 0; }
  .navbar-item-active {
    color: var(--color-accent-300);
    background: var(--color-accent-a15);
    font-weight: var(--font-weight-semibold);
  }
  /* Right-aligned slot — user email, sign-out, theme toggle, etc. */
  .navbar-actions {
    display: flex;
    align-items: center;
    gap: var(--spacing-3);
    margin-left: auto;
    flex-shrink: 0;
  }
  /* Hamburger only appears in the navbar below the lg breakpoint (see responsive overrides) */
  .navbar .hamburger { display: none; }

  /* ===== TABS ===== */
  .tab-list {
    display: flex;
    border-bottom: 1px solid var(--color-border-subtle);
  }
  .tab {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: var(--spacing-2);
    padding: var(--spacing-2) var(--spacing-4);
    font-size: var(--tab-font-size);
    font-weight: var(--font-weight-semibold);
    color: var(--color-foreground-ghost);
    background: none;
    border: none;
    cursor: pointer;
    transition: color var(--duration-fast);
  }
  .tab:hover { color: var(--color-foreground-muted); }
  .tab svg { width: 20px; height: 20px; flex-shrink: 0; }
  /* Active state applies via either an explicit `.tab-active` class (plain
     HTML consumers) OR Radix's `data-state="active"` attribute (React Tabs
     wraps @radix-ui/react-tabs which sets this automatically). Both selectors
     emit the same rule so consumers don't need to choose. */
  .tab-active,
  .tab[data-state="active"] {
    color: var(--color-foreground);
  }
  .tab-active::after,
  .tab[data-state="active"]::after {
    content: '';
    position: absolute;
    bottom: -1px;
    left: 6px;
    right: 6px;
    height: var(--tab-indicator-height);
    border-radius: var(--radius-full);
    background: var(--color-accent-400);
    box-shadow: 0 2px 12px var(--color-accent-a25);
  }
  /* Sortable tabs (diannara/react/sortable): each item pairs a drag handle with
     the tab trigger. The handle owns the drag, kept separate from the tab button
     so activation (Enter/Space selects) never collides with keyboard reorder
     (Space picks up). */
  .tab-sortable {
    display: inline-flex;
    align-items: center;
  }
  .tab-handle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: var(--spacing-1);
    color: var(--color-foreground-ghost);
    cursor: grab;
    border-radius: var(--radius-sm);
    opacity: 0.5;
    transition: opacity var(--duration-fast), color var(--duration-fast);
  }
  .tab-sortable:hover .tab-handle,
  .tab-handle:focus-visible {
    opacity: 1;
    color: var(--color-foreground-muted);
  }
  .tab-handle:focus-visible {
    outline: none;
    box-shadow: var(--shadow-accent-focus);
  }
  .tab-handle:active,
  .tab-sortable[data-dragging="true"] .tab-handle {
    cursor: grabbing;
  }

  /* ===== SORTABLE LIST (diannara/react/sortable) ===== */
  /* Vertical (or horizontal) list of rich rows with drag-to-reorder. Same
     handle contract as .tab-handle: a dedicated grip owns the drag, kept
     separate from the row's own interactive content so clicking a button or
     editing a field in the row never collides with keyboard reorder (Space
     picks up). The drag behavior itself ships in the React SortableList;
     these classes carry the layout + affordance styling. */
  .sortable-list {
    display: flex;
    flex-direction: column;
    gap: var(--spacing-2);
    margin: 0;
    padding: 0;
    /* React pairs this with an explicit role="list" — Safari/VoiceOver
       drops the implicit list role when list-style is none. */
    list-style: none;
  }
  .sortable-list[data-orientation="horizontal"] {
    flex-direction: row;
    align-items: center;
  }
  .sortable-list-item {
    display: flex;
    align-items: center;
    gap: var(--spacing-2);
  }
  .sortable-list-handle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    padding: var(--spacing-1);
    color: var(--color-foreground-ghost);
    cursor: grab;
    border-radius: var(--radius-sm);
    opacity: 0.5;
    transition: opacity var(--duration-fast), color var(--duration-fast);
  }
  .sortable-list-item:hover .sortable-list-handle,
  .sortable-list-handle:focus-visible {
    opacity: 1;
    color: var(--color-foreground-muted);
  }
  .sortable-list-handle:focus-visible {
    outline: none;
    box-shadow: var(--shadow-accent-focus);
  }
  .sortable-list-handle:active,
  .sortable-list-item[data-dragging="true"] .sortable-list-handle {
    cursor: grabbing;
  }
  .sortable-list-item[data-disabled="true"] .sortable-list-handle {
    opacity: 0.25;
    cursor: not-allowed;
  }

  /* Full-surface drag (#129) — the whole row is the pointer activator, so the
     row itself carries the grab cursor. The grip keeps its own cursor rules
     above; it remains the keyboard activator in this mode. */
  .sortable-list-item[data-drag-surface="full"]:not([data-disabled="true"]),
  .sortable-board-item[data-drag-surface="full"]:not([data-disabled="true"]) {
    cursor: grab;
  }
  .sortable-list-item[data-drag-surface="full"][data-dragging="true"],
  .sortable-board-item[data-drag-surface="full"][data-dragging="true"] {
    cursor: grabbing;
  }

  /* ===== SORTABLE BOARD ===== */

  /* Cross-container drag surface (#129). Ships arrangement and the drop
     affordance only — column width, padding, and card styling stay with the
     consumer, because a board's columns are app layout, not component layout. */
  .sortable-board {
    display: flex;
    align-items: flex-start;
    gap: var(--spacing-4);
  }
  .sortable-board-container {
    display: flex;
    flex-direction: column;
    gap: var(--spacing-2);
    border-radius: var(--radius-lg);
    transition: background var(--duration-fast) var(--easing-out);
  }
  /* The drop affordance. data-over comes from the container's own useDroppable,
     which is also what keeps an EMPTY column a valid target — with no child
     items there is nothing else for a collision test to hit. */
  .sortable-board-container[data-over="true"] {
    background: var(--color-accent-a08);
  }
  .sortable-board-item {
    position: relative;
  }
  /* The grip sits over the card rather than beside it: in full-surface mode it
     is an affordance hint and the keyboard entry point, not the primary target,
     so it must not take a column of the card's width. */
  .sortable-board-handle {
    position: absolute;
    top: var(--spacing-2);
    right: var(--spacing-2);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: var(--spacing-1);
    color: var(--color-foreground-ghost);
    cursor: grab;
    border-radius: var(--radius-sm);
    opacity: 0;
    transition: opacity var(--duration-fast), color var(--duration-fast);
  }
  .sortable-board-item:hover .sortable-board-handle,
  .sortable-board-handle:focus-visible {
    opacity: 1;
    color: var(--color-foreground-muted);
  }
  .sortable-board-handle:focus-visible {
    outline: none;
    box-shadow: var(--shadow-accent-focus);
  }
  .sortable-board-item[data-disabled="true"] .sortable-board-handle {
    cursor: not-allowed;
  }

  /* ===== PROGRESS ===== */
  .progress {
    height: var(--progress-height);
    background: var(--progress-track-bg);
    border-radius: var(--progress-radius);
    overflow: hidden;
  }
  .progress-bar {
    height: 100%;
    border-radius: var(--progress-radius);
    background: linear-gradient(90deg, var(--color-accent-500), var(--color-accent-300));
    transition: width var(--duration-slow) var(--easing-out);
  }
  .progress-bar-success { background: linear-gradient(90deg, var(--color-success), var(--color-emerald-300)); }
  .progress-bar-error { background: linear-gradient(90deg, var(--color-error), var(--color-crimson-300)); }

  /* Circular / radial progress. The track + indicator <circle>s are drawn by
     the ProgressRing React component (stroke-dasharray/offset); these rules
     handle layout, the rotation that starts the arc at 12 o'clock, the tone
     colors, and the centered label. */
  .progress-ring { display: inline-flex; position: relative; }
  .progress-ring__svg { transform: rotate(-90deg); }
  .progress-ring__track { stroke: var(--progress-track-bg); fill: none; }
  .progress-ring__indicator {
    stroke: var(--color-accent-500);
    fill: none;
    stroke-linecap: round;
    transition: stroke-dashoffset var(--duration-slow) var(--easing-out);
  }
  .progress-ring__indicator-success { stroke: var(--color-success); }
  .progress-ring__indicator-error { stroke: var(--color-error); }
  .progress-ring__label {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: var(--text-sm);
    font-weight: var(--font-weight-semibold);
    color: var(--color-foreground);
  }

  /* ===== METER (read-only N-of-M segments) ===== */
  /* A static "N of M filled" scale — dots by default, bars via .meter-bars.
     Empty segments use the neutral track; filled segments take the accent (or
     a status tone). Purely presentational; the React component wires ARIA. */
  .meter { display: inline-flex; align-items: center; gap: var(--spacing-1); }
  .meter__segment {
    width: 8px;
    height: 8px;
    border-radius: var(--radius-full);
    background: var(--color-neutral-300);
    transition: background var(--duration-fast);
  }
  .meter__segment-filled { background: var(--color-accent-500); }
  .meter__segment-success { background: var(--color-success); }
  .meter__segment-warning { background: var(--color-warning); }
  .meter__segment-error { background: var(--color-error); }
  .meter-bars .meter__segment { width: 20px; height: 6px; border-radius: var(--radius-sm); }

  /* ===== DISTRIBUTION (proportional split of a total) ===== */
  /* A thin stacked bar whose segments are sized by their SHARE of the total —
     "how a whole splits across categories, in one glance". Deliberately not
     .meter, which fills N of M *equal* segments (a coarse rating); and not
     .progress, which is one value against one maximum.

     Segments carry no width: each sets flex-grow to its raw value over a
     flex-basis of 0, so the browser does the ratio arithmetic and the markup
     never has to compute percentages that drift when one value changes.

     Purely presentational — the React component owns role="img" and the
     generated text description; the segments themselves are aria-hidden. */
  .distribution {
    display: flex;
    align-items: stretch;
    width: 100%;
    height: var(--distribution-height);
    border-radius: var(--distribution-radius);
    overflow: hidden;
    background: var(--distribution-track-bg);
  }
  .distribution__segment {
    flex-basis: 0;
    /* Keeps a 1-in-500 share from vanishing. Segments deliberately carry no
       border-radius: the track already clips to a pill, so a per-segment radius
       would only notch the junctions between neighbours. */
    min-width: var(--distribution-segment-min-width);
    transition: flex-grow var(--duration-slow) var(--easing-out);
  }
  /* Tone fills. Every one of these must stay distinguishable from
     --distribution-track-bg, or the segment reads as a hole in the bar — the
     neutral ramp is theme-relative (higher step = further from the surface in
     BOTH themes), so picking by step keeps light and dark correct together. */
  .distribution__segment-neutral { background: var(--color-neutral-600); }
  .distribution__segment-muted   { background: var(--color-neutral-800); }
  .distribution__segment-accent  { background: var(--color-accent-500); }
  .distribution__segment-info    { background: var(--color-info); }
  .distribution__segment-success { background: var(--color-success); }
  .distribution__segment-warning { background: var(--color-warning); }
  .distribution__segment-error   { background: var(--color-error); }

  /* ===== FAB ===== */
  .fab {
    width: var(--fab-size);
    height: var(--fab-size);
    border-radius: var(--fab-radius);
    background: var(--color-accent-500);
    color: var(--color-on-accent);
    border: none;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: var(--shadow-md);
    transition: all var(--duration-normal) var(--easing-spring);
  }
  .fab:hover { background: var(--color-accent-400); transform: scale(1.02); }
  .fab:active { transform: scale(0.98); }
  .fab svg { width: var(--fab-icon-size); height: var(--fab-icon-size); }
  .fab-mini { width: var(--fab-size-mini); height: var(--fab-size-mini); }
  .fab-mini svg { width: 20px; height: 20px; }
  .fab-extended {
    width: auto;
    padding: 0 var(--spacing-6);
    gap: var(--spacing-2);
    font-size: var(--text-sm);
    font-weight: var(--font-weight-semibold);
  }
  .fab-extended svg { width: 20px; height: 20px; }

  /* ===== PAGINATION ===== */
  .pagination {
    display: flex;
    align-items: center;
    gap: 4px;
  }
  .page-btn {
    width: var(--pagination-button-size);
    height: var(--pagination-button-size);
    border-radius: var(--pagination-radius);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: var(--text-sm);
    font-weight: var(--font-weight-medium);
    color: var(--color-foreground-subtle);
    background: none;
    border: none;
    cursor: pointer;
    transition: background var(--duration-fast), color var(--duration-fast);
  }
  .page-btn:hover { background: var(--color-accent-a04); color: var(--color-foreground-muted); }
  .page-btn:focus-visible {
    outline: none;
    box-shadow: var(--shadow-accent-focus);
  }
  .page-btn:disabled,
  .page-btn[aria-disabled="true"] {
    opacity: 0.4;
    cursor: not-allowed;
    pointer-events: none;
  }
  .page-btn svg { width: 16px; height: 16px; }
  .page-btn-active {
    background: var(--color-accent-500);
    color: var(--color-on-accent);
  }
  .page-btn-arrow { color: var(--color-foreground-ghost); }
  .page-btn-arrow:hover { color: var(--color-foreground); }
  .page-btn-ellipsis {
    color: var(--color-foreground-ghost);
    cursor: default;
    pointer-events: none;
    background: none;
  }

  /* ===== LOAD MORE ===== */
  .load-more {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--spacing-3);
  }
  .load-more-count {
    font-size: var(--text-sm);
    color: var(--color-foreground-subtle);
  }
  .load-more-btn {
    min-width: 140px;
  }

  /* ===== SPINNER ===== */
  .spinner {
    border-radius: var(--radius-full);
    border: 3px solid var(--color-neutral-400);
    border-top-color: var(--color-accent-400);
    animation: spin 0.8s linear infinite;
  }
  .spinner-sm { width: var(--spinner-size-sm); height: var(--spinner-size-sm); border-width: 2px; }
  .spinner-md { width: var(--spinner-size-md); height: var(--spinner-size-md); }
  .spinner-lg { width: var(--spinner-size-lg); height: var(--spinner-size-lg); border-width: 4px; }

  @keyframes spin { to { transform: rotate(360deg); } }

  /* A spinner loops forever by definition, so it is the one animation guaranteed to
     still be moving whenever a vestibular-sensitive user is looking at it. The
     override was documented in loading.md from the start and simply never shipped —
     invisible because the guide hand-rolled its spinners from `animate-spin`, so
     .spinner was rendered nowhere and nobody met the rule that was missing.

     Frozen rather than slowed, matching every other reduced-motion fallback in this
     file (.skeleton, .status-dot-*, .streaming-*). The arc stays visible as a static
     "in progress" glyph, and the meaning is carried by role="status" + its label
     rather than by the motion. */
  @media (prefers-reduced-motion: reduce) {
    .spinner { animation: none; }
  }

  /* ===== SKELETON =====
     The React <Skeleton> has always emitted these classes, but no CSS backed them,
     so <Skeleton /> rendered a zero-height invisible div and loading.md told every
     consumer to paste a "reference implementation" into their own app. A component
     whose entire job is to BE the grey shimmering bar cannot ship without the bar.
     Same class of gap as .rte-body's missing block flow and the accordion chevron
     that never rotated: shipped, documented, and non-functional. */
  .skeleton {
    background: linear-gradient(90deg,
      var(--color-bg-elevated-1) 25%,
      var(--color-bg-elevated-2) 50%,
      var(--color-bg-elevated-1) 75%);
    background-size: 200% 100%;
    animation: shimmer 1.8s ease infinite;
    border-radius: 4px;
  }
  /* Shape presets. `shape="none"` (the React default) intentionally sets no
     dimensions — it takes the size of whatever box the consumer puts it in. */
  .skeleton--text   { height: 14px; margin-bottom: 10px; }
  .skeleton--avatar { width: 40px; height: 40px; border-radius: var(--radius-full); }
  .skeleton--card   { height: 160px; border-radius: var(--radius-lg); }

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

  /* The shimmer is a large moving gradient — exactly the sustained motion
     vestibular triggers react to. Falls back to the flat mid-tone. */
  @media (prefers-reduced-motion: reduce) {
    .skeleton {
      animation: none;
      background: var(--color-bg-elevated-2);
    }
  }

  /* ===== ACCORDION ===== */
  .accordion { border-radius: var(--card-radius); overflow: hidden; }
  .accordion-item { border-bottom: 1px solid var(--card-border); }
  .accordion-item:last-child { border-bottom: none; }
  .accordion-trigger {
    display: flex;
    align-items: center;
    gap: var(--spacing-4);
    width: 100%;
    padding: var(--spacing-6);
    font-size: var(--text-base);
    font-weight: var(--font-weight-semibold);
    color: var(--color-foreground);
    background: none;
    border: none;
    cursor: pointer;
    text-align: left;
    transition: background var(--duration-fast);
  }
  .accordion-trigger:hover { background: var(--color-accent-a04); }
  /* accordion.md's overview promises "a chevron that rotates on expand" and no rule
     shipped it, so every consumer's chevron sat still. Keyed off the trigger's own
     aria-expanded rather than the item's data-state: plain-HTML consumers set it by
     hand and Radix sets it too, so one selector covers both layers — the same
     both-worlds approach .tab-active / .tab[data-state="active"] takes. */
  .accordion-trigger svg {
    flex-shrink: 0;
    transition: transform var(--duration-fast) var(--easing-out);
  }
  .accordion-trigger[aria-expanded="true"] svg { transform: rotate(180deg); }
  .accordion-body {
    padding: 0 var(--spacing-6) var(--spacing-6);
    font-size: var(--text-sm);
    color: var(--color-foreground-body);
    line-height: var(--font-line-height-loose);
  }

  /* ===== DIVIDER =====
     `border: 0` resets Tailwind v4 preflight (`hr { border-top-width: 1px }`
     in inherited text color) so the divider line comes ONLY from our
     own rule — keeps the line color from being doubled-up by preflight's
     default border. */
  .divider {
    border: 0;
    height: 1px;
    background: var(--color-border-subtle);
  }
  /* Accent variant — solid 2px accent line. Per specs/components/dividers.md
     "Accent" entry (line 67) and the HTML guide's accent-divider pattern.
     Implemented as background (not border-top) so it composes cleanly with
     .divider's `border: 0` reset and stays at the documented 2px thickness
     regardless of preflight changes. */
  .divider-accent {
    height: 2px;
    background: var(--color-accent-400);
  }
  /* Vertical orientation — a rule between inline/toolbar controls. Stretches to
     the flex parent's cross axis (align-self), falling back to a min height when
     the parent isn't a flex row. Horizontal margin replaces the block margin the
     horizontal rule would carry. */
  .divider-vertical {
    width: 1px;
    height: auto;
    min-height: 16px;
    align-self: stretch;
    margin: 0 12px;
  }
  .divider-vertical.divider-accent {
    width: 2px;
    height: auto;
  }

  /* ===== EMPTY STATE ===== */
  .empty-state {
    border: 1px dashed var(--color-border-default);
    border-radius: var(--card-radius);
    padding: var(--spacing-12);
    text-align: center;
  }
  .empty-state-icon {
    width: 48px;
    height: 48px;
    margin: 0 auto var(--spacing-4);
    color: var(--color-foreground-ghost);
  }
  .empty-state-title {
    font-size: var(--text-base);
    font-weight: var(--font-weight-bold);
    color: var(--color-foreground-secondary);
    margin-bottom: var(--spacing-2);
  }
  .empty-state-body {
    font-size: var(--text-sm);
    color: var(--color-foreground-body);
    max-width: 320px;
    margin: 0 auto var(--spacing-6);
  }

  /* ===== TIMELINE ===== */
  .timeline {
    position: relative;
    padding-left: 32px;
  }

  .timeline__line {
    position: absolute;
    left: 11px;
    top: 8px;
    bottom: 8px;
    width: 2px;
    background: var(--color-border-default);
  }

  .timeline__item {
    position: relative;
    padding-bottom: var(--spacing-8);
  }

  /* The untoned icon must render as something. It used to set no background at
     all while stroking its glyph in --color-on-accent, i.e. a white mark on the
     page — the default state of the component was an invisible one (#194).
     --color-neutral-600 is the same neutral .timeline__dot already defaults to,
     fifteen rules below, so the precedent is inside this component. */
  .timeline__icon {
    position: absolute;
    left: -32px;
    top: 2px;
    width: 24px;
    height: 24px;
    border-radius: var(--radius-full);
    background: var(--color-neutral-600);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 1;
  }

  /* --color-foreground, not --color-on-accent: the neutral fill is mid-grey in
     both themes, and foreground is the one that inverts to stay legible on it
     (9.9:1 dark, 8.6:1 light). --color-on-accent is white in both, which reads
     2.2:1 on the light-theme neutral. */
  .timeline__icon svg {
    width: 14px;
    height: 14px;
    stroke: var(--color-foreground);
    fill: none;
    stroke-width: 2.5;
    stroke-linecap: round;
    stroke-linejoin: round;
  }

  .timeline__icon--neutral { background: var(--color-neutral-600); }
  .timeline__icon--accent  { background: var(--color-accent-500); }
  .timeline__icon--success { background: var(--color-success); }
  .timeline__icon--warning { background: var(--color-warning); }
  .timeline__icon--info    { background: var(--color-info); }
  .timeline__icon--error   { background: var(--color-error); }

  /* The glyph colour belongs to the fill, not to the component. Every toned icon
     used to stroke white, but dark's status fills are the bright 400-level stops
     — white measured 1.9:1 on success, 1.7:1 on warning, 2.5:1 on info, all below
     the 3:1 non-text minimum. No single token fixes it: a theme-inverting one
     rescues success and info but drops accent to 2.6:1 on its deep purple. So it
     is assigned per tone. Worst case across all ten tone/theme pairs is 4.6:1. */
  .timeline__icon--accent svg  { stroke: var(--color-on-accent); }
  .timeline__icon--error svg   { stroke: var(--color-on-accent); }
  .timeline__icon--warning svg { stroke: var(--color-on-warning); }
  .timeline__icon--success svg { stroke: var(--color-on-success); }
  .timeline__icon--info svg    { stroke: var(--color-on-info); }

  .timeline__title { color: var(--color-foreground); font-size: var(--text-sm); font-weight: var(--font-weight-bold); }
  .timeline__time  { color: var(--color-foreground-subtle); font-size: var(--text-xs); margin-bottom: 8px; }

  .timeline__card {
    background: var(--color-bg-elevated-2);
    border: 1px solid var(--color-border-default);
    border-radius: var(--radius-md);
    padding: 12px;
    color: var(--color-foreground-body);
    font-size: var(--text-sm);
  }

  /* Tinted card, so a highlighted entry can carry its tone past the icon (#193).
     Percentages and token choices mirror .card-tinted-* exactly — 8% fill, 15%
     border, accent via the pre-mixed --color-accent-a08/a15 — so the two tinted
     surfaces in the system agree instead of each inventing a strength. Untoned
     is untouched, so this is purely additive. */
  .timeline__card--accent  { background: var(--color-accent-a08); border-color: var(--color-accent-a15); }
  .timeline__card--success { background: color-mix(in srgb, var(--color-success) 8%, transparent); border-color: color-mix(in srgb, var(--color-success) 15%, transparent); }
  .timeline__card--warning { background: color-mix(in srgb, var(--color-warning) 8%, transparent); border-color: color-mix(in srgb, var(--color-warning) 15%, transparent); }
  .timeline__card--info    { background: color-mix(in srgb, var(--color-info) 8%, transparent);    border-color: color-mix(in srgb, var(--color-info) 15%, transparent); }
  .timeline__card--error   { background: color-mix(in srgb, var(--color-error) 8%, transparent);   border-color: color-mix(in srgb, var(--color-error) 15%, transparent); }

  /* Compact variant */
  .timeline--compact { padding-left: 24px; }
  .timeline--compact .timeline__line { left: 7px; }
  .timeline--compact .timeline__item { padding-bottom: var(--spacing-6); }

  .timeline__dot {
    position: absolute;
    left: -24px;
    top: 6px;
    width: 16px;
    height: 16px;
    border-radius: var(--radius-full);
    background: var(--color-neutral-600);
    border: 3px solid var(--color-bg-elevated-1);
    z-index: 1;
  }

  .timeline__dot--accent { background: var(--color-accent-500); }

  /* ===== CODE BLOCK ===== */
  .code-block {
    position: relative;
    background: var(--color-bg-elevated-2);
    border: 1px solid var(--color-border-default);
    border-radius: var(--radius-lg);
    overflow: hidden;
  }

  .code-block-header {
    background: var(--color-bg-elevated-3);
    border-bottom: 1px solid var(--color-border-subtle);
    padding: 10px 16px;
    font-family: var(--font-family-mono);
    font-size: var(--text-xs);
    color: var(--color-foreground-subtle);
  }

  .code-block pre {
    margin: 0;
    padding: 20px 20px 20px 0;
    overflow-x: auto;
    font-family: var(--font-family-mono);
    font-size: var(--text-sm);
    line-height: 1.5;
    color: var(--color-foreground);
  }

  .line-number {
    display: inline-block;
    min-width: 32px;
    padding-right: 16px;
    text-align: right;
    color: var(--color-foreground-ghost);
    user-select: none;
  }

  /* Syntax token colors */
  .token-keyword { color: var(--color-accent-300); }
  .token-string  { color: var(--color-emerald-300); }
  .token-punct   { color: var(--color-foreground-ghost); }
  .token-subtle  { color: var(--color-foreground-subtle); }
  .token-comment { color: var(--color-foreground-subtle); font-style: italic; }

  .code-block-copy {
    position: absolute;
    top: 10px;
    right: 12px;
    width: 32px;
    height: 32px;
    background: var(--color-bg-elevated-3);
    border: 1px solid var(--color-border-default);
    border-radius: var(--radius-md);
    color: var(--color-foreground-subtle);
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transition: background var(--duration-fast), color var(--duration-fast);
  }

  .code-block-copy:hover { background: var(--color-neutral-400); color: var(--color-foreground); }
  .code-block-copy.copied { color: var(--color-success); }

  .code-block-copy svg {
    width: 16px;
    height: 16px;
    stroke: currentColor;
    fill: none;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
  }

  /* When header is present, shift copy button down to avoid the header */
  .code-block.has-header .code-block-copy {
    top: 48px;
  }

  /* ===== BREADCRUMBS ===== */
  .breadcrumb {
    display: inline-flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0;
  }

  .breadcrumb-link {
    font-size: var(--text-sm);
    font-weight: var(--font-weight-medium);
    color: var(--color-foreground-subtle);
    text-decoration: none;
    transition: color var(--duration-fast);
  }

  .breadcrumb-link:hover {
    color: var(--color-foreground-body);
    text-decoration: underline;
  }

  .breadcrumb-link:focus-visible {
    outline: 2px solid var(--color-accent-400);
    outline-offset: 2px;
    border-radius: 2px;
  }

  .breadcrumb-link--icon {
    display: inline-flex;
    align-items: center;
    gap: 4px;
  }

  .breadcrumb-link--icon svg {
    width: 16px;
    height: 16px;
    stroke: currentColor;
    fill: none;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
  }

  .breadcrumb-sep {
    margin: 0 8px;
    color: var(--color-foreground-ghost);
    font-size: var(--text-sm);
    user-select: none;
  }

  .breadcrumb-current {
    font-size: var(--text-sm);
    font-weight: var(--font-weight-medium);
    color: var(--color-accent-300);
    cursor: default;
  }

  /* ===== STEPPER ===== */
  .stepper {
    display: flex;
    align-items: flex-start;
    gap: 0;
  }

  .stepper-step {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
  }

  .stepper-step:not(:last-child) {
    position: relative;
  }

  .stepper-step:not(:last-child)::after {
    content: '';
    position: absolute;
    top: 15px;
    left: calc(50% + 16px);
    right: calc(-50% + 16px);
    height: 2px;
    background: var(--color-border-strong);
  }

  .stepper-step.completed::after,
  .stepper-step.current::after {
    background: var(--color-accent-500);
  }

  .stepper-circle {
    width: 32px;
    height: 32px;
    border-radius: var(--radius-full);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: var(--text-sm);
    font-weight: var(--font-weight-semibold);
    border: 2px solid;
    position: relative;
    z-index: 1;
    background: var(--color-bg-surface);
  }

  .stepper-step.completed .stepper-circle {
    background: var(--color-accent-500);
    border-color: var(--color-accent-500);
    color: var(--color-on-accent);
  }

  .stepper-step.completed .stepper-circle svg {
    width: 14px;
    height: 14px;
    stroke: var(--color-on-accent);
    fill: none;
    stroke-width: 2.5;
    stroke-linecap: round;
    stroke-linejoin: round;
  }

  .stepper-step.current .stepper-circle {
    background: var(--color-accent-a15);
    border-color: var(--color-accent-400);
    color: var(--color-accent-300);
    animation: stepper-pulse 2s ease-out infinite;
  }

  @keyframes stepper-pulse {
    0%   { box-shadow: 0 0 0 0 var(--color-accent-a25); }
    70%  { box-shadow: 0 0 0 8px transparent; }
    100% { box-shadow: 0 0 0 0 transparent; }
  }

  .stepper-step.upcoming .stepper-circle {
    background: transparent;
    border-color: var(--color-border-strong);
    color: var(--color-foreground-disabled);
  }

  .stepper-label {
    margin-top: 8px;
    font-size: var(--text-xs);
    text-align: center;
  }

  .stepper-step.completed .stepper-label { color: var(--color-foreground-subtle); }
  .stepper-step.current   .stepper-label { color: var(--color-foreground); font-weight: var(--font-weight-semibold); }
  .stepper-step.upcoming  .stepper-label { color: var(--color-foreground-disabled); }

  /* ===== RADIO BUTTONS ===== */
  /* Hide native input */
  .radio-input {
    position: absolute;
    opacity: 0;
    width: 0;
    height: 0;
  }

  /* Label wrapper */
  .radio-label {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    cursor: pointer;
    font-size: var(--text-sm);
    font-weight: var(--font-weight-medium);
    color: var(--color-foreground-body);
    transition: color var(--duration-fast);
  }

  /* Group layouts */
  .radio-group            { display: flex; flex-direction: column; gap: 12px; }
  .radio-group.horizontal { flex-direction: row; gap: 24px; }

  /* Custom circle */
  .radio-circle {
    width: 20px;
    height: 20px;
    border-radius: var(--radius-full);
    border: 2px solid var(--color-border-strong);
    background: var(--color-bg-surface);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    transition: background var(--duration-fast), border-color var(--duration-fast), box-shadow var(--duration-fast);
  }

  /* Only enabled radios get the interactive hover border. */
  .radio-label:hover .radio-input:not(:disabled) + .radio-circle { border-color: var(--color-border-emphasis); }

  /* Inner dot */
  .radio-dot {
    width: 8px;
    height: 8px;
    border-radius: var(--radius-full);
    background: var(--color-accent-500);
    opacity: 0;
    transform: scale(0);
    transition: opacity var(--duration-fast), transform var(--duration-fast) ease-out;
  }

  /* Checked state */
  .radio-input:checked + .radio-circle {
    background: var(--color-accent-a15);
    border-color: var(--color-accent-400);
  }

  .radio-input:checked + .radio-circle .radio-dot {
    opacity: 1;
    transform: scale(1);
  }

  /* Brighten the label text when its radio is checked (.radio-text wraps the
     label so a sibling combinator can reach it — same pattern as checkbox). */
  .radio-input:checked ~ .radio-text {
    color: var(--color-foreground);
  }

  /* Focus ring */
  .radio-input:focus-visible + .radio-circle {
    box-shadow: var(--shadow-accent-focus);
  }

  /* Disabled */
  .radio-input:disabled + .radio-circle {
    opacity: 0.45;
    cursor: not-allowed;
  }

  .radio-input:disabled ~ * {
    color: var(--color-foreground-disabled);
    cursor: not-allowed;
  }

  /* ===== CHECKBOX ===== */
  /* Hide native input — drives the box via sibling selectors */
  .checkbox-input {
    position: absolute;
    opacity: 0;
    width: 0;
    height: 0;
  }

  /* Label wrapper */
  .checkbox-label {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    cursor: pointer;
    font-size: var(--text-sm);
    font-weight: var(--font-weight-medium);
    color: var(--color-foreground-body);
    transition: color var(--duration-fast);
  }

  /* Group layouts */
  .checkbox-group            { display: flex; flex-direction: column; gap: 12px; }
  .checkbox-group.horizontal { flex-direction: row; gap: 24px; }

  /* Custom box */
  .checkbox-box {
    width: 20px;
    height: 20px;
    border-radius: var(--radius-sm);
    border: 2px solid var(--color-border-strong);
    background: var(--color-bg-surface);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    position: relative;
    transition: background var(--duration-fast), border-color var(--duration-fast), box-shadow var(--duration-fast);
  }

  /* Only enabled checkboxes get the interactive hover border. */
  .checkbox-label:hover .checkbox-input:not(:disabled) + .checkbox-box { border-color: var(--color-border-emphasis); }

  /* Check icon (inline SVG inside .checkbox-box) — hidden until :checked */
  .checkbox-box svg {
    width: 14px;
    height: 14px;
    color: var(--color-on-accent);
    opacity: 0;
    transform: scale(0.6);
    transition: opacity var(--duration-fast), transform var(--duration-fast) ease-out;
  }

  /* Indeterminate dash — ::before on the box, hidden until :indeterminate */
  .checkbox-box::before {
    content: '';
    position: absolute;
    width: 10px;
    height: 2px;
    border-radius: 1px;
    background: var(--color-on-accent);
    opacity: 0;
    transform: scale(0.6);
    transition: opacity var(--duration-fast), transform var(--duration-fast) ease-out;
  }

  /* Checked — accent fill, white check */
  .checkbox-input:checked + .checkbox-box {
    background: var(--color-accent-500);
    border-color: var(--color-accent-500);
  }
  .checkbox-input:checked + .checkbox-box svg {
    opacity: 1;
    transform: scale(1);
  }

  /* Indeterminate — accent fill, white dash (check icon stays hidden) */
  .checkbox-input:indeterminate + .checkbox-box {
    background: var(--color-accent-500);
    border-color: var(--color-accent-500);
  }
  .checkbox-input:indeterminate + .checkbox-box::before {
    opacity: 1;
    transform: scale(1);
  }

  /* Focus ring */
  .checkbox-input:focus-visible + .checkbox-box {
    box-shadow: var(--shadow-accent-focus);
  }

  /* Disabled */
  .checkbox-input:disabled + .checkbox-box {
    opacity: 0.45;
    cursor: not-allowed;
  }
  .checkbox-input:disabled ~ * {
    color: var(--color-foreground-disabled);
    cursor: not-allowed;
  }

  /* ===== FILE UPLOAD ===== */
  .drop-zone {
    background: var(--color-bg-elevated-1);
    border: 2px dashed var(--color-border-default);
    border-radius: var(--radius-lg);
    padding: 40px 24px;
    text-align: center;
    cursor: pointer;
    transition: border-color var(--duration-fast), background var(--duration-fast), box-shadow var(--duration-fast);
  }

  .drop-zone:hover,
  .drop-zone.drag-over {
    border-color: var(--color-accent-400);
    background: var(--color-accent-a15);
  }

  .drop-zone:focus-visible {
    outline: none;
    box-shadow: var(--shadow-accent-focus);
  }

  .drop-zone-icon {
    margin-bottom: 12px;
    color: var(--color-foreground-disabled);
  }
  .drop-zone-icon svg { width: 40px; height: 40px; stroke: currentColor; fill: none; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }

  .drop-zone-text { font-size: var(--text-base); font-weight: var(--font-weight-semibold); color: var(--color-foreground-body); margin-bottom: 6px; }
  .drop-zone-hint { font-size: var(--text-xs); color: var(--color-foreground-disabled); }

  .drop-zone--compact { padding: 20px 24px; }
  .drop-zone--compact .drop-zone-icon svg { width: 28px; height: 28px; }

  /* Rejected/invalid state (#107) — a wrong type or oversized drop. The message copy
     is app-supplied (the app knows why), rendered in .drop-zone-error below the zone. */
  .drop-zone--error {
    border-color: var(--color-error);
    background: color-mix(in srgb, var(--color-error) 8%, transparent);
  }
  .drop-zone-error {
    margin-top: 8px;
    font-size: var(--text-xs);
    color: var(--color-error);
  }

  /* File list */
  .file-list { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }

  .file-item {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 14px;
    background: var(--color-bg-elevated-2);
    border: 1px solid var(--color-border-default);
    border-radius: var(--radius-md);
  }

  .file-item-icon { color: var(--color-foreground-subtle); display: flex; flex-shrink: 0; }
  .file-item-icon svg { width: 20px; height: 20px; stroke: currentColor; fill: none; stroke-width: 2; }

  .file-item-info { flex: 1; min-width: 0; }
  .file-item-name { font-size: var(--text-sm); font-weight: var(--font-weight-semibold); color: var(--color-foreground); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .file-item-size { font-size: var(--text-xs); color: var(--color-foreground-subtle); }

  .file-item-progress {
    height: 4px;
    background: var(--color-border-subtle);
    border-radius: var(--radius-full);
    margin-top: 6px;
    overflow: hidden;
  }

  .file-item-progress-fill {
    height: 100%;
    background: var(--color-accent-500);
    border-radius: var(--radius-full);
    transition: width var(--duration-normal) ease-out;
  }

  .file-item-remove {
    background: none;
    border: none;
    color: var(--color-foreground-subtle);
    cursor: pointer;
    display: flex;
    padding: 4px;
    border-radius: var(--radius-sm);
    transition: color var(--duration-fast);
    flex-shrink: 0;
  }
  .file-item-remove:hover { color: var(--color-error); }
  .file-item-remove svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 2; }

  /* ===== COMMAND PALETTE ===== */
  .cmd-backdrop {
    position: fixed;
    inset: 0;
    background: var(--command-palette-backdrop);
    backdrop-filter: blur(4px);
    z-index: var(--z-command);
    animation: fade-in var(--duration-normal) ease-out;
  }

  .command-palette {
    position: fixed;
    top: 20%;
    left: 50%;
    transform: translateX(-50%);
    width: 100%;
    max-width: 600px;
    background: var(--color-bg-elevated-2);
    border: 1px solid var(--color-neutral-400);
    border-radius: var(--radius-lg);
    box-shadow: var(--command-palette-shadow);
    overflow: hidden;
    z-index: var(--z-command-content);
    animation: cmd-in var(--duration-normal) ease-out;
  }

  @keyframes cmd-in {
    from { opacity: 0; transform: translateX(-50%) scale(0.96); }
    to   { opacity: 1; transform: translateX(-50%) scale(1); }
  }

  @keyframes fade-in {
    from { opacity: 0; }
    to   { opacity: 1; }
  }

  .command-palette-input-wrap {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 14px 16px;
    border-bottom: 1px solid var(--color-border-default);
  }

  .command-palette-input-wrap svg {
    width: 18px;
    height: 18px;
    stroke: var(--color-foreground-subtle);
    fill: none;
    stroke-width: 2;
    flex-shrink: 0;
  }

  .command-palette-input {
    flex: 1;
    background: transparent;
    border: none;
    outline: none;
    font-size: var(--text-base);
    color: var(--color-foreground);
  }

  .command-palette-input::placeholder { color: var(--color-foreground-disabled); }

  .command-palette-results {
    max-height: 400px;
    overflow-y: auto;
    padding: 8px 0;
  }

  .command-palette-group-label {
    padding: 8px 14px 4px;
    font-size: var(--text-xs);
    font-weight: var(--font-weight-semibold);
    color: var(--color-foreground-subtle);
    text-transform: uppercase;
    letter-spacing: 0.5px;
  }

  .command-palette-item {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 14px;
    cursor: pointer;
    transition: background var(--duration-fast);
  }

  .command-palette-item svg {
    width: 18px;
    height: 18px;
    stroke: var(--color-foreground-subtle);
    fill: none;
    stroke-width: 2;
    flex-shrink: 0;
  }

  .command-palette-item:hover,
  .command-palette-item:focus {
    background: var(--color-accent-a15);
    outline: none;
  }

  .command-palette-item:hover .command-palette-item-label,
  .command-palette-item:focus .command-palette-item-label {
    color: var(--color-foreground);
  }

  .command-palette-item-label {
    flex: 1;
    font-size: var(--text-sm);
    color: var(--color-foreground-body);
  }

  .command-palette-kbd {
    font-family: var(--font-family-mono);
    font-size: var(--text-xs);
    color: var(--color-foreground-subtle);
    background: var(--color-neutral-350);
    border-radius: var(--radius-sm);
    padding: 3px 7px;
    white-space: nowrap;
  }

  .command-palette-divider {
    height: 1px;
    background: var(--color-neutral-350);
    margin: 4px 0;
  }

  /* ===== SLIDER ===== */
  .slider-wrap {
    display: flex;
    align-items: center;
    gap: 0;
  }

  .slider-min,
  .slider-max {
    font-size: var(--text-xs);
    color: var(--color-foreground-subtle);
    white-space: nowrap;
  }

  .slider-min { margin-right: 10px; }
  .slider-max { margin-left: 10px; }

  .slider-value-label {
    font-size: var(--text-xs);
    font-weight: var(--font-weight-semibold);
    color: var(--color-accent-400);
    margin-left: 12px;
    min-width: 32px;
    text-align: right;
  }

  /* Range input reset */
  .slider {
    -webkit-appearance: none;
    appearance: none;
    flex: 1;
    height: 6px;
    border-radius: var(--radius-full);
    outline: none;
    cursor: pointer;
    background: linear-gradient(
      to right,
      var(--color-accent-500) 0%,
      var(--color-accent-500) var(--fill, 40%),
      var(--color-border-default) var(--fill, 40%),
      var(--color-border-default) 100%
    );
  }

  /* Thumb — WebKit */
  .slider::-webkit-slider-thumb {
    -webkit-appearance: none;
    width: 18px;
    height: 18px;
    border-radius: var(--radius-full);
    background: var(--color-accent-500);
    border: 2px solid var(--color-accent-400);
    cursor: pointer;
    transition: background var(--duration-fast), box-shadow var(--duration-fast), transform var(--duration-fast);
  }

  .slider::-webkit-slider-thumb:hover {
    background: var(--color-accent-400);
    transform: scale(1.1);
  }

  .slider:active::-webkit-slider-thumb {
    background: var(--color-accent-600);
    box-shadow: 0 0 0 6px var(--color-accent-a15);
  }

  .slider:focus-visible::-webkit-slider-thumb {
    box-shadow: var(--shadow-accent-focus);
  }

  /* Thumb — Firefox */
  .slider::-moz-range-thumb {
    width: 18px;
    height: 18px;
    border-radius: var(--radius-full);
    background: var(--color-accent-500);
    border: 2px solid var(--color-accent-400);
    cursor: pointer;
    transition: background var(--duration-fast), box-shadow var(--duration-fast);
  }

  .slider::-moz-range-progress {
    background: var(--color-accent-500);
    height: 6px;
    border-radius: var(--radius-full);
  }

  .slider::-moz-range-track {
    background: var(--color-border-default);
    height: 6px;
    border-radius: var(--radius-full);
  }

  /* Disabled */
  .slider:disabled {
    cursor: not-allowed;
    opacity: 0.45;
  }

  /* Radix Slider composite — used by the React SpectrumSlider. The `.slider`
     rules above style a native <input type="range">; Radix renders a div tree
     (Root > Track > Range, + Thumb) that needs its own rules. Values mirror the
     native slider: 6px track, accent-500 fill, 18px accent thumb. */
  .slider-root {
    position: relative;
    display: flex;
    align-items: center;
    flex: 1;
    height: 18px;
    cursor: pointer;
    touch-action: none;
    user-select: none;
  }

  .slider-track {
    position: relative;
    flex-grow: 1;
    height: 6px;
    border-radius: var(--radius-full);
    background: var(--color-border-default);
  }

  .slider-range {
    position: absolute;
    height: 100%;
    border-radius: var(--radius-full);
    background: var(--color-accent-500);
  }

  .slider-thumb {
    display: block;
    width: 18px;
    height: 18px;
    border-radius: var(--radius-full);
    background: var(--color-accent-500);
    border: 2px solid var(--color-accent-400);
    cursor: pointer;
    transition: background var(--duration-fast), box-shadow var(--duration-fast), transform var(--duration-fast);
  }

  .slider-thumb:hover {
    background: var(--color-accent-400);
    transform: scale(1.1);
  }

  .slider-thumb:focus-visible {
    outline: none;
    box-shadow: var(--shadow-accent-focus);
  }

  .slider-thumb:active {
    background: var(--color-accent-600);
    box-shadow: 0 0 0 6px var(--color-accent-a15);
  }

  .slider-root[data-disabled] {
    cursor: not-allowed;
    opacity: 0.45;
  }

  /* ===== DATE PICKER ===== */
  .datepicker-input-wrap {
    position: relative;
    display: flex;
    align-items: center;
  }

  .datepicker-icon {
    position: absolute;
    right: 14px;
    color: var(--color-foreground-subtle);
    display: flex;
    pointer-events: none;
  }

  .datepicker-icon svg {
    width: 16px;
    height: 16px;
    stroke: currentColor;
    fill: none;
    stroke-width: 2;
  }

  .datepicker-calendar {
    background: var(--color-bg-elevated-2);
    border: 1px solid var(--color-neutral-400);
    border-radius: var(--datepicker-radius);
    padding: 12px;
    margin-top: 4px;
    animation: dp-in var(--duration-normal) ease-out;
  }

  @keyframes dp-in {
    from { opacity: 0; transform: translateY(-4px); }
    to   { opacity: 1; transform: translateY(0); }
  }

  .datepicker-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 12px;
  }

  .datepicker-month-label {
    font-weight: var(--font-weight-extrabold);
    font-size: var(--text-base);
    color: var(--color-foreground);
    /* Button reset — the label was a static span; it is a <button> in the day and month
       panes, where it zooms out to the month and year grids. It stays a <span> in the
       year pane, which is the top of the zoom stack and has nowhere further to go. */
    background: transparent;
    border: 0;
    padding: 4px 8px;
    border-radius: var(--radius-md);
    font-family: inherit;
    transition: background var(--duration-fast), color var(--duration-fast);
  }

  /* Only the button form is interactive; the span form must not pick up hover chrome —
     and `cursor: pointer` is hover chrome. It sat on the shared rule, so the year pane's
     inert <span> advertised a click it cannot perform, contradicting the line above it. */
  button.datepicker-month-label {
    cursor: pointer;
  }

  button.datepicker-month-label:hover {
    background: var(--color-foreground-a05);
    color: var(--color-accent-300);
  }

  /* The month and year panes: a 3x4 grid of periods, pairing with .datepicker-days /
     .datepicker-day above. One grid class serves both — twelve months and twelve years
     are the same shape, and giving them separate rules would mean two places to keep in
     step for no gain. */
  .datepicker-periods {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 6px;
    text-align: center;
    /* Match the day grid's natural width — 7 cells plus 6 two-pixel gaps. The popover
       sizes to its content, so without this the panel snapped from 292px to ~130px the
       moment the month pane opened and back again on the way out: the calendar visibly
       jumped under the pointer mid-navigation. It also keeps a period cell ~93px wide
       instead of 33px, which is what puts it past the 44px touch target on its own. */
    min-width: calc(var(--datepicker-day-size) * 7 + 12px);
  }

  .datepicker-period {
    height: var(--datepicker-period-height);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--radius-md);
    font-size: var(--text-sm);
    color: var(--color-foreground-muted);
    cursor: pointer;
    transition: background var(--duration-fast);
    background: transparent;
    border: 0;
    padding: 0;
    font-family: inherit;
  }

  .datepicker-period:hover:not(:disabled) { background: var(--color-foreground-a05); }
  .datepicker-period:disabled { color: var(--color-foreground-disabled); cursor: not-allowed; }
  .datepicker-period:disabled:hover { background: transparent; }
  /* The period the calendar is currently viewing — an outline rather than the accent
     fill, so it never reads as a selected VALUE. Nothing is chosen until a day is. */
  .datepicker-period.current {
    box-shadow: inset 0 0 0 1px var(--color-border-default);
    color: var(--color-foreground);
  }

  /* Footer row beneath the grid — the date-time picker's time control, and the home for
     Today / Clear actions when those ship. */
  .datepicker-footer {
    margin-top: 12px;
    padding-top: 12px;
    border-top: 1px solid var(--color-border-subtle);
    display: flex;
    align-items: center;
    gap: 8px;
  }

  .datepicker-footer .time-picker { flex: 1; }

  /* Keyboard focus must be visible on every control in the calendar. This system has no
     global button focus rule — each interactive class carries its own — and all three of
     these were missing one. It matters more here than elsewhere: the grids use a roving
     tabindex, so arrow keys move focus between cells that would otherwise show no sign of
     having it. `.datepicker-day` predates the panes and had the same gap. */
  .datepicker-day:focus-visible,
  .datepicker-period:focus-visible,
  button.datepicker-month-label:focus-visible,
  .datepicker-nav-btn:focus-visible {
    outline: none;
    box-shadow: var(--shadow-accent-focus);
  }

  .datepicker-weekdays {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    text-align: center;
    margin-bottom: 8px;
  }

  .datepicker-weekdays span {
    font-size: var(--text-xs);
    color: var(--color-foreground-subtle);
    font-weight: var(--font-weight-semibold);
  }

  .datepicker-days {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    gap: 2px;
    text-align: center;
  }

  .datepicker-day {
    width: var(--datepicker-day-size);
    height: var(--datepicker-day-size);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--radius-full);
    font-size: var(--text-xs);
    color: var(--color-foreground-muted);
    cursor: pointer;
    transition: background var(--duration-fast);
    /* Button reset (#104) — the day cells were authored as static spans; a real
       interactive picker renders them as <button>s, which need the chrome removed. */
    background: transparent;
    border: 0;
    padding: 0;
    font-family: inherit;
  }

  .datepicker-day:hover    { background: var(--color-foreground-a05); }
  /* Out-of-range days (min/max) — dimmed and inert (#104). */
  .datepicker-day:disabled { color: var(--color-foreground-disabled); cursor: not-allowed; }
  .datepicker-day:disabled:hover { background: transparent; }
  .datepicker-day.today    { background: var(--color-accent-500); color: var(--color-on-accent); }
  .datepicker-day.selected { background: var(--color-accent-500); color: var(--color-on-accent); }
  .datepicker-day.in-range { background: var(--color-accent-a15); color: var(--color-foreground); border-radius: 0; }
  .datepicker-day.range-start,
  .datepicker-day.range-end { background: var(--color-accent-500); color: var(--color-on-accent); border-radius: var(--radius-full); }
  .datepicker-day.empty    { cursor: default; }

  /* Header prev/next month buttons (#104). The spec documents .datepicker-nav-btn but
     the CSS never shipped it — completing that here, a small ghost icon button. */
  .datepicker-nav-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    border-radius: var(--radius-md);
    border: 0;
    background: transparent;
    color: var(--color-foreground-subtle);
    cursor: pointer;
    transition: background var(--duration-fast), color var(--duration-fast);
  }
  .datepicker-nav-btn:hover { background: var(--color-foreground-a05); color: var(--color-foreground); }
  .datepicker-nav-btn svg { width: 18px; height: 18px; }

  .datepicker-range-wrap {
    display: flex;
    align-items: center;
    gap: 12px;
  }

  .datepicker-range-wrap .datepicker-input-wrap { flex: 1; }

  .datepicker-range-arrow {
    color: var(--color-foreground-subtle);
    display: flex;
    flex-shrink: 0;
  }

  .datepicker-range-arrow svg {
    width: 16px;
    height: 16px;
    stroke: currentColor;
    fill: none;
    stroke-width: 2;
  }

  /* ===== TIME PICKER (#110) ===== */
  /* A typeable text input with a clock affordance that opens a popover of
     step-quantized options — the styled counterpart to a native <input type="time">. */
  .time-picker {
    position: relative;
    display: flex;
    align-items: center;
  }
  .time-picker-icon {
    position: absolute;
    right: 14px;
    color: var(--color-foreground-subtle);
    display: flex;
    pointer-events: none;
  }
  .time-picker-icon svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 2; }
  /* Scrollable option list inside the popover. */
  .time-picker-list {
    display: flex;
    flex-direction: column;
    gap: 2px;
    max-height: 240px;
    overflow-y: auto;
    min-width: 8rem;
  }
  .time-picker-option {
    display: flex;
    align-items: center;
    width: 100%;
    padding: var(--dropdown-item-padding-y) var(--dropdown-item-padding-x);
    border-radius: var(--dropdown-item-radius);
    font-size: var(--text-sm);
    color: var(--color-foreground-secondary);
    background: none;
    border: none;
    cursor: pointer;
    text-align: left;
    transition: background var(--duration-fast);
  }
  .time-picker-option:hover { background: var(--color-accent-a08); }
  .time-picker-option.selected {
    background: var(--color-accent-a15);
    color: var(--color-foreground);
    font-weight: var(--font-weight-semibold);
  }

  /* ===== SEARCH ===== */
  /* Keyboard shortcut badge — monospace pill for hints like ⌘K, Esc, ↵.
     General utility: use anywhere a keyboard shortcut appears — search pill,
     tooltips, dropdown action menus, command palette. Parent owns positioning. */
  .kbd {
    display: inline-flex;
    align-items: center;
    background: var(--color-neutral-350);
    color: var(--color-foreground-subtle);
    font-family: var(--font-family-mono);
    font-size: var(--text-xs);
    border-radius: var(--radius-sm);
    padding: 2px 6px;
  }

  /* Search pill — clickable trigger that reveals the full search bar.
     Common in top nav bars. Not a state of the bar — different component. */
  .search-pill {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 8px 12px;
    background: var(--color-bg-elevated-2);
    border: 1px solid var(--color-border-default);
    border-radius: var(--radius-md);
    color: var(--color-foreground-disabled);
    font-size: var(--text-sm);
    cursor: pointer;
    transition: border-color var(--duration-fast), color var(--duration-fast);
  }

  .search-pill:hover {
    border-color: var(--color-border-strong);
    color: var(--color-foreground-subtle);
  }

  /* Inside .search-pill, push the keyboard shortcut badge to the far right. */
  .search-pill .kbd { margin-left: auto; }

  /* Size the leading icon. Without this an inline <svg viewBox="0 0 24 24"> takes the
     replaced-element default and balloons in the flex row — every other icon-bearing
     component ships this one-liner (.badge svg, .alert svg, …) and Search was the one
     that didn't. 16px per search.md. */
  .search-pill svg { width: 16px; height: 16px; flex-shrink: 0; }

  /* Search bar — the full expanded input state with icon on the left and
     accent focus ring. Use as a standalone search field or revealed from
     a .search-pill trigger. */
  .search-bar {
    position: relative;
    display: flex;
    align-items: center;
    background: var(--color-bg-elevated-2);
    border: 1px solid var(--color-border-default);
    border-radius: var(--radius-md);
  }

  .search-bar-icon {
    position: absolute;
    left: 12px;
    color: var(--color-foreground-disabled);
    display: flex;
  }

  .search-bar-icon svg { width: 16px; height: 16px; flex-shrink: 0; }

  .search-bar-input {
    width: 100%;
    background: transparent;
    border: none;
    outline: none;
    padding: 10px 14px 10px 40px;
    font-size: var(--text-sm);
    color: var(--color-foreground);
  }

  .search-bar:focus-within {
    border-color: var(--color-accent-400);
    box-shadow: var(--shadow-accent-focus);
  }

  .search-results {
    position: absolute;
    top: 100%;
    left: -1px;
    right: -1px;
    background: var(--color-bg-elevated-2);
    border: 1px solid var(--color-neutral-400);
    border-top: none;
    border-radius: 0 0 var(--radius-md) var(--radius-md);
    padding: 8px 0;
    z-index: var(--z-floating);
  }

  .search-section-label {
    padding: 6px 14px 4px;
    font-size: var(--text-xs);
    font-weight: var(--font-weight-semibold);
    color: var(--color-foreground-subtle);
    text-transform: uppercase;
    letter-spacing: 0.5px;
  }

  .search-result-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 9px 14px;
    cursor: pointer;
    transition: background var(--duration-fast);
  }

  .search-result-item:hover {
    background: var(--color-accent-a15);
    color: var(--color-foreground);
  }

  .search-result-item svg { width: 16px; height: 16px; flex-shrink: 0; }

  .search-result-meta {
    margin-left: auto;
    font-size: var(--text-xs);
    color: var(--color-foreground-subtle);
  }

  .search-divider {
    height: 1px;
    background: var(--color-neutral-350);
    margin: 4px 0;
  }

  /* ===== COLOR PICKER ===== */
  .color-picker {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
  }

  /* The interactive palette button. Named .color-swatch, NOT .swatch: the data dot
     down in the SWATCH section also renders .swatch, and two rules for one selector
     in the same @layer silently merge — the dot (later) won its size onto this
     button, and this button leaked cursor:pointer + a grow-on-hover onto every
     decorative dot. Distinct components, distinct classes; this one lives with the
     .color-picker family it belongs to. (#126) */
  .color-swatch {
    width: 36px;
    height: 36px;
    border-radius: var(--radius-full);
    border: none;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition: transform var(--duration-fast);
  }

  .color-swatch:hover { transform: scale(1.1); }

  .color-swatch--selected {
    box-shadow: 0 0 0 2px var(--color-bg-base), 0 0 0 4px var(--color-foreground);
  }

  .color-swatch .check {
    width: 16px;
    height: 16px;
    stroke: var(--color-on-accent);
    fill: none;
    stroke-width: 2.5;
    stroke-linecap: round;
    stroke-linejoin: round;
    filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));
  }

  .color-swatch:focus-visible {
    outline: 2px solid var(--color-accent-400);
    outline-offset: 2px;
  }

  .color-picker-extended { display: flex; flex-direction: column; gap: 12px; }

  .color-group-label {
    font-size: var(--text-xs);
    color: var(--color-foreground-ghost);
    margin-bottom: 4px;
  }

  .color-group-swatches {
    display: flex;
    gap: 6px;
    flex-wrap: wrap;
  }

  .color-group-swatches .swatch {
    width: 32px;
    height: 32px;
    border-radius: var(--radius-md);
  }

  /* Advanced / freeform picker (#109) — the spec's third variant: a saturation/
     brightness field + hue bar + hex input, for arbitrary user colour (belt/group hex)
     that AccentColorPicker's token-bound swatches can't express. */
  .color-picker-advanced {
    display: flex;
    flex-direction: column;
    gap: 12px;
    width: 16rem; /* w-64, per the spec */
  }
  /* 2D field: base hue (set inline via --color-field-hue), white→transparent across
     (saturation), transparent→black down (brightness). */
  /* The field/hue colours are the sRGB colour space itself, not themeable tokens —
     expressed in their natural forms (hsl for the hue rainbow, rgba for the
     saturation/brightness overlays) rather than hex. That reasoning is now stated to
     the linter as well as to the reader: lint-tokens learned to see functional colour
     notation, and a hue spectrum is the picker's DATA — the same exemption a Swatch's
     user-chosen colour gets. Keep the pragma tight to the gradient. */
  .color-field {
    position: relative;
    width: 100%;
    height: 140px;
    border-radius: var(--radius-md);
    cursor: crosshair;
    touch-action: none;
    /* lint-tokens-disable */
    background:
      linear-gradient(to top, rgb(0, 0, 0), rgba(0, 0, 0, 0)),
      linear-gradient(to right, rgb(255, 255, 255), rgba(255, 255, 255, 0)),
      var(--color-field-hue, hsl(0, 100%, 50%));
    /* lint-tokens-enable */
  }
  .color-field:focus-visible { outline: none; box-shadow: var(--shadow-accent-focus); }
  /* Two axis sliders overlay the field (saturation + brightness) — a 2D field can't be one
     ARIA slider (#116). Keyboard-only ARIA proxies: the container is the pointer surface,
     so these opt out of pointer events; focus lights the whole field via the shared ring. */
  .color-field-axis {
    position: absolute;
    inset: 0;
    border-radius: inherit;
    pointer-events: none;
  }
  .color-field-axis:focus-visible { outline: none; box-shadow: var(--shadow-accent-focus); }
  .color-field-dot,
  .color-hue-dot {
    position: absolute;
    width: 14px;
    height: 14px;
    border-radius: var(--radius-full);
    border: 2px solid rgb(255, 255, 255);
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.4);
    transform: translate(-50%, -50%);
    pointer-events: none;
  }
  /* Hue bar: full rainbow, a draggable thumb along it. */
  .color-hue {
    position: relative;
    width: 100%;
    height: 14px;
    border-radius: var(--radius-full);
    cursor: pointer;
    touch-action: none;
    /* lint-tokens-disable — the rainbow IS the control's data (see .color-field). */
    background: linear-gradient(
      to right,
      hsl(0, 100%, 50%), hsl(60, 100%, 50%), hsl(120, 100%, 50%), hsl(180, 100%, 50%),
      hsl(240, 100%, 50%), hsl(300, 100%, 50%), hsl(360, 100%, 50%)
    );
    /* lint-tokens-enable */
  }
  .color-hue:focus-visible { outline: none; box-shadow: var(--shadow-accent-focus); }
  .color-hue-dot { top: 50%; }
  /* Preview + hex row. */
  .color-picker-row { display: flex; align-items: center; gap: 8px; }
  .color-preview {
    width: 32px;
    height: 32px;
    border-radius: var(--radius-md);
    flex-shrink: 0;
    border: 1px solid var(--color-border-subtle);
    background: var(--color-preview-fill, #000);
  }
  .color-hex-input { flex: 1; text-transform: lowercase; }
  /* Optional quick-preset swatches. */
  .color-presets { display: flex; gap: 6px; flex-wrap: wrap; }
  .color-preset {
    width: 24px;
    height: 24px;
    border-radius: var(--radius-md);
    border: 1px solid var(--color-border-subtle);
    cursor: pointer;
    padding: 0;
    background: var(--color-preset-fill, #000);
  }
  .color-preset:focus-visible { outline: none; box-shadow: var(--shadow-accent-focus); }

  /* The compact TRIGGER form of the picker (#140) — a swatch button that opens
     .color-picker-advanced in a popover, for the case the panel is wrong for: a colour
     that is one field among several in a narrow column. Named .color-trigger, NOT
     .color-field — .color-field is already the saturation/brightness surface above, and
     this repo has done the two-meanings-one-word mistake once already (.color-swatch vs
     .swatch, #126).

     Sized to .input rather than to a NUMBER. Same vertical padding, same border width,
     and a ::before of exactly one .input line box, so --spacing-3 or --input-font-size
     moving moves this with it. That is the whole point of the ticket: the hand-rolled
     trigger it replaces was 44px tall against a 50px input, already 6px out of line with
     every control beside it, and nothing would ever have told the consumer. Do not
     "simplify" this to height: 50px — that re-freezes the drift the class exists to end. */
  .color-trigger {
    display: inline-flex;
    flex-shrink: 0;
    padding: var(--spacing-3);
    font-size: var(--input-font-size);
    border: 1px solid var(--input-border);
    border-radius: var(--input-radius);
    background: var(--color-trigger-fill, #000);
    cursor: pointer;
    transition: border-color var(--duration-fast), box-shadow var(--duration-fast);
  }
  /* The content box: a SQUARE of one .input line box, so 12px padding on all four sides
     plus the border lands on .input's exact height in both axes.

     A button with no children has no line box at all, so without this the control
     collapses to padding + border and renders 26px tall against a 50px input — the very
     misalignment the class exists to end.

     Both axes are set deliberately. `aspect-ratio: 1` does NOT work here: .color-trigger's
     documented placement is a flex row, and a flex item resolves its main size from
     content before aspect-ratio gets a definite dimension to work from — measured 26x50,
     right height, wrong width. Sizing the content box directly is what actually squares it. */
  .color-trigger::before { content: ""; display: block; width: 1lh; height: 1lh; }
  .color-trigger:hover:not(:disabled) { border-color: var(--color-accent-a25); }
  /* Matches .input:focus, not the --shadow-accent-focus the small swatches use: this
     control sits in a form row beside real inputs and has to focus like one. */
  .color-trigger:focus-visible {
    outline: none;
    border-color: var(--color-accent-400);
    box-shadow: 0 0 0 3px var(--color-accent-a15);
  }
  .color-trigger:disabled {
    cursor: not-allowed;
    opacity: 0.45;
  }

  /* ===== CALENDAR ===== */
  .cal-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 16px;
  }

  .cal-title {
    font-size: var(--text-lg);
    font-weight: var(--font-weight-extrabold);
    color: var(--color-foreground);
    letter-spacing: var(--font-letter-spacing-tight);
  }

  .cal-weekdays {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    text-align: center;
    margin-bottom: 8px;
  }

  .cal-weekdays span {
    color: var(--color-foreground-subtle);
    font-size: var(--text-xs);
    font-weight: var(--font-weight-semibold);
  }

  .cal-grid {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    gap: 2px;
  }

  .cal-day {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    border-radius: var(--radius-full);
    font-size: var(--text-xs);
    color: var(--color-foreground-muted);
    cursor: pointer;
    transition: background var(--duration-fast);
  }

  .cal-day:hover { background: var(--color-foreground-a05); }
  .cal-day.today { background: var(--color-accent-500); color: var(--color-on-accent); }
  .cal-day.selected { background: var(--color-accent-a15); color: var(--color-foreground); }
  .cal-day.empty { cursor: default; }

  .cal-day.has-event::after {
    content: '';
    display: block;
    width: 4px;
    height: 4px;
    border-radius: var(--radius-full);
    background: var(--color-accent-400);
    margin-top: 2px;
  }

  /* Event list card */
  .cal-event-card {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px;
    background: var(--color-bg-elevated-2);
    border-radius: var(--radius-md);
  }
  .cal-event-card--accent  { border-left: 3px solid var(--color-accent-400); }
  .cal-event-card--success { border-left: 3px solid var(--color-success); }
  .cal-event-card--warning { border-left: 3px solid var(--color-warning); }

  .cal-event-title { font-weight: var(--font-weight-semibold); color: var(--color-foreground); font-size: var(--text-sm); }
  .cal-event-time  { color: var(--color-foreground-subtle); font-size: var(--text-xs); }

  /* Week view */
  .cal-week-grid {
    display: grid;
    grid-template-columns: 60px repeat(7, 1fr);
    gap: 1px;
  }

  .cal-week-cell {
    min-height: 48px;
    background: var(--color-bg-surface);
    border: 1px solid var(--color-border-subtle);
    padding: 4px;
  }

  .cal-event-block {
    border-radius: var(--radius-sm);
    padding: 4px 8px;
    font-size: var(--text-xs);
    font-weight: var(--font-weight-medium);
  }

  .cal-event-block--accent  { background: var(--color-accent-a15);                border-left: 3px solid var(--color-accent-400);    color: var(--color-accent-300); }
  .cal-event-block--success { background: color-mix(in srgb, var(--color-success) 12%, transparent); border-left: 3px solid var(--color-success); color: var(--color-success); }
  .cal-event-block--warning { background: color-mix(in srgb, var(--color-warning) 12%, transparent); border-left: 3px solid var(--color-warning); color: var(--color-warning); }

  /* ===== SELECT ===== */
  .select {
    position: relative;
    display: flex;
    align-items: center;
    gap: var(--select-chip-gap);
    background: var(--color-bg-surface);
    border: 1px solid var(--color-border-default);
    border-radius: var(--select-radius);
    padding: 8px 12px;
    min-height: 48px;
    cursor: pointer;
    transition: border-color var(--duration-fast), box-shadow var(--duration-fast);
  }
  .select--multi { flex-wrap: wrap; }

  .select:hover { border-color: var(--color-border-strong); }
  .select.open  { border-color: var(--color-accent-400); box-shadow: var(--shadow-accent-focus); }

  .select-value {
    font-size: var(--text-sm);
    color: var(--color-foreground);
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .select-value--empty { color: var(--color-foreground-disabled); }

  .select-chevron {
    margin-left: auto;
    color: var(--color-foreground-subtle);
    display: flex;
    transition: transform var(--duration-normal);
  }
  .select.open .select-chevron { transform: rotate(180deg); }
  .select-chevron svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 2; }

  .select-count { font-size: var(--text-xs); color: var(--color-foreground-subtle); }

  .select-dropdown {
    background: var(--color-bg-elevated-2);
    border: 1px solid var(--color-neutral-400);
    border-radius: var(--select-dropdown-radius);
    margin-top: 4px;
    max-height: 320px;
    z-index: var(--z-floating);
    animation: select-in var(--duration-normal) ease-out;
    /* Column so .select-options can take the leftover height and scroll alone —
       see the note on .select-options below. */
    display: flex;
    flex-direction: column;
  }

  /* The scrollable list itself. This class emitted from the guide, the specs and
     the React Select, but had no rule — the scroll lived on .select-dropdown, which
     also contains .select-search and .select-actions. That meant filtering a long
     list scrolled the search field you were typing in up and out of view. Scoping
     the scroll here keeps the search box and bulk actions pinned while only the
     options move. */
  .select-options {
    overflow-y: auto;
    min-height: 0;                        /* let it shrink inside the flex column */
    overscroll-behavior: contain;         /* don't chain to the page at the ends */
  }

  @keyframes select-in {
    from { opacity: 0; transform: translateY(-4px); }
    to   { opacity: 1; transform: translateY(0); }
  }

  .select-search { border-bottom: 1px solid var(--color-border-default); }
  .select-search input {
    width: 100%;
    background: transparent;
    border: none;
    outline: none;
    padding: 10px 14px;
    font-size: var(--text-sm);
    color: var(--color-foreground);
  }
  .select-search input::placeholder { color: var(--color-foreground-disabled); }

  .select-actions {
    display: flex;
    justify-content: space-between;
    padding: 8px 14px;
    border-bottom: 1px solid var(--color-border-default);
  }

  .select-action-link {
    background: none;
    border: none;
    font-size: var(--text-xs);
    color: var(--color-accent-400);
    cursor: pointer;
    transition: color var(--duration-fast);
  }
  .select-action-link:hover { color: var(--color-accent-300); }

  .select-group-label {
    padding: 8px 14px 4px;
    font-size: var(--text-xs);
    font-weight: var(--font-weight-semibold);
    color: var(--color-foreground-subtle);
    text-transform: uppercase;
    letter-spacing: 0.5px;
  }

  .select-option {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 9px 14px;
    cursor: pointer;
    color: var(--color-foreground-body);
    transition: background var(--duration-fast), color var(--duration-fast);
  }
  .select-option:hover { background: var(--color-accent-a15); color: var(--color-foreground); }
  .select-option[aria-disabled="true"] {
    color: var(--color-foreground-disabled);
    cursor: not-allowed;
  }
  .select-option[aria-disabled="true"]:hover { background: transparent; color: var(--color-foreground-disabled); }

  .select-check {
    margin-left: auto;
    display: none;
    color: var(--color-accent-400);
  }
  .select-option[aria-selected="true"] .select-check { display: inline-flex; }
  .select-check svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 2.5; }

  .select-checkbox {
    width: 16px;
    height: 16px;
    border-radius: var(--radius-sm);
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  .select-checkbox.checked {
    background: var(--color-accent-500);
  }
  .select-checkbox.checked svg {
    width: 11px;
    height: 11px;
    stroke: var(--color-on-accent);
    fill: none;
    stroke-width: 2.5;
  }
  .select-checkbox.unchecked {
    background: var(--color-border-subtle);
    border: 1px solid var(--color-border-emphasis);
  }

  .select-empty {
    text-align: center;
    color: var(--color-foreground-disabled);
    font-size: var(--text-sm);
    padding: 24px 14px;
  }

  /* ===== SEGMENTED CONTROL (EXTENDED) ===== */
  /* The three layout declarations below are load-bearing as a SET, and each one
     exists to cancel a failure the other two cause. Removing any of them looks
     safe and is not (#139):
       flex-wrap   — a 5-option track is ~417px. In a container narrower than that
                     it used to overflow with no wrap and no scroll, so the trailing
                     options were clipped and literally unreachable.
       flex-shrink — but wrap alone lowers the track's min-content floor from "all
                     options" to "the widest one", and flexbox reads that as
                     permission to squeeze. A 3-option control beside an ellipsising
                     heading then split into 2 rows at 440px and 3 rows at 320px,
                     on desktop, where nothing was ever wrong. shrink:0 makes the
                     sibling absorb the squeeze exactly as it did before.
       max-width   — shrink:0 on its own would just re-overflow a too-narrow flex
                     parent, so cap the track at its container. Wrap turns that cap
                     into rows instead of an overflow.
     Net: identical geometry everywhere the track already fit, wrapping only where
     it genuinely could not. That is why this is not a `@media (max-width: 767px)`
     rule — the failure is a container that is too narrow, which happens at every
     viewport (a 360px rail on a 1440px screen), and a viewport query cannot see it. */
  .segmented-control {
    display: inline-flex;
    flex-wrap: wrap;
    flex-shrink: 0;
    max-width: 100%;
    background: var(--color-bg-elevated-2);
    border: 1px solid var(--color-border-default);
    border-radius: var(--radius-md);
    padding: 3px;
    gap: 2px;
  }

  .segmented-btn {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 7px 16px;
    border: 1px solid transparent;
    border-radius: calc(var(--radius-md) - 2px);
    background: transparent;
    font-size: var(--text-sm);
    font-weight: var(--font-weight-medium);
    color: var(--color-foreground-subtle);
    cursor: pointer;
    white-space: nowrap;
    transition: background var(--duration-fast), color var(--duration-fast), border-color var(--duration-fast);
  }

  .segmented-btn svg {
    width: 16px;
    height: 16px;
    stroke: currentColor;
    fill: none;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
  }

  .segmented-btn:hover:not(.active) {
    color: var(--color-foreground-body);
    background: var(--color-foreground-a03);
  }

  /* Default active */
  .segmented-btn.active {
    background: var(--color-bg-elevated-3);
    color: var(--color-foreground);
    font-weight: var(--font-weight-semibold);
    border-color: var(--color-border-strong);
  }

  /* Accent active */
  .segmented-control.accent .segmented-btn.active {
    background: var(--color-accent-a20);
    color: var(--color-accent-300);
    border-color: var(--color-accent-400);
  }

  .segmented-btn:focus-visible {
    outline: none;
    box-shadow: inset 0 0 0 2px var(--color-accent-400);
  }

  .segmented-btn:disabled {
    opacity: 0.35;
    cursor: not-allowed;
    pointer-events: none;
  }

  /* ===== ACCORDION (EXTENDED) ===== */
  .accordion-body-inner {
    padding: 0 20px 16px;
    color: var(--color-foreground-body);
    font-size: var(--text-sm);
    line-height: 1.6;
  }

  /* ===== DIVIDER (EXTENDED) ===== */
  .divider-compact  { margin: 8px 0; }
  .divider-spacious { margin: 32px 0; }

  /* Centered label */
  .divider-label {
    display: flex;
    align-items: center;
    margin: 16px 0;
    font-size: var(--text-xs);
    color: var(--color-foreground-subtle);
    font-weight: var(--font-weight-semibold);
  }

  .divider-label::before,
  .divider-label::after {
    content: '';
    flex: 1;
    height: 1px;
    background: var(--color-border-default);
  }

  .divider-label::before { margin-right: 16px; }
  .divider-label::after  { margin-left: 16px; }

  /* Left-aligned label */
  .divider-label-left {
    display: flex;
    align-items: center;
    margin: 16px 0;
    font-size: var(--text-xs);
    color: var(--color-foreground-subtle);
    font-weight: var(--font-weight-semibold);
  }

  .divider-label-left::after {
    content: '';
    flex: 1;
    height: 1px;
    background: var(--color-border-default);
    margin-left: 12px;
  }

  /* Spacing on a labelled divider (#98).
     .divider-label / -left set their own `margin: 16px 0`, and at equal specificity
     they sit LATER in this file than .divider-compact / .divider-spacious — so
     `<Divider label="…" spacing="compact" />` would silently lose the cascade and
     do nothing. A combined selector wins on specificity instead of source order, so
     a future reshuffle of this stylesheet cannot quietly resurrect the bug. */
  .divider-label.divider-compact,
  .divider-label-left.divider-compact { margin: 8px 0; }
  .divider-label.divider-spacious,
  .divider-label-left.divider-spacious { margin: 32px 0; }

  /* ===== ERROR PAGE ===== */
  .error-page {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--spacing-8);
  }

  .error-page__content {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    max-width: 480px;
    gap: 16px;
  }

  /* 404 ghost number */
  .error-page__number {
    font-size: clamp(80px, 15vw, 160px);
    font-weight: var(--font-weight-extrabold);
    letter-spacing: -2px;
    line-height: 1;
    background: linear-gradient(135deg, var(--color-neutral-350), var(--color-neutral-600));
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
    margin-bottom: 8px;
    user-select: none;
  }

  .error-page__title {
    font-size: var(--text-2xl);
    font-weight: var(--font-weight-extrabold);
    color: var(--color-foreground);
    letter-spacing: var(--font-letter-spacing-tight);
  }

  .error-page__desc {
    font-size: var(--text-base);
    color: var(--color-foreground-body);
    line-height: 1.6;
    max-width: 420px;
  }

  .error-page__actions {
    display: flex;
    gap: 12px;
    margin-top: 8px;
  }

  /* 500 icon container */
  .error-page__icon-container {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: var(--spacing-6);
    background: color-mix(in srgb, var(--color-error) 8%, transparent);
    border: 1px solid var(--color-error-bg);
    border-radius: var(--radius-lg);
    margin-bottom: 8px;
  }

  /* ===== RICH TEXT EDITOR ===== */
  .rte-container {
    background: var(--color-bg-elevated-1);
    border: 1px solid var(--color-border-default);
    border-radius: var(--rich-text-editor-radius);
    overflow: hidden;
  }

  .rte-toolbar {
    display: flex;
    align-items: center;
    gap: 2px;
    flex-wrap: wrap;
    padding: 8px 12px;
    background: var(--rich-text-editor-toolbar-bg);
    border-bottom: 1px solid var(--rich-text-editor-toolbar-border);
  }

  .rte-toolbar-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--rich-text-editor-btn-size);
    height: var(--rich-text-editor-btn-size);
    border: none;
    border-radius: var(--radius-md);
    background: transparent;
    color: var(--color-foreground-subtle);
    cursor: pointer;
    transition: background var(--duration-fast), color var(--duration-fast);
  }

  .rte-toolbar-btn svg {
    width: 16px;
    height: 16px;
    stroke: currentColor;
    fill: none;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
  }

  .rte-toolbar-btn:hover { background: var(--color-bg-elevated-3); color: var(--color-foreground); }
  .rte-toolbar-btn.active { background: var(--color-accent-a10); color: var(--color-accent-300); }
  .rte-toolbar-btn:disabled { opacity: 0.35; cursor: not-allowed; }

  .rte-toolbar-divider {
    width: 1px;
    height: 20px;
    background: var(--color-border-default);
    margin: 0 6px;
    align-self: center;
    flex-shrink: 0;
  }

  .rte-body {
    padding: 20px 24px;
    min-height: 200px;
    color: var(--color-foreground-body);
    font-size: var(--text-base);
    line-height: 1.6;
    outline: none;
    /* The editor area is its own surface. Without this it inherits .rte-container's
       elevated-1 and reads as one flat panel with its own toolbar — which is why a
       separate --rich-text-editor-editor-bg token exists, defined and referenced by
       nothing since it shipped (#163). */
    background: var(--rich-text-editor-editor-bg);
  }

  .rte-body h2 { color: var(--color-foreground); font-size: var(--text-xl); font-weight: var(--font-weight-semibold); margin-bottom: 12px; }
  /* Block flow inside the editor. Without these, a reset (Tailwind preflight strips
     list-style and margins) leaves the editor unable to render the very content its
     spec documents — the markup example there contains a <ul>, and its bullets and
     indent simply vanished. Found when the guide demo stopped hand-rolling
     `list-disc pl-6` and started using .rte-body for real. */
  .rte-body p { margin-bottom: 12px; }
  .rte-body ul,
  .rte-body ol { padding-left: 24px; margin-bottom: 12px; }
  .rte-body ul { list-style: disc; }
  .rte-body ol { list-style: decimal; }
  .rte-body li { margin-bottom: 4px; }
  .rte-body > :last-child { margin-bottom: 0; }
  .rte-body code {
    background: var(--color-accent-a10);
    color: var(--color-accent-300);
    font-family: var(--font-family-mono);
    font-size: var(--text-xs);
    border-radius: var(--radius-sm);
    padding: 2px 6px;
  }

  .rte-footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 8px 16px;
    background: var(--rich-text-editor-toolbar-bg);
    border-top: 1px solid var(--rich-text-editor-toolbar-border);
  }

  .rte-char-count { font-size: var(--text-xs); color: var(--color-foreground-subtle); }
  .rte-toggle-label { display: flex; align-items: center; gap: 8px; font-size: var(--text-xs); color: var(--color-foreground-subtle); }

  /* ===== CHAT MESSAGES ===== */

  /* Message list container — flex column with consistent gap */
  .chat-msg-list {
    display: flex;
    flex-direction: column;
    gap: var(--chat-msg-gap);
  }

  /* Alignment wrappers — consumers wrap each message to control direction */
  .chat-msg-wrapper { display: flex; }
  .chat-msg-wrapper-sent { justify-content: flex-end; }
  .chat-msg-wrapper-received { justify-content: flex-start; }

  /* Base message */
  .chat-msg {
    padding: var(--chat-msg-padding-y) var(--chat-msg-padding-x);
    font-size: var(--chat-msg-font-size);
    line-height: 1.6;
    max-width: var(--chat-msg-max-width);
    border-radius: 1rem;
    word-break: break-word;
  }

  /* Sent — accent tint, flat bottom-right corner */
  .chat-msg-sent {
    background: linear-gradient(135deg, var(--color-accent-a10), var(--color-accent-a04));
    border: 1px solid var(--color-accent-a15);
    border-radius: 1rem 1rem 0.375rem 1rem;
    color: var(--color-foreground);
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
  }

  /* Received — neutral, flat bottom-left corner */
  .chat-msg-received {
    background: linear-gradient(135deg, color-mix(in srgb, var(--color-foreground-body) 6%, transparent), color-mix(in srgb, var(--color-foreground-body) 2%, transparent));
    border: 1px solid var(--color-border-subtle);
    border-radius: 1rem 1rem 1rem 0.375rem;
    color: var(--color-foreground-body);
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
  }

  /* System — centered, muted, no bubble */
  .chat-msg-system {
    background: none;
    border: none;
    border-radius: 0;
    box-shadow: none;
    font-size: var(--chat-msg-font-size-system);
    color: var(--color-foreground-subtle);
    text-align: center;
    max-width: 100%;
    align-self: center;
    padding: var(--chat-msg-padding-y) 0;
  }

  /* Tool — left accent border, amber tint */
  .chat-msg-tool {
    background: color-mix(in srgb, var(--color-warning) 4%, transparent);
    border: 1px solid color-mix(in srgb, var(--color-warning) 8%, transparent);
    border-left: var(--chat-msg-tool-border) solid color-mix(in srgb, var(--color-warning) 30%, transparent);
    border-radius: 0 0.5rem 0.5rem 0;
    color: var(--color-foreground-subtle);
    font-size: var(--chat-msg-font-size-system);
    max-width: var(--chat-msg-max-width);
    box-shadow: none;
  }
  .chat-msg-tool strong {
    color: var(--color-warning);
    font-weight: var(--font-weight-medium);
  }

  /* ===== CHAT INPUT ===== */

  /* Wrapper — card-like container */
  .chat-input {
    background: var(--chat-input-bg);
    border: 1px solid var(--chat-input-border);
    border-radius: var(--chat-input-radius);
    padding: var(--chat-input-padding);
    box-shadow: var(--shadow-lg);
    transition: border-color var(--duration-fast), box-shadow var(--duration-fast);
  }

  /* Focus-within — accent ring on the whole container */
  .chat-input:focus-within {
    border-color: var(--color-accent-a15);
    box-shadow: 0 0 0 3px var(--color-accent-a08), var(--shadow-lg);
  }

  /* Streaming state — subtle visual change */
  .chat-input-streaming {
    border-color: color-mix(in srgb, var(--color-error) 20%, var(--chat-input-border));
  }

  /* Textarea — transparent, no border, auto-resize */
  .chat-input-textarea {
    display: block;
    width: 100%;
    background: transparent;
    border: none;
    outline: none;
    resize: none;
    color: var(--color-foreground);
    font-size: var(--chat-input-font-size);
    font-family: inherit;
    line-height: 1.6;
    max-height: var(--chat-input-max-height);
    overflow-y: auto;
  }
  .chat-input-textarea::placeholder {
    color: var(--color-foreground-disabled);
  }
  .chat-input-textarea:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }

  /* Footer row — border-top separator, flex row for count + buttons */
  .chat-input-footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-top: 0.5rem;
    padding-top: 0.5rem;
    border-top: 1px solid var(--color-border-subtle);
  }

  /* Character count */
  .chat-input-count {
    font-size: var(--text-xs);
    color: var(--color-foreground-disabled);
    font-variant-numeric: tabular-nums;
  }

  /* Action buttons container */
  .chat-input-actions {
    display: flex;
    align-items: center;
    gap: 0.5rem;
  }

  /* Send button — accent gradient, dark text */
  .chat-input-send {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    background: linear-gradient(135deg, var(--color-accent-300), var(--color-accent-500));
    color: var(--chat-input-send-fg);
    padding: 0.5rem 1rem;
    border-radius: var(--radius-lg);
    font-size: var(--text-xs);
    font-weight: var(--font-weight-bold);
    letter-spacing: 0.05em;
    border: none;
    cursor: pointer;
    transition: opacity var(--duration-fast), transform var(--duration-fast);
  }
  .chat-input-send svg { width: 16px; height: 16px; }
  .chat-input-send:hover { transform: scale(1.02); }
  .chat-input-send:active { transform: scale(0.98); }
  .chat-input-send:disabled {
    opacity: 0.4;
    cursor: not-allowed;
    transform: none;
  }

  /* Stop button — error tint, pulsing */
  .chat-input-stop {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    background: color-mix(in srgb, var(--color-error) 10%, transparent);
    color: var(--color-error);
    padding: 0.5rem 1rem;
    border-radius: var(--radius-lg);
    font-size: var(--text-xs);
    font-weight: var(--font-weight-bold);
    letter-spacing: 0.05em;
    border: 1px solid color-mix(in srgb, var(--color-error) 20%, transparent);
    cursor: pointer;
    animation: chat-input-pulse 1.5s ease-in-out infinite;
  }
  .chat-input-stop svg { width: 16px; height: 16px; }
  .chat-input-stop:hover {
    background: color-mix(in srgb, var(--color-error) 15%, transparent);
  }

  @keyframes chat-input-pulse {
    0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-error) 20%, transparent); }
    50%      { box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-error) 8%, transparent); }
  }

  /* ===== SIDE PANEL ===== */

  /* Base — flex column, full height, fixed width
     Override width: set --side-panel-width on the element or a parent */
  .side-panel {
    width: var(--side-panel-width);
    flex-shrink: 0;
    background: var(--side-panel-bg);
    display: flex;
    flex-direction: column;
    height: 100%;
  }

  /* Docking variants */
  .side-panel-right {
    border-left: 1px solid var(--side-panel-border);
  }
  .side-panel-left {
    border-right: 1px solid var(--side-panel-border);
  }

  /* Header — flex row with gap */
  .side-panel-header {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: var(--side-panel-header-padding-y) var(--side-panel-header-padding-x);
    border-bottom: 1px solid var(--side-panel-border);
    flex-shrink: 0;
  }

  /* Scrollable content area */
  .side-panel-body {
    flex: 1;
    overflow-y: auto;
    padding: var(--side-panel-body-padding-y) var(--side-panel-body-padding-x);
    min-height: 0;
    /* Firefox — the ::-webkit- rules below don't reach it. */
    scrollbar-width: thin;
    scrollbar-color: var(--color-neutral-800) transparent;
  }
  .side-panel-body::-webkit-scrollbar { width: 4px; }
  .side-panel-body::-webkit-scrollbar-track { background: transparent; }
  .side-panel-body::-webkit-scrollbar-thumb { background: var(--color-neutral-800); border-radius: var(--radius-full); }
  .side-panel-body::-webkit-scrollbar-thumb:hover { background: var(--color-neutral-900); }

  /* Pinned footer */
  .side-panel-footer {
    padding: var(--side-panel-footer-padding-y) var(--side-panel-footer-padding-x);
    border-top: 1px solid var(--side-panel-border);
    flex-shrink: 0;
  }

  /* Backdrop — separate from sidebar-backdrop for independent layering */
  .side-panel-backdrop {
    position: fixed;
    inset: 0;
    z-index: var(--z-backdrop);
    background: var(--side-panel-backdrop);
    backdrop-filter: blur(var(--side-panel-blur));
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--duration-slow) var(--easing-out);
  }
  .side-panel-backdrop.open { opacity: 1; pointer-events: auto; }

  /* React (Radix) path (#114) — the SidePanel overlay is portalled to <body> and is
     ALWAYS an overlay (data-overlay), so it takes the fixed/fullscreen treatment at any
     viewport rather than the base static column. Driven by data-state with keyframes
     (Radix Presence keeps it mounted through the exit only for a CSS animation, not a
     transition) — the same model as Sidebar.Drawer (#95). */
  .side-panel[data-overlay] {
    position: fixed;
    inset: 0;
    width: 100%;
    z-index: var(--z-drawer);
    visibility: visible;
  }
  .side-panel[data-overlay][data-state="open"] {
    animation: side-panel-in var(--duration-slow) var(--easing-out);
  }
  .side-panel[data-overlay][data-state="closed"] {
    animation: side-panel-out var(--duration-slow) var(--easing-out);
  }
  .side-panel-left[data-overlay][data-state="open"] {
    animation: side-panel-left-in var(--duration-slow) var(--easing-out);
  }
  .side-panel-left[data-overlay][data-state="closed"] {
    animation: side-panel-left-out var(--duration-slow) var(--easing-out);
  }
  @keyframes side-panel-in {
    from { transform: translateX(100%); }
    to   { transform: translateX(0); }
  }
  @keyframes side-panel-out {
    from { transform: translateX(0); }
    to   { transform: translateX(100%); }
  }
  @keyframes side-panel-left-in {
    from { transform: translateX(-100%); }
    to   { transform: translateX(0); }
  }
  @keyframes side-panel-left-out {
    from { transform: translateX(0); }
    to   { transform: translateX(-100%); }
  }
  .side-panel-backdrop[data-overlay][data-state="open"] {
    opacity: 1;
    pointer-events: auto;
    animation: side-panel-backdrop-in var(--duration-slow) var(--easing-out);
  }
  .side-panel-backdrop[data-overlay][data-state="closed"] {
    animation: side-panel-backdrop-out var(--duration-slow) var(--easing-out);
  }
  @keyframes side-panel-backdrop-in { from { opacity: 0; } to { opacity: 1; } }
  @keyframes side-panel-backdrop-out { from { opacity: 1; } to { opacity: 0; } }
  @media (prefers-reduced-motion: reduce) {
    .side-panel[data-overlay][data-state],
    .side-panel-backdrop[data-overlay][data-state] { animation: none; }
  }

  /* Native <dialog> side panel — the RECOMMENDED vanilla (HTML / PHP / Python)
     on-demand panel, and the twin of dialog.sidebar-drawer (#99). showModal() gives
     focus containment, Esc-to-close, focus-restore-to-trigger, an inert background,
     and top-layer stacking for free.

     Viewport-independent by design: this is the below-xl half of the dock-at-xl rail
     pattern (specs/components/side-panel.md) — a docked <aside class="side-panel …">
     shown by a consumer utility at xl, plus this dialog for every width under it.
     [data-overlay] is NOT the vanilla answer: it has no closed state without Radix's
     data-state, so a hand-stamped one covers the viewport permanently.

     No slide-in, for the same reason the sidebar dialog has none (see :1629): a
     transform entry animation can stick at its off-screen START value, and an
     unreachable panel is a worse failure than an unanimated one. */
  dialog.side-panel {
    position: fixed;
    inset: 0 0 0 auto;
    width: var(--side-panel-width);
    max-width: 100%;
    /* height:100% is load-bearing — the UA gives a dialog fit-content height, which
       otherwise beats inset's top/bottom:0 and yields a stubby floating box. */
    height: 100%;
    max-height: none;
    margin: 0;
    padding: 0;
    border: none;
    border-left: 1px solid var(--side-panel-border);
    /* Neutralise the class-toggle base: a closed <dialog> leaves layout on its own. */
    transform: none;
    visibility: visible;
  }
  /* The base .side-panel sets display:flex, and an author rule beats the UA's
     dialog:not([open]) { display: none } — so the closed state must be restored
     explicitly. dialog.sidebar-drawer needs no such rule: its base sets no display. */
  dialog.side-panel:not([open]) { display: none; }
  dialog.side-panel.side-panel-left {
    inset: 0 auto 0 0;
    border-left: none;
    border-right: 1px solid var(--side-panel-border);
  }
  dialog.side-panel::backdrop {
    background: var(--side-panel-backdrop);
  }

  /* ===== CHIP EXTENSIONS ===== */

  /* Small variant — for compact bars and context indicators */
  .chip-sm {
    padding: 3px 8px;
    font-size: var(--text-xs);
    gap: 4px;
    border-radius: 6px;
  }
  .chip-sm .chip-close {
    width: 14px;
    height: 14px;
  }

  /* Status color variants — matches .badge-* color family */
  .chip-success {
    background: color-mix(in srgb, var(--color-success) 15%, transparent);
    color: var(--color-emerald-300);
  }
  .chip-warning {
    background: color-mix(in srgb, var(--color-warning) 15%, transparent);
    color: var(--color-amber-300);
  }
  .chip-error {
    background: color-mix(in srgb, var(--color-error) 15%, transparent);
    color: var(--color-crimson-300);
  }
  .chip-info {
    background: color-mix(in srgb, var(--color-info) 15%, transparent);
    color: var(--color-blue-400);
  }

  /* Chip bar — container for grouped chips with optional add button.
     No padding of its own (#174). It used to carry `padding: 0.5rem`, which was
     defensible standing alone and wrong everywhere it actually gets used: both
     real consumers nest it in something already inset — a Card.Body, a fieldset —
     so the two stacked and the chip row sat 8px right of every sibling sharing
     that left edge. The guide had already worked around it with an inline
     `padding:0`, which is the tell.
     Same call as .sidebar-footer in #171: a container that only arranges its
     children does not get to decide their inset. A chip bar that genuinely needs
     breathing room is inside something whose job that is. */
  .chip-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
  }

  /* Add button — dashed border pill at end of chip bar */
  .chip-bar-add {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 3px 8px;
    border-radius: 6px;
    border: 1px dashed var(--color-foreground-ghost);
    background: none;
    color: var(--color-foreground-disabled);
    font-size: var(--text-xs);
    font-weight: var(--font-weight-medium);
    cursor: pointer;
    transition: border-color var(--duration-fast), color var(--duration-fast);
  }
  .chip-bar-add:hover {
    border-color: var(--color-foreground-subtle);
    color: var(--color-foreground-muted);
  }

  /* ===== TOGGLE CHIP (#151) =====
     A pill that REPORTS a binary state and FLIPS it. Backed by Radix Toggle in
     React, so it is a real <button> carrying aria-pressed and data-state.

     aria-pressed, not aria-checked: a two-state action, not a checkbox. A bare
     <button> labelled "Required" announces as a COMMAND — as though pressing it
     makes the thing required — rather than as the current state.

     tone names the ON colour. The OFF treatment is uniform and lives here, NOT
     at the call site. Before this component, Diannara's own examples swapped
     tone for off while the consumer's swapped tone AND variant — two visual
     languages for one control, because nothing owned the off-state. */
  .toggle-chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 4px 10px;
    border-radius: var(--radius-full);
    font-family: inherit;
    font-size: var(--text-xs);
    font-weight: var(--font-weight-medium);
    line-height: 1.25;
    white-space: nowrap;
    /* A <button> needs resets a .chip <span> does not. */
    background: none;
    border: 1px solid transparent;
    cursor: pointer;
    transition: background var(--duration-fast), box-shadow var(--duration-fast),
      color var(--duration-fast);
  }

  /* OFF — transparent fill with an inset ring, so "not pressed" reads as an
     outline rather than as a differently-coloured fill. Follows .badge-outline
     in using inset box-shadow rather than `border`, so the state change does
     not alter the element's box size and the pill cannot shift by a pixel as
     it toggles. */
  .toggle-chip[data-state="off"] {
    background: transparent;
    color: var(--color-foreground-muted);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, currentColor 40%, transparent);
  }
  .toggle-chip[data-state="off"]:hover {
    color: var(--color-foreground);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, currentColor 65%, transparent);
  }

  /* ON — the tone's fill. Same fills as the matching .chip-* tones, so a pressed
     ToggleChip and an equivalent Chip are the same object visually. */
  .toggle-chip[data-state="on"].toggle-chip-accent {
    background: var(--color-accent-a15);
    color: var(--color-accent-300);
  }
  .toggle-chip[data-state="on"].toggle-chip-accent:hover {
    background: var(--color-accent-a20);
  }
  .toggle-chip[data-state="on"].toggle-chip-neutral {
    background: var(--color-neutral-350);
    color: var(--color-foreground-muted);
  }
  .toggle-chip[data-state="on"].toggle-chip-success {
    background: color-mix(in srgb, var(--color-success) 15%, transparent);
    color: var(--color-emerald-300);
  }
  .toggle-chip[data-state="on"].toggle-chip-warning {
    background: color-mix(in srgb, var(--color-warning) 15%, transparent);
    color: var(--color-amber-300);
  }
  .toggle-chip[data-state="on"].toggle-chip-error {
    background: color-mix(in srgb, var(--color-error) 15%, transparent);
    color: var(--color-crimson-300);
  }
  .toggle-chip[data-state="on"].toggle-chip-info {
    background: color-mix(in srgb, var(--color-info) 15%, transparent);
    color: var(--color-blue-400);
  }

  /* ON with an arbitrary user colour (#158) — a group/belt hex from the DB, not a
     tone. Same fill and same derived foreground as .chip-color, so a pressed
     ToggleChip and an equivalent Chip stay the same object visually, and a
     consumer migrating off a `Chip asChild` + `<button aria-pressed>` hand-roll
     keeps the `--chip-color` custom property they already set.

     toggle-chip.md parked this as "deciding how a user hex interacts with a
     pressed fill, for a case nobody has yet". The case arrived, and it dissolves
     the question rather than answering it: the hex participates ONLY in the
     pressed fill, so OFF is untouched — still transparent with the 40%
     currentColor ring, which is exactly the "default bordered pill" an unselected
     pill wants. There is no interaction to arbitrate.

     Deliberately no `tint` counterpart. Chip needs one because it has no off
     state to carry the lighter-weight reading; here the off state already IS the
     outline treatment, so a tint variant would be a second vocabulary for the
     same thing — the drift this component exists to prevent. */
  .toggle-chip[data-state="on"].toggle-chip-color {
    background: var(--chip-color, var(--color-neutral-500));
    color: oklch(from var(--chip-color, var(--color-neutral-500)) clamp(0, (0.62 - l) * 1000, 1) 0 0);
  }

  /* accessibility.md's ":focus-visible on every interactive element" Do rule
     requires a visible focus indicator. The Badge-asChild hand-roll this
     replaces had none of its own — it inherited whatever the UA gave a bare
     <button>. */
  .toggle-chip:focus-visible {
    outline: none;
    box-shadow: var(--shadow-accent-focus);
  }
  .toggle-chip[data-state="off"]:focus-visible {
    box-shadow: inset 0 0 0 1px color-mix(in srgb, currentColor 40%, transparent),
      var(--shadow-accent-focus);
  }
  /* Hover and focus-visible are equal specificity here, so without this the
     later-declared rule always wins outright — an off chip that is hovered
     AND focused would drop from hover's 65% ring back to focus-visible's 40%
     the instant it gained focus, making the ring fainter while more attention
     states are active. This compound selector is more specific than either
     single-state rule, so it wins regardless of declaration order and keeps
     the stronger hover ring alongside the accent focus ring. */
  .toggle-chip[data-state="off"]:hover:focus-visible {
    box-shadow: inset 0 0 0 1px color-mix(in srgb, currentColor 65%, transparent),
      var(--shadow-accent-focus);
  }

  .toggle-chip:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }

  /* Compact variant, mirroring .chip-sm. Font size still comes from a token and
     never drops below 14px. */
  .toggle-chip-sm {
    padding: 3px 8px;
    border-radius: var(--radius-md);
  }

  /* ===== LIST SIDEBAR ===== */

  /* Container — fixed width, full height, flex column */
  .list-sidebar {
    width: var(--list-sidebar-width);
    height: 100%;
    background: var(--list-sidebar-bg);
    border-right: 1px solid var(--list-sidebar-border);
    display: flex;
    flex-direction: column;
    flex-shrink: 0;
  }

  /* Header — title + action button */
  .list-sidebar-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 1rem;
    border-bottom: 1px solid var(--list-sidebar-border);
    flex-shrink: 0;
  }

  /* Scrollable item list */
  .list-sidebar-list {
    flex: 1;
    overflow-y: auto;
    padding: 0.5rem;
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-height: 0;
    /* Firefox — the ::-webkit- rules below don't reach it. */
    scrollbar-width: thin;
    scrollbar-color: var(--color-neutral-800) transparent;
  }
  .list-sidebar-list::-webkit-scrollbar { width: 4px; }
  .list-sidebar-list::-webkit-scrollbar-track { background: transparent; }
  .list-sidebar-list::-webkit-scrollbar-thumb { background: var(--color-neutral-800); border-radius: var(--radius-full); }
  .list-sidebar-list::-webkit-scrollbar-thumb:hover { background: var(--color-neutral-900); }

  /* Group label — matches .sidebar-group-label pattern */
  .list-sidebar-group {
    font-size: var(--list-sidebar-group-font-size);
    font-weight: var(--font-weight-semibold);
    color: var(--color-foreground-disabled);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    padding: 0.75rem 0.75rem 0.375rem;
  }

  /* Individual item — two-line layout (title + meta) */
  .list-sidebar-item {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: var(--list-sidebar-item-padding-y) var(--list-sidebar-item-padding-x);
    border-radius: var(--list-sidebar-item-radius);
    cursor: pointer;
    transition: background var(--duration-fast);
    position: relative;
  }
  .list-sidebar-item:hover {
    background: var(--color-foreground-a03);
  }

  /* Item title — single line, ellipsis */
  .list-sidebar-item-title {
    font-size: var(--list-sidebar-item-font-size);
    color: var(--color-foreground-body);
    font-weight: var(--font-weight-medium);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  /* ----- Edit slot (#150) -----
     The in-place-edit counterpart to .list-sidebar-item-title. Takes the same track,
     so a row that swaps into an edit state reuses the row shell — same padding, same
     radius, same surface — instead of being rebuilt from tokens at the call site.

     Deliberately NO overflow: hidden. That single declaration is the entire difference
     from .list-sidebar-item-title, whose ellipsis overflow clips an input's focus ring:
     the one thing a field being edited must show.

     Named in the :has() trigger above so an edit row carrying ONLY this slot still gets
     the grid rather than falling back to the flex column — otherwise the two states of
     one row would use two different layout models. */
  .list-sidebar-item-edit {
    grid-column: 2;
    /* Row 1, not spanning — same reason as leading / trailing / controls: a spanning
       item distributes its height across every track it spans and manufactures an
       implicit second row. */
    grid-row: 1;
    /* A grid item floors at min-content, which would push the track past the container.
       This slot holds a field rather than text, so nothing else resolves it to 0. */
    min-width: 0;
  }

  /* Reaching into the child is the point, not an overreach. A default .input is
     var(--spacing-3) x2 + 2px border + a 24px line box (50px) — the line box is 24px,
     not the ~18px a single-line assumption would suggest, because .input sets no
     line-height and inherits the document's normal one, while .btn sets line-height: 1
     explicitly. The tallest thing in a display row is a .btn-sm at 32px. Left alone, the
     "correct" slot would make the row ~18px TALLER in edit state than in display state —
     a bigger version of the 6px drift that motivated #150.

     Precedent: .list-sidebar-item-leading svg sizes its own child for the same reason —
     a slot whose job is fitting content to a row needs a say in that content's box. */
  .list-sidebar-item-edit .input {
    padding-block: var(--list-sidebar-item-edit-padding-y);
  }

  /* Item meta — small, muted */
  .list-sidebar-item-meta {
    font-size: var(--list-sidebar-item-meta-font-size);
    color: var(--color-foreground-disabled);
    display: flex;
    align-items: center;
    gap: 0.375rem;
  }

  /* Active/selected state */
  .list-sidebar-item-active {
    background: var(--color-accent-a08);
  }
  .list-sidebar-item-active .list-sidebar-item-title {
    color: var(--color-foreground);
  }
  .list-sidebar-item-active .list-sidebar-item-meta {
    color: var(--color-foreground-subtle);
  }

  /* ----- Leading / trailing slots (#141) -----
     The row grows a media column and a trailing column, so it can be the conventional
     list-row anatomy instead of a title/meta stack only. Before this, a row that needed
     a swatch, avatar, file-type icon or status glyph had nowhere to put it — a third
     child stacked BELOW the title — and the spec's own stated use cases (conversations,
     documents, files) all conventionally carry one.

     Opt-in via :has(), so a row with neither slot keeps the flex column above, byte for
     byte. Every consumer shipped before this renders identically.

     A row with no .list-sidebar-item-meta is a ONE-LINE row, which is the shape the
     master-detail lists that prompted this actually use. That falls out of the grid
     rather than needing a variant. */
  .list-sidebar-item:has(
      .list-sidebar-item-leading,
      .list-sidebar-item-trailing,
      .list-sidebar-item-controls,
      .list-sidebar-item-edit
    ) {
    display: grid;
    /* Both outer tracks are `auto` and collapse to zero when their slot is absent, so
       ONE rule covers leading-only, trailing-only and both. The column gaps live on the
       slots themselves (margins below) rather than on `column-gap` for exactly that
       reason: a `column-gap` here would also apply to the collapsed track and indent the
       title by a gap-width with no leading slot present, or leave dead space on the right
       with no trailing one. Looks like it simplifies to column-gap. It does not. */
    grid-template-columns: auto 1fr auto auto;
    row-gap: 2px;
  }
  .list-sidebar-item-leading {
    grid-column: 1;
    /* Row 1, NOT spanning. Because the glyph aligns to the title's line (below), a span
       buys nothing visually — and it costs real layout: a spanning item distributes its
       height across every track it spans, so `1 / span 2` conjures an IMPLICIT second
       row onto a row that has no meta line to put in it. Re-measured after #161 moved
       the title to 17px: a title-only row is 49.19px on one track (27.19px), identical
       to the same row with the leading slot deleted; forcing `1 / span 2` grows it to
       53.78px on two tracks (27.19px + 2.59px), which with the 2px row-gap is the whole
       +4.59px. On a row that DOES carry a meta line the span changes nothing (71.58px
       either way) — there is a real second track there. Alignment is identical in every
       case: slot and title share a top edge to 0.0px whether it spans or not. */
    grid-row: 1;
    width: var(--list-sidebar-item-leading-size);
    margin-right: var(--spacing-3);
    display: flex;
    align-items: center;
    justify-content: center;
    /* Align the glyph to the TITLE's line rather than to the centre of a two-line row,
       so it stays beside the name however far a long meta wraps.
       `font-size` is what keeps `1lh` honest: 1lh resolves against the font-size in
       EFFECT, and the title reads --list-sidebar-item-font-size while this slot would
       otherwise read whatever the host's ambient size is. Re-measured after #161 moved
       the title to 17px: on a host at 17px the declaration changes nothing (slot and
       title both 27.2px), but on a host at the browser-default 16px, deleting it drops
       the slot to 25.6px against the title's 27.2px — 1.6px of drift, with the glyph
       riding HIGH of the name rather than low. So it is defensive here and load-bearing
       in a consumer whose body size is not ours, which is the case worth surviving. */
    align-self: start;
    font-size: var(--list-sidebar-item-font-size);
    height: 1lh;
    color: var(--color-foreground-subtle);
  }
  .list-sidebar-item-leading svg { width: 16px; height: 16px; flex-shrink: 0; }
  /* Static trailing content — a count Badge, a timestamp, a status pill. Named to match
     .dropdown-item-trailing rather than coining a third word for the same idea.
     NOT the same thing as .list-sidebar-item-action below, which is a hover-revealed
     action: a count is neither an action nor hover-discoverable. The distinction is what
     it IS, not what it renders — ItemAction takes asChild and is routinely an <a>. */
  .list-sidebar-item-trailing {
    grid-column: 3;
    /* Row 1 by default, for the same reason as the leading slot: a span would inflate a
       one-line row by forcing an implicit second track. */
    grid-row: 1;
    align-self: center;
    margin-left: var(--spacing-3);
    display: flex;
    align-items: center;
    flex-shrink: 0;
  }
  /* Unlike the leading glyph — which is pinned to the title's line — trailing content is
     a ROW-level marker, so it centres against both lines when there are two. Gated on the
     meta actually existing, so the span never manufactures a second row.

     `1 / span 2` and NOT `1 / -1`: these rows are implicit, and -1 resolves to the last
     EXPLICIT grid line. Measured with -1: the span collapses to row 1 and the meta
     auto-places into column 1, underneath the leading glyph rather than beside the title. */
  .list-sidebar-item:has(.list-sidebar-item-meta) .list-sidebar-item-trailing {
    grid-row: 1 / span 2;
  }
  /* ----- Persistent controls slot (#144) -----
     A cluster of always-visible interactive controls at the far edge — an edit button, a
     required/optional toggle, a remove button. Distinct from both neighbours on purpose:
     .list-sidebar-item-trailing is STATIC content, .list-sidebar-item-action is a single
     HOVER-REVEALED affordance in the corner. Neither can describe a control that reports
     state, mutates it, and must stay visible without one of its own defining words
     becoming false.

     Named -controls and not -actions: one character from -action would be the worst
     possible pair of names for two slots whose entire distinction is disclosure. */
  /* Mutually exclusive with .list-sidebar-item-action, which is absolutely positioned at
     top/right 0.5rem and therefore sits ON TOP of this cluster's right end. Measured with
     both on one row: 16px x 18px of overlap, the action landing over the last control.
     CSS cannot police this without hiding one of them, which would be worse than the
     overlap, so it is a contract — see list-sidebar.md. A row either has a hover-revealed
     corner action or a persistent control cluster, not both. */
  .list-sidebar-item-controls {
    grid-column: 4;
    /* Row 1, not spanning — same reason as leading and trailing. A spanning item
       distributes its height across every track it spans, which manufactures an implicit
       second row on a title-only row. Measured below. */
    grid-row: 1;
    align-self: center;
    margin-left: var(--spacing-3);
    display: flex;
    align-items: center;
    gap: var(--spacing-1);
    flex-shrink: 0;
  }
  /* Controls are a ROW-level cluster, like trailing rather than like the leading glyph, so
     they centre across both lines when a meta line exists. Gated on the meta actually
     existing so the span never manufactures a second row. */
  .list-sidebar-item:has(.list-sidebar-item-meta) .list-sidebar-item-controls {
    grid-row: 1 / span 2;
  }

  .list-sidebar-item:has(
      .list-sidebar-item-leading,
      .list-sidebar-item-trailing,
      .list-sidebar-item-controls
    )
    > .list-sidebar-item-title,
  .list-sidebar-item:has(
      .list-sidebar-item-leading,
      .list-sidebar-item-trailing,
      .list-sidebar-item-controls
    )
    > .list-sidebar-item-meta {
    grid-column: 2;
    /* A grid item's `min-width: auto` floors it at min-content, which would push the
       track past the container instead of ellipsising. The title self-resolves to 0
       because it sets `overflow: hidden`; the meta is a bare flex row and does not. */
    min-width: 0;
  }

  /* ----- Bordered surface (#141) -----
     Opt-in, because adding a border to the base row would restyle every consumer already
     shipping. For rows that read as discrete objects rather than entries in a flush list
     — most obviously a drag-to-reorder list, where each row is something you pick up. */
  .list-sidebar-item-surface {
    border: 1px solid var(--color-border-default);
    background: var(--color-bg-surface);
    transition: background var(--duration-fast), border-color var(--duration-fast);
  }
  .list-sidebar-item-surface:hover { border-color: var(--color-accent-a25); }
  /* (0,2,0) on purpose. .list-sidebar-item-active and .list-sidebar-item-surface are both
     (0,1,0) and both set `background`, so which one won would come down to source order —
     and the selected row silently losing its accent tint is not something a border change
     should be able to cause. Stating the combination outright makes it order-proof. */
  .list-sidebar-item-surface.list-sidebar-item-active {
    background: var(--color-accent-a08);
    border-color: var(--color-accent-400);
  }

  /* ----- Size variant (issue 152) -----
     Every part of the row already reads its scale from a token, so the variant is
     one class on the ROOT that re-points those tokens — no per-part rules, and the
     leading slot and padding scale together as a unit by construction.

     Used to re-point the title size too: the rail this component was built for was
     --text-xs, and this variant needed --text-base for a standalone full-width
     drag-to-reorder list's row title. #161 moved list-sidebar's item-font-size to
     --text-base as the default for every row, rail included, so that re-point now
     restated the default and was removed. What's left — the leading slot and the
     padding — is what the rail's smaller row still needs scaled up to read as the
     same row on this variant.

     The class applies at EITHER level — the root or the item. It only sets custom
     properties, and those resolve on the element that declares them and inherit down,
     so a row composed rootless (inside a consumer's own bordered surface, which is
     what this variant was written for) reaches it by carrying the class itself.
     Documented as root-only until issue 162, which is why the consumer it was written
     for could not use it.

     --list-sidebar-item-meta-font-size and --list-sidebar-group-font-size are
     deliberately NOT re-pointed here: both are already at the 14px floor, and a
     meta line matching its own title's size would flatten the row's hierarchy. */
  .list-sidebar-md {
    --list-sidebar-item-leading-size: 32px;
    --list-sidebar-item-padding-y: 12px;
  }

  /* Hover-reveal action button (delete, pin, etc.)
     Hidden with `opacity`, NOT `display: none` (#147). A `display: none` element is not
     rendered and therefore not in the tab order, so the action was unreachable by keyboard
     above 768px — there was no key sequence that got there.
     `:focus-within` on the row cannot fix that: it needs focus to already be inside the
     row, and the only thing it would reveal is the element that can't receive focus while
     it's hidden. Where the action is the row's only control, focus can never get in, so the
     rule never matches. It reads like a one-line fix and does nothing.
     `visibility: hidden` fails the same way — also removed from the tab order.
     `opacity: 0` keeps the element rendered and focusable, so tabbing reaches it and its
     own `:focus-visible` reveals it. The element is absolutely positioned, so this costs
     the row no layout either way. */
  .list-sidebar-item-action {
    position: absolute;
    top: 0.5rem;
    right: 0.5rem;
    width: 20px;
    height: 20px;
    border-radius: var(--radius-sm);
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0;
    /* Not clickable while invisible — the row's hover reveals it before a pointer can
       reach it, so this costs nothing and prevents a click on something unseen. */
    pointer-events: none;
    background: transparent;
    border: none;
    color: var(--color-foreground-disabled);
    cursor: pointer;
    transition: background var(--duration-fast), color var(--duration-fast),
      opacity var(--duration-fast);
  }
  /* `:focus` and not `:focus-visible`. `:focus-visible` exists to suppress focus RINGS on
     mouse clicks, and it matches on a browser heuristic about input modality. What is being
     revealed here is not a ring but the control itself, and "focused but still invisible" is
     precisely the bug this fixes — so any focus reveals it, whatever the modality.
     `:focus` is also a superset of `:focus-visible`, so nothing is lost.

     Note for anyone verifying this: `:focus` only matches while the DOCUMENT holds system
     focus, so an automated/headless page shows `document.activeElement === el` and yet
     `el.matches(':focus') === false`. Check the reveal by tabbing in a real focused window;
     the tab-order half is what's assertable programmatically. */
  .list-sidebar-item:hover .list-sidebar-item-action,
  .list-sidebar-item-action:focus {
    opacity: 1;
    pointer-events: auto;
  }
  .list-sidebar-item-action:hover {
    background: color-mix(in srgb, var(--color-error) 10%, transparent);
    color: var(--color-error);
  }
  .list-sidebar-item-action svg { width: 12px; height: 12px; }

  /* Footer — count or summary
     font-size was hardcoded at 0.6875rem (11px) with no token at all, below the system's
     14px floor — the same defect the meta line and group label had, missed in that pass and
     closed here (issue 152). Not re-pointed by .list-sidebar-md, same reasoning as meta and
     group: already at the floor, and scaling it with the title would flatten the hierarchy. */
  .list-sidebar-footer {
    padding: 0.75rem 1rem;
    border-top: 1px solid var(--list-sidebar-border);
    font-size: var(--list-sidebar-footer-font-size);
    color: var(--color-foreground-disabled);
    text-align: center;
    flex-shrink: 0;
  }

  /* ===== TOGGLE SWITCH ===== */

  /* Hidden checkbox drives the toggle via sibling selectors */
  .toggle {
    position: absolute;
    opacity: 0;
    width: 0;
    height: 0;
  }

  /* Visible track — pill-shaped */
  .toggle__track {
    display: inline-block;
    width: 44px;
    height: 24px;
    border-radius: var(--radius-full);
    background: var(--color-neutral-400);
    position: relative;
    cursor: pointer;
    transition: background var(--duration-normal);
    flex-shrink: 0;
  }

  /* Thumb — slides left/right */
  .toggle__thumb {
    position: absolute;
    top: 2px;
    left: 2px;
    width: 20px;
    height: 20px;
    border-radius: var(--radius-full);
    background: var(--color-foreground-muted);
    transition: transform var(--duration-normal), background var(--duration-normal), box-shadow var(--duration-normal);
  }

  /* Checked — accent track, white thumb */
  .toggle:checked ~ .toggle__track {
    background: linear-gradient(90deg, var(--color-accent-500), var(--color-accent-400));
  }
  .toggle:checked ~ .toggle__track .toggle__thumb {
    transform: translateX(20px);
    background: var(--color-on-accent);
    box-shadow: 0 0 8px var(--color-accent-a25);
  }

  /* Focus-visible ring on the track */
  .toggle:focus-visible ~ .toggle__track {
    box-shadow: 0 0 0 3px var(--color-accent-a15);
  }

  /* Disabled */
  .toggle:disabled ~ .toggle__track {
    opacity: 0.5;
    cursor: not-allowed;
  }

  /* Small variant — 36x20 track, 16px thumb */
  .toggle-sm ~ .toggle__track {
    width: 36px;
    height: 20px;
  }
  .toggle-sm ~ .toggle__track .toggle__thumb {
    width: 16px;
    height: 16px;
  }
  .toggle-sm:checked ~ .toggle__track .toggle__thumb {
    transform: translateX(16px);
  }

  /* Toggle label — wraps the whole control */
  .toggle-label {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    cursor: pointer;
    font-size: var(--text-sm);
    color: var(--color-foreground-body);
  }

  /* ===== STREAMING INDICATORS ===== */

  /* Cursor — blinking block appended via ::after */
  .streaming-cursor::after {
    content: '';
    display: inline-block;
    width: 7px;
    height: 17px;
    background: var(--color-accent-300);
    margin-left: 2px;
    vertical-align: text-bottom;
    animation: streaming-blink 0.8s infinite;
  }

  @keyframes streaming-blink {
    0%, 100% { opacity: 1; }
    50% { opacity: 0; }
  }

  /* Pulse — fading opacity for "thinking" states */
  .streaming-pulse {
    animation: streaming-pulse-anim 1.5s ease-in-out infinite;
  }

  @keyframes streaming-pulse-anim {
    0%, 100% { opacity: 0.5; }
    50% { opacity: 1; }
  }

  /* Dots — three bouncing circles */
  .streaming-dots {
    display: inline-flex;
    align-items: center;
    gap: 4px;
  }
  .streaming-dots::before,
  .streaming-dots::after,
  .streaming-dots span {
    content: '';
    display: block;
    width: 6px;
    height: 6px;
    border-radius: var(--radius-full);
    background: var(--color-foreground-subtle);
    animation: streaming-bounce 1.2s ease-in-out infinite;
  }
  .streaming-dots::after {
    animation-delay: 0.2s;
  }
  .streaming-dots span {
    animation-delay: 0.4s;
  }

  @keyframes streaming-bounce {
    0%, 80%, 100% { transform: translateY(0); opacity: 0.4; }
    40% { transform: translateY(-6px); opacity: 1; }
  }

  /* ===== DIFF HIGHLIGHTING ===== */

  /* Line wrapper — flex row with marker + text */
  .diff-line {
    display: flex;
    padding: 0.25rem 1rem;
    font-size: var(--text-sm);
    line-height: 1.6;
  }

  /* Added line */
  .diff-line-add {
    background: var(--color-accent-a10);
    border-left: 3px solid var(--color-accent-300);
  }

  /* Removed line */
  .diff-line-remove {
    background: color-mix(in srgb, var(--color-error) 10%, transparent);
    border-left: 3px solid var(--color-error);
  }

  /* Context (unchanged) line */
  .diff-line-context {
    border-left: 3px solid transparent;
  }

  /* Marker column — fixed width for +/−/space */
  .diff-marker {
    width: 1.25rem;
    flex-shrink: 0;
    font-size: var(--text-xs);
    color: var(--color-foreground-disabled);
    user-select: none;
  }
  .diff-marker-add { color: var(--color-accent-300); }
  .diff-marker-remove { color: var(--color-error); }

  /* Text column */
  .diff-text {
    flex: 1;
    min-width: 0;
    white-space: pre-wrap;
    word-break: break-word;
    color: var(--color-foreground-muted);
  }
  .diff-text-add { color: var(--color-accent-300); }
  .diff-text-remove {
    color: var(--color-error);
    text-decoration: line-through;
  }

  /* ===== STATUS DOT ===== */

  /* Base — small circle */
  .status-dot {
    display: inline-block;
    width: 10px;
    height: 10px;
    border-radius: var(--radius-full);
    flex-shrink: 0;
    background: var(--color-foreground-disabled);
  }

  /* Size variants */
  .status-dot-sm { width: 6px; height: 6px; }
  .status-dot-lg { width: 12px; height: 12px; }

  /* Semantic states */
  .status-dot-success {
    background: var(--color-success);
    animation: status-dot-pulse 2s ease-in-out infinite;
  }
  .status-dot-error {
    background: var(--color-error);
    /* Static — no animation. Error should feel definitive. */
  }
  .status-dot-warning {
    background: var(--color-warning);
    animation: status-dot-pulse 2s ease-in-out infinite;
  }
  .status-dot-pending {
    background: var(--color-foreground-disabled);
    /* Static — neutral placeholder */
  }
  /* Categorical/informational tones (#111) — so `accent` (the tone an app maps a
     domain category like "assisting" to) is available on a status dot as well as on
     Badge / Chip / StatCard. Static: these aren't live-attention states. */
  .status-dot-accent {
    background: var(--color-accent-400);
  }
  .status-dot-info {
    background: var(--color-info);
  }

  @keyframes status-dot-pulse {
    0%, 100% { opacity: 1; transform: scale(1); }
    50% { opacity: 0.5; transform: scale(0.85); }
  }

  /* ===== SWATCH =====
     Free-color user-data dot (belt ranks, group/skill colors). The fill is
     runtime user data via --swatch-color; the contrast ring keeps pale colors
     visible on the surface in both themes. Semantic states use .status-dot. */
  .swatch {
    display: inline-block;
    width: 14px;
    height: 14px;
    border-radius: var(--radius-full);
    flex-shrink: 0;
    background: var(--swatch-color, var(--color-foreground-disabled));
    box-shadow: 0 0 0 1px var(--color-border-strong);
  }
  .swatch-sm { width: 10px; height: 10px; }
  .swatch-lg { width: 18px; height: 18px; }

  /* ===== HEATMAP =====
     Contribution-graph density grid. Fixed-size cells scroll horizontally
     rather than shrinking below a readable/tappable size. Intensity via the
     --heatmap-ramp-color ramp (accent-derived by default); corner markers via
     semantic tokens. */
  .heatmap {
    overflow-x: auto;
    padding-bottom: var(--spacing-2);
  }
  .heatmap-months {
    display: flex;
    /* No inter-label gap: each label's width already equals its span × the 23px
       column pitch (20px cell + 3px gap), so labels stay flush-aligned with the
       grid columns beneath them. A gap here would double-count and drift right. */
    margin-bottom: var(--spacing-1);
    font-size: var(--text-xs);
    color: var(--color-foreground-subtle);
  }
  /* Width spans whole columns. Column pitch = cell (20px) + gap (3px) = 23px;
     keep the 23 in sync with .heatmap-cell size + .heatmap-grid gap below. */
  .heatmap-month-label { flex: 0 0 auto; width: calc(var(--span, 1) * 23px); }
  .heatmap-months[data-daylabels] { padding-left: calc(32px + var(--spacing-2)); }
  .heatmap-body { display: flex; gap: var(--spacing-2); }
  .heatmap-days {
    display: grid;
    grid-template-rows: repeat(7, 20px);
    gap: 3px;
    width: 32px;
    font-size: var(--text-xs);
    color: var(--color-foreground-subtle);
  }
  .heatmap-day-label { line-height: 20px; }
  .heatmap-grid { display: flex; gap: 3px; position: relative; }
  /* Zero-size anchor the single hover/focus tooltip attaches to; positioned over
     the active cell via inline left/top/width/height. Never intercepts pointer. */
  .heatmap-tip-anchor { position: absolute; pointer-events: none; }
  .heatmap-col { display: grid; grid-template-rows: repeat(7, 20px); gap: 3px; }
  .heatmap-cell {
    position: relative;
    width: 20px;
    height: 20px;
    padding: 0;
    border: none;
    border-radius: var(--radius-sm);
    overflow: hidden;
    background: var(--heatmap-level-0);
    cursor: pointer;
  }
  /* Intensity ramp (#118). Two rules govern the shape of this, both verified in a
     browser rather than assumed — get either wrong and an override silently
     no-ops instead of erroring:

     1. The color-mix is evaluated HERE, on the cell, not in a :root token. A
        custom property substitutes its var()s at the scope where it is DECLARED,
        so a :root-declared ramp bakes in :root's accent and every descendant
        inherits that already-resolved colour — no override below :root could
        ever reach it. Mixing at the cell lets --heatmap-ramp-color be set on
        .heatmap, or any ancestor, and still win.
     2. --heatmap-ramp-color is never declared; the default lives in the var()
        fallback. A declared default would beat an *inherited* override, so
        scoping the hook on a wrapper around .heatmap would do nothing.

     Same runtime-hook contract as --marker-color below: fallback form, no
     declaration, which is also what keeps it exempt from the dist-integrity
     "every no-fallback var() resolves to a built token" guard.

     --heatmap-level-0 stays a real :root token: it is the empty track (a themed
     surface), not a step on the ramp, and does not follow the ramp's colour. */
  .heatmap-cell[data-level="0"] { background: var(--heatmap-level-0); }
  .heatmap-cell[data-level="1"] { background: color-mix(in srgb, var(--heatmap-ramp-color, var(--color-accent-500)) 28%, var(--color-bg-elevated-2)); }
  .heatmap-cell[data-level="2"] { background: color-mix(in srgb, var(--heatmap-ramp-color, var(--color-accent-500)) 52%, var(--color-bg-elevated-2)); }
  .heatmap-cell[data-level="3"] { background: color-mix(in srgb, var(--heatmap-ramp-color, var(--color-accent-500)) 76%, var(--color-bg-elevated-2)); }
  .heatmap-cell[data-level="4"] { background: var(--heatmap-ramp-color, var(--color-accent-500)); }

  /* Full-cell tone fill (#108) — a per-datum override of the intensity ramp, so a
     value:0 "planned absence" day reads as a solid colour, not an empty track. The
     React layer emits data-tone INSTEAD of data-level, so there is no cascade fight.
     Diannara's tone vocabulary (#111), not arbitrary colour. Distinct from the corner
     marker's .heatmap-cell-marker[data-tone]. */
  .heatmap-cell[data-tone="success"] { background: var(--color-success); }
  .heatmap-cell[data-tone="warning"] { background: var(--color-warning); }
  .heatmap-cell[data-tone="error"]   { background: var(--color-error); }
  .heatmap-cell[data-tone="info"]    { background: var(--color-info); }
  .heatmap-cell[data-tone="accent"]  { background: var(--color-accent-400); }
  .heatmap-cell[data-tone="neutral"] { background: var(--color-neutral-500); }
  .heatmap-cell:focus-visible { outline: none; box-shadow: var(--shadow-accent-focus); }
  .heatmap-cell-empty { background: transparent; cursor: default; }
  .heatmap-cell[aria-hidden] { cursor: default; }
  .heatmap-cell-marker {
    position: absolute;
    width: 9px;
    height: 9px;
    background: var(--marker-color, transparent);
  }
  .heatmap-cell-marker-tr { top: 0; right: 0; clip-path: polygon(100% 0, 0 0, 100% 100%); }
  .heatmap-cell-marker-bl { bottom: 0; left: 0; clip-path: polygon(0 100%, 0 0, 100% 100%); }
  /* Tone set is identical to .heatmap-cell[data-tone] above — same vocabulary, same
     tokens (#111's "unify the tone sets"; the accent/neutral gap was #118). `accent`
     only became reachable here once the ramp stopped owning the accent: before that,
     scoping data-accent to colour a marker recoloured the ramp underneath it to match,
     and the marker vanished into its own cell. */
  .heatmap-cell-marker[data-tone="success"] { --marker-color: var(--color-success); }
  .heatmap-cell-marker[data-tone="warning"] { --marker-color: var(--color-warning); }
  .heatmap-cell-marker[data-tone="error"]   { --marker-color: var(--color-error); }
  .heatmap-cell-marker[data-tone="info"]    { --marker-color: var(--color-info); }
  .heatmap-cell-marker[data-tone="accent"]  { --marker-color: var(--color-accent-400); }
  .heatmap-cell-marker[data-tone="neutral"] { --marker-color: var(--color-neutral-500); }
  /* flex-wrap lets custom categorical legend entries (#108) — a swatch + label each —
     wrap to a second line rather than overflow the tight Less→More ramp's width. */
  .heatmap-legend {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 3px;
    margin-top: var(--spacing-2);
    font-size: var(--text-xs);
    color: var(--color-foreground-subtle);
  }
  .heatmap-legend-item {
    display: inline-flex;
    align-items: center;
    gap: var(--spacing-1);
    margin-right: var(--spacing-2);
  }
  /* The swatch is a real .heatmap-cell, so it inherits the cell's size/radius and
     anchors a corner marker; position:relative lets the marker clip to it. */
  .heatmap-legend-swatch { position: relative; }

  /* ===== SLIDER INPUT ===== */

  /* Compound wrapper — pairs range slider with number input */
  .slider-input {
    display: flex;
    align-items: center;
    gap: 0.75rem;
  }
  .slider-input .slider {
    flex: 1;
    min-width: 0;
  }
  .slider-input .input {
    width: 6rem;
    text-align: center;
    flex-shrink: 0;
  }

  /* Optional label above the row */
  .slider-input-label {
    display: block;
    font-size: var(--text-xs);
    color: var(--color-foreground-subtle);
    margin-bottom: 0.375rem;
  }

  /* Optional hint below (min–max range) */
  .slider-input-hint {
    font-size: var(--text-xs);
    color: var(--color-foreground-disabled);
    margin-top: 0.25rem;
  }

  /* ===== RESPONSIVE OVERRIDES ===== */

  /* Navbar — collapse the horizontal item row below lg; the brand + a
     .hamburger remain, and the consumer wires the hamburger to the shared
     .sidebar-drawer + .sidebar-backdrop (same drawer the sidebar uses). */
  @media (max-width: 1023px) {
    .navbar-nav { display: none; }
    .navbar .hamburger { display: flex; }
  }

  /* Sidebar drawer — auto-static on desktop (lg+).
     :not([data-overlay]) scopes this to the VANILLA drawer (defect 2, #95). Plain
     markup carries no data-overlay, so it matches and morphs into a static column as
     before. The React DrawerContent stamps data-overlay: it is a Radix modal
     portalled to <body>, and must NOT become a static column parked at the end of the
     document when the viewport crosses this breakpoint.
     visibility:visible undoes the new base visibility:hidden (defect 1) — without it
     the entire vanilla desktop rail would be removed from the a11y tree.
     :not(dialog) excludes the native-<dialog> drawer (#99): it is a mobile-only modal
     (role=dialog), and a closed <dialog> is display:none by UA default — morphing it into
     a static column here would resurrect it as a permanent, mislabelled desktop rail. */
  @media (min-width: 1024px) {
    .sidebar-drawer:not([data-overlay]):not(dialog) {
      position: static;
      transform: none;
      visibility: visible;
      z-index: auto;
      width: var(--sidebar-width);
      display: flex;
      flex-direction: column;
      flex-shrink: 0;
      height: 100%;
    }
    .sidebar-backdrop:not([data-overlay]) { display: none; }

    /* The responsive rail becomes the in-flow desktop column. */
    .sidebar-responsive {
      display: flex;
      flex-direction: column;
      flex-shrink: 0;
      height: 100%;
    }

    /* The desktop half of the same split .sidebar-responsive ships. Without it the
       fixed bar stays at top:0 with --z-backdrop over .sidebar-app-header at every
       desktop width (#160). Additive for consumers already hiding it themselves —
       their rule and this one agree. */
    .mobile-topbar { display: none; }

    /* The bottom bar's desktop half — missing for the same reason as .mobile-topbar
       above, and found by the same review. Without it the fixed bar stays at
       bottom:0 with --z-backdrop over page content at every desktop width. Additive
       for consumers already hiding it themselves — their rule and this one agree. */
    .mobile-nav { display: none; }
  }

  /* Stepper — reduced motion */
  @media (prefers-reduced-motion: reduce) {
    .stepper-step.current .stepper-circle { animation: none; }
  }

  /* Table — compact cell padding.
     --table-cell-padding-x is 20px: desktop padding, and until now the only
     padding a table had at any width. At a 390px phone the wrap is ~339px and
     three columns spend 120px of it on padding alone — 35% — which overflows a
     3-column table by 36px (measured: scrollWidth 375 vs clientWidth 339).
     Every table in every consumer paid that, including the plain horizontal
     scroll we already ship. Padding only; the y value is unchanged because
     vertical rhythm isn't what runs out of room. */
  @media (max-width: 767px) {
    .table th,
    .table td {
      padding-left: var(--table-cell-padding-x-compact);
      padding-right: var(--table-cell-padding-x-compact);
    }
  }

  /* Chat messages — mobile */
  @media (max-width: 767px) {
    .chat-msg {
      padding: 0.375rem 0.75rem;
    }
    .chat-msg-list {
      gap: 1rem;
    }
  }

  /* Chat input — reduced motion */
  @media (prefers-reduced-motion: reduce) {
    .chat-input-stop { animation: none; }
  }

  /* Chat input — mobile */
  @media (max-width: 767px) {
    .chat-input {
      padding: 0.625rem 0.75rem;
      border-radius: var(--radius-lg);
    }
    .chat-input-send {
      padding: 0.5rem 0.75rem;
    }
  }

  /* Side panel — mobile fullscreen overlay */
  @media (max-width: 767px) {
    .side-panel {
      position: fixed;
      inset: 0;
      width: 100%;
      z-index: var(--z-drawer);
      transform: translateX(100%);
      /* visibility:hidden (defect 1, #114) removes the closed vanilla panel from the tab
         order + a11y tree; the transition delay keeps it visible during the slide-out. */
      visibility: hidden;
      transition: transform var(--duration-slow) var(--easing-out),
                  visibility 0s var(--duration-slow);
    }
    .side-panel.open {
      transform: translateX(0);
      visibility: visible;
      transition: transform var(--duration-slow) var(--easing-out), visibility 0s 0s;
    }
    .side-panel-left {
      transform: translateX(-100%);
      border-right: none;
    }
    .side-panel-left.open {
      transform: translateX(0);
    }
  }

  /* Side panel — desktop static column. :not([data-overlay]) scopes this to the VANILLA
     panel (defect 2, #114); the portalled React overlay keeps its fixed treatment.
     :not(dialog) excludes the native-<dialog> panel the same way the ≥lg sidebar rule
     excludes its own dialog drawer (:4924, #99): an open dialog.side-panel is an
     on-demand overlay at EVERY width — forcing it static at ≥768 would drop an open
     rail into the document flow mid-interaction. */
  @media (min-width: 768px) {
    .side-panel:not([data-overlay]):not(dialog) {
      position: static;
      transform: none;
      visibility: visible;
      z-index: auto;
    }
    .side-panel-backdrop:not([data-overlay]) { display: none; }
  }

  /* List sidebar — mobile drawer */
  @media (max-width: 767px) {
    .list-sidebar {
      position: fixed;
      left: 0;
      top: 0;
      bottom: 0;
      z-index: 45;
      transform: translateX(-100%);
      transition: transform var(--duration-slow) var(--easing-out);
      box-shadow: 4px 0 20px rgba(0, 0, 0, 0.4);
    }
    .list-sidebar.open {
      transform: translateX(0);
    }
    /* Action buttons always visible — no hover on touch. Now expressed in the same
       mechanism as the reveal above (#147); `display` is no longer what hides it. */
    .list-sidebar-item-action {
      opacity: 1;
      pointer-events: auto;
    }
  }

  /* Chip bar — mobile: always show close buttons */
  @media (max-width: 767px) {
    .chip-close { display: inline-flex; }
  }

  /* Streaming & status-dot — reduced motion */
  @media (prefers-reduced-motion: reduce) {
    .streaming-cursor::after { animation: none; }
    .streaming-pulse { animation: none; }
    .streaming-dots::before,
    .streaming-dots::after,
    .streaming-dots span { animation: none; }
    .status-dot-success { animation: none; }
    .status-dot-warning { animation: none; }
  }

  /* Diff — mobile */
  @media (max-width: 767px) {
    .diff-line {
      padding: 0.25rem 0.5rem;
    }
  }

  /* Modal — mobile full-screen sheet. specs/responsive.md has promised this row
     ("Modals: full-screen sheet from bottom") since before .modal shipped; this is
     the rule that keeps it. inset:0 stretches the fixed box edge to edge, which puts
     the header and footer at the screen edges and hands the whole remaining height to
     .modal-body (already flex:1 + overflow-y:auto + overscroll-behavior:contain).
     The 90vh/90dvh cap and the .modal-sm/md/lg widths keep governing at ≥768 — here
     the sheet owns its geometry, so both are explicitly released.
     The accent stripe stays: it is the brand signature and costs 3px. */
  @media (max-width: 767px) {
    .modal {
      /* inset overrides the base top/left:50% centering; transform:none the -50% pull. */
      inset: 0;
      transform: none;
      width: auto;
      max-width: none;
      max-height: none;
      border: none;
      border-radius: 0;
      animation: modal-sheet-in var(--duration-slow) var(--easing-out);
    }
    /* Explicit, so a size class can never win the width back on source reorder. */
    .modal-sm,
    .modal-md,
    .modal-lg { width: auto; }
    /* React (Radix) exit — Presence holds the node mounted while a CSS ANIMATION runs
       (it reads animationName), so the slide-out is keyframes, not a transition. The
       vanilla class-toggle path closes instantly: same honest asymmetry as
       dialog.sidebar-drawer. Entry needs no [data-state] partner — the sheet animates
       on mount (React) and on the display:none → rendered flip (vanilla). */
    .modal[data-state="closed"] {
      animation: modal-sheet-out var(--duration-slow) var(--easing-out);
    }
  }
  @keyframes modal-sheet-in {
    from { transform: translateY(100%); }
    to   { transform: translateY(0); }
  }
  @keyframes modal-sheet-out {
    from { transform: translateY(0); }
    to   { transform: translateY(100%); }
  }
  @media (prefers-reduced-motion: reduce) {
    .modal,
    .modal[data-state] { animation: none; }
  }

  /* ============================================================
     Carousel — sequenced item viewer
     ------------------------------------------------------------
     .carousel             root flex column
     .carousel-toolbar     top row (action buttons)
     .carousel-toolbar-end right-aligned end of the toolbar
     .carousel-viewport    current item's bordered frame
     .carousel-footer      prev / counter / next
     .carousel-prev / -next    chevron icon buttons
     .carousel-counter     "3 of 12" accent pill
     .carousel-empty       fallback when items.length === 0 and consumer passes no emptyState
     ============================================================ */

  .carousel {
    display: flex;
    flex-direction: column;
    gap: var(--spacing-3);
  }

  .carousel-toolbar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: var(--spacing-2);
  }

  .carousel-toolbar-end {
    display: flex;
    gap: var(--spacing-2);
    margin-left: auto;
  }

  .carousel-viewport {
    background: var(--color-bg-elevated-1);
    border: 1px solid var(--color-border-default);
    border-radius: var(--radius-lg);
    padding: var(--spacing-4);
    min-height: 8rem;
  }

  .carousel-footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--spacing-3);
  }

  /* Chevron buttons — flex-shrink:0 so they don't get squashed in narrow rows.
     Pair with .btn-icon for the icon-button surface treatment. The disabled
     state fades the button so "at the end" reads visually, not just as an
     inert click target.                                                       */
  .carousel-prev {
    flex-shrink: 0;
  }
  .carousel-next {
    flex-shrink: 0;
  }
  .carousel-prev:disabled,
  .carousel-next:disabled {
    opacity: 0.4;
    cursor: not-allowed;
  }

  .carousel-counter {
    background: var(--color-accent-a15);
    color: var(--color-accent-300);
    font-size: var(--text-sm);
    font-weight: var(--font-weight-semibold);
    padding: 2px 10px;
    border-radius: var(--radius-full);
    line-height: 1.4;
  }

  .carousel-empty {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 8rem;
    color: var(--color-foreground-muted);
    font-size: var(--text-base);
  }

  /* Direction-aware slide-in. The viewport's data-direction attribute is
     set by <Carousel> on every index change.                              */
  @keyframes carousel-slide-in-right {
    from { transform: translateX(8%); opacity: 0; }
    to   { transform: translateX(0);  opacity: 1; }
  }
  @keyframes carousel-slide-in-left {
    from { transform: translateX(-8%); opacity: 0; }
    to   { transform: translateX(0);   opacity: 1; }
  }
  .carousel-viewport[data-direction="forward"] {
    animation: carousel-slide-in-right var(--duration-normal) var(--easing-spring);
  }
  .carousel-viewport[data-direction="back"] {
    animation: carousel-slide-in-left var(--duration-normal) var(--easing-spring);
  }
  @media (prefers-reduced-motion: reduce) {
    .carousel-viewport[data-direction="forward"],
    .carousel-viewport[data-direction="back"] {
      animation: none;
    }
  }

  /* ============================================================
     SpectrumSlider — two-pole trait dial
     ------------------------------------------------------------
     Composes the existing .slider class for the track + thumb.
     .spectrum-slider-* covers only the chrome around the slider:
     pole labels, active-pole highlight, optional center anchor.
     ============================================================ */

  .spectrum-slider {
    display: flex;
    align-items: center;
    gap: var(--spacing-3);
  }

  .spectrum-slider-pole {
    flex-shrink: 0;
    color: var(--color-foreground-muted);
    font-size: var(--text-sm);
    font-weight: var(--font-weight-semibold);
    white-space: nowrap;
    transition: color var(--duration-fast);
  }

  .spectrum-slider-pole-active {
    color: var(--color-accent-300);
  }

  .spectrum-slider-track-wrap {
    flex: 1;
    position: relative;
  }

  /* Optional midpoint anchor — sits at track center so it's visible
     without interrupting the thumb's path.                            */
  .spectrum-slider-anchor {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    width: 1px;
    height: 12px;
    background: var(--color-border-emphasis);
    pointer-events: none;
  }

  /* Disabled state — dim the poles to match Radix's disabled track. */
  .spectrum-slider[data-disabled="true"] .spectrum-slider-pole,
  .spectrum-slider[data-disabled="true"] .spectrum-slider-pole-active {
    color: var(--color-foreground-disabled);
  }


  /* ===== TREE (issue 153) =====
     An arbitrary-depth nav tree. Rows reuse .sidebar-item chrome — a nav row should
     look like a nav row — so only the indent, the wrap behaviour and the nesting
     guide live here.

     Depth is a custom property, NOT descendant selectors. A rule written as
     .tree-content .tree-content .tree-row caps at however many levels someone
     hand-writes, which is the exact bug this component exists to fix. One inline
     --tree-depth per row buys depth that never runs out.

     This is a STEP (indent-base + depth * indent-step), unlike
     --sidebar-item-nested-padding-x, which is one optical alignment putting a child's
     label under its parent's LABEL and has nowhere to put a grandchild. The two
     coexist deliberately; see navigation.md and specs/components/tree.md. */
  .tree,
  .tree ul {
    list-style: none;
    margin: 0;
    padding: 0;
  }

  .tree-row {
    padding-left: calc(
      var(--tree-indent-base) + var(--tree-depth, 0) * var(--tree-indent-step)
    );
    /* start, not center: when a long label wraps, the chevron and any leading icon
       must stay on the FIRST line rather than drifting to the optical middle of a
       two-line row. */
    align-items: start;
    /* Narrower than .sidebar-item's 12px — see --tree-row-gap. Declared after
       .sidebar-item in source order, which is what makes it win at equal
       specificity. */
    /* Undo .sidebar-item's `overflow: hidden` (#159). A tree row can hold focusable
       DESCENDANTS rather than being the focusable thing itself: on .tree-row-split the
       .tree-link sits flush with the row's padding box on top, bottom and right (all
       three gaps measured at 0.0px) and .tree-disclosure is flush top and bottom.
       Neither ships a focus rule, so both rely on the UA ring, which is drawn OUTSIDE
       the border box — a clip on the row erases it on three sides. The same clip also
       eats .tree-disclosure's (pointer: coarse) 44px ::after target: hit-tested at the
       row edge it returns the page container under `hidden` and the disclosure under
       `visible`. Precedent: .list-sidebar-item-edit carries the identical note.
       The row-level clip buys the tree nothing anyway — .tree-label does its own
       clipping, and truncation stays opt-in via .tree-row-truncate below (a tree label
       sits in a column narrowed by indentation at every depth, where an ellipsis eats a
       larger fraction of a shorter line).
       Ties with .sidebar-item at (0,1,0) and wins on source order — this whole rule is
       declared after .sidebar-item, the same mechanism --tree-row-gap above relies on.
       Folded into this rule rather than a second .tree-row block: a separate rule here
       would trip the #126 duplicate-bare-class guard, which exists for exactly this
       reason — two rules for one selector silently merge, and merging by hand is the
       fix it asks for. */
    overflow: visible;
  }

  /* A leaf reserves the disclosure column so labels line up at a given depth, the
     way they do in every file tree. :has() rather than a modifier class, so a
     plain-HTML consumer gets the reservation without remembering to add one —
     a row either contains a chevron or it does not, and the CSS can see that. */
  .tree-row:not(:has(.tree-chevron)) {
    padding-left: calc(
      var(--tree-indent-base) + var(--tree-depth, 0) * var(--tree-indent-step) +
        var(--tree-chevron-size) + var(--tree-row-gap)
    );
  }

  /* 1lh pins the glyph to the first line's box. Same technique as
     .list-sidebar-item-leading, where dropping it measured 2.4px of drift on every
     row — small enough to miss by eye, which is why it is written down.
     .tree-meta is in this set for the same reason on the other edge: without it
     the trailing content sinks to the optical middle of a two-line row while the
     chevron stays put, and the two visibly disagree. */
  .tree-chevron,
  .tree-leading,
  .tree-meta {
    align-self: start;
    height: 1lh;
    flex: none;
    display: flex;
    align-items: center;
  }

  /* The chevron is a fixed LEADING column shared by all three row kinds — the
     disclosure button in a split row, a span in a Trigger, and reserved-but-empty
     on a leaf. One position for the whole component: a tree mixing folder rows and
     parent-document rows would otherwise show two different disclosure
     affordances. */
  .tree-chevron {
    width: var(--tree-chevron-size);
    justify-content: center;
  }

  /* The glyph, not the column. `.sidebar-item svg` (0,1,1) forces every svg inside a
     row to --sidebar-item-icon-size (20px) and beats the markup's own width="16", so
     without this the chevron renders 20px inside a 16px column: it bleeds 2px past
     --tree-indent-base on the left and eats 2px of --tree-row-gap on the right, making
     the measured glyph-to-label gap 6px rather than 8. This is the same (0,1,1) fight
     .sidebar-group-chevron documents above, settled the same way — on class count
     (0,2,1), not on source order.

     `height: 1lh` rather than --tree-chevron-size because this box is what pins the
     glyph to the label's FIRST line. The viewBox is square and preserveAspectRatio
     defaults to xMidYMid meet, so the drawing still renders at the 16px width and
     centres itself in the taller box. Putting the 1lh box on the glyph rather than on
     .tree-chevron is what frees .tree-disclosure to be the full row height below. */
  .tree-row .tree-chevron svg {
    width: var(--tree-chevron-size);
    height: 1lh;
    transition: transform var(--duration-fast);
  }

  /* Trailing static content (an id, a timestamp). Static text, not a control — it
     composes inside .tree-link, so the whole row minus the chevron stays one
     navigation target. margin-left:auto matches .sidebar-item-count's convention
     and lets .tree-label keep `flex: 0 1 auto` and shrink into its clamp. */
  .tree-meta {
    margin-left: auto;
  }

  /* SPLIT ROW (#157) — a row that both NAVIGATES and DISCLOSES, for a tree whose
     parents are themselves documents. The disclosure and the link must be
     siblings: nesting either inside the other puts an interactive element inside
     an interactive element.

     So the row becomes a CONTAINER and the children carry the padding. The
     chevron's column is the button at full row height; everything from the gap to
     the right edge — including the trailing inset — is the link. No stretched-link
     ::after overlay, no z-index stacking, and no dead strip where a click does
     nothing. The indent, radius and fill stay on the container, which is the whole
     reason to use Tree rather than hand-rolling two controls in a wrapper. */
  .tree-row-split {
    padding-block: 0;
    padding-inline-end: 0;
  }

  /* .tree-disclosure IS the whole column, and saying so takes three declarations the
     padding alone does not buy. The button carries .tree-chevron too, so the 1lh group
     above ties with this rule at (0,1,0) — and this rule only wins because it is
     declared AFTER it, which is why the group was moved above the split row rather than
     left below it. Without the override the button stays 16px x 1lh and top-aligned
     inside a row its own padding-block makes 2 x --sidebar-item-padding-y taller: that
     padding then belongs to the row's <div> rather than to any control, and 16px of the
     chevron column renders cursor:pointer and the row's hover fill while a click on it
     does nothing. Stretching the button is what makes "no dead strip" above true rather
     than aspirational.

     align-items: flex-start, not center: a stretched box centres its glyph on the
     optical middle of a wrapped two-line row. flex-start puts the glyph's 1lh box flush
     with the top of the content box, which is exactly where .tree-link's first line box
     starts — both children carry the same padding-block. The first-line alignment lives
     on the glyph (.tree-row .tree-chevron svg, above), not on this box. */
  .tree-disclosure {
    align-self: stretch;
    height: auto;
    align-items: flex-start;
    padding-block: var(--sidebar-item-padding-y);
    /* <button> resets, same set .sidebar-item applies for the same reason — a row
       that ACTS can BE the styled thing rather than restyling one. */
    background: none;
    border: 0;
    color: inherit;
    font: inherit;
    cursor: pointer;
  }

  .tree-link {
    flex: 1;
    min-width: 0;
    display: flex;
    align-items: start;
    gap: var(--tree-row-gap);
    padding-block: var(--sidebar-item-padding-y);
    padding-inline-end: var(--sidebar-item-padding-x);
    color: inherit;
    text-decoration: none;
  }

  /* Wrap to two lines by default. Truncation is opt-in (.tree-row-truncate), not the
     only behaviour — a 300px rail clipped "ADR 002: Token di…" and that is the
     failure this default exists to avoid. */
  .tree-label {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    overflow: hidden;
    min-width: 0;
    /* Without this a title carrying one long unbroken token (an id-ish slug, a
       URL) overflows the rail instead of wrapping — the clamp only breaks at
       opportunities, and such a title offers none. */
    overflow-wrap: anywhere;
  }

  .tree-row-truncate .tree-label {
    display: block;
    white-space: nowrap;
    text-overflow: ellipsis;
  }

  /* Radix stamps data-state on the trigger itself in both shapes — a <button>
     wrapping the chevron, or a <button> that IS the chevron — so the rotation no
     longer depends on the DOM path between .tree-branch and the glyph.

     The GLYPH turns, not its box. When .tree-chevron is a <span> in a Trigger the two
     are nearly the same thing, but .tree-disclosure IS the .tree-chevron and is now the
     full row height — rotating that box 90deg would swing a 40px-tall button into a
     40px-WIDE hit area lying across the link, and carry the glyph 8px down and 8px
     right of the column with it (transform does not touch layout, so nothing would
     reflow to reveal it). Browser-measured on the guide before this was moved: a
     `transform` on the button reported a 40 x 16 bounding box for a 16 x 40 element.
     The svg's own box is --tree-chevron-size x 1lh with the square drawing centred in
     it, so turning the svg turns the glyph about its own centre in both shapes. */
  [data-state="open"] > .tree-chevron > svg,
  .tree-chevron[data-state="open"] > svg {
    transform: rotate(90deg);
  }

  /* The nesting guide. Sits midway between the parent's inset and this level's, so it
     runs down the gutter the indent opens up. Without it a deep tree loses the tie
     between a child and a parent that has scrolled out of view.

     With the chevron now leading, the hairline lands in the middle of the column
     the PARENT's chevron occupies — so it reads as descending out of the parent's
     disclosure. That falls out of the existing expression; nothing moved. */
  .tree-content {
    position: relative;
  }

  .tree-content::before {
    content: "";
    position: absolute;
    inset-block: 0;
    left: calc(
      var(--tree-indent-base) + (var(--tree-depth, 1) - 0.5) * var(--tree-indent-step)
    );
    width: var(--tree-guide-width);
    background: var(--tree-guide-color);
  }

  /* ANCESTOR TRAIL — the rows between the root and the current one. They lift
     without taking a fill, and the guide lights along the path, so the trail reads
     as "you came through here" without competing with the actual selection. This is
     component state rather than something a call site paints on, because inventing
     an opinion later would be a visual change to every shipped row.

     The ladder these values sit on, because picking them by feel is how the first
     attempt shipped two rules that changed nothing: .sidebar-item rests at
     --color-foreground-subtle and hovers to --color-foreground-muted, and
     .sidebar-item-active takes --color-accent-300 with --font-weight-semibold. A trail
     row has to read BETWEEN those, so it takes --color-foreground-body — one step above
     the hover tone, which is what keeps a resting trail row from being mistaken for a
     hovered normal one. Naming --color-foreground-subtle here (as this block first did)
     restates what .sidebar-item already sets, and --font-weight-medium on the label
     restates what it already inherits: both were no-ops, and with neither shipped demo
     carrying a leading icon a trail row rendered pixel-identical to a normal row. */
  .tree-row-trail {
    color: var(--color-foreground-body);
  }
  /* One step brighter than the row again, so the glyph leads the label rather than
     trailing it — the same relationship the row has to a normal row. */
  .tree-row-trail .tree-leading {
    color: var(--color-foreground-secondary);
  }
  /* On the label, not the row: the lift belongs to the title, and a trailing
     .tree-meta id reads better staying at the row's own weight. */
  .tree-row-trail .tree-label {
    font-weight: var(--font-weight-semibold);
  }
  /* .sidebar-item:hover is (0,2,0) and would otherwise DIM a trail row on hover —
     --color-foreground-muted is darker than the resting tone above. Same specificity,
     later in source, so the trail continues up the scale instead of down. */
  .tree-row-trail:hover {
    color: var(--color-foreground-secondary);
  }
  .tree-content-trail::before {
    background: var(--tree-guide-color-trail);
  }

  /* THE ACTIVE MARKER (#157). Retires --sidebar-active-bar-width, which shipped
     from at least v1.6.0 to v2.2.0 declared in three files and implemented by no
     rule — a consumer correctly inferred the feature from the published token and
     hand-drew it twice, in two different vocabularies.

     It lives on .tree-row and NOT on .sidebar-item-active: a sidebar can carry
     .sidebar-item-active on a CollapsibleGroup trigger AND its nested child
     simultaneously, and because nesting there is padding-left rather than a box
     offset, both boxes start at the same x. A general bar would draw two markers
     one row apart for a single selection. A tree has exactly one current row.

     `left` is derived from the GUIDE's own expression rather than a constant that
     happens to match, so the two cannot drift apart if --tree-indent-step ever
     changes. Matching constants ship green and misprint two releases later.

     The bar centres on the guide hairline's CENTRE, not its left edge. The guide's
     `left` is the hairline's own left edge, so the hairline's box spans
     [L, L + guide-width] and its centre sits at L + guide-width/2 — not at L. A bar
     that centres itself directly on L (subtracting only its own half-width) would
     land guide-width/2 short of the hairline's true centre: invisible at the
     guide's current 1px width (a 0.5px miss), but a real, token-scaled offset that
     would show up the moment --tree-guide-width changes — exactly the kind of drift
     this whole derivation exists to prevent. Adding + --tree-guide-width / 2 shifts
     the bar's anchor from the hairline's left edge to its centre, so both markers
     resolve to the same x for any value of either width token. Browser-measured
     (issue #157 Task 7): before this term, bar centre and hairline centre were
     324.0px vs 324.5px — a real, if sub-pixel, divergence, not the "coincide"
     invariant the design claims.

     Inset to the row height rather than a fixed-height pill: a pill is centred
     correctly at exactly one row height and sits high the moment a label wraps,
     which is the same failure the 1lh alignment above exists to prevent. */
  .tree-row.sidebar-item-active::before {
    content: "";
    position: absolute;
    top: 4px;
    bottom: 4px;
    width: var(--tree-active-bar-width);
    border-radius: 0 var(--tree-active-bar-width) var(--tree-active-bar-width) 0;
    background: var(--color-accent-400);
    left: calc(
      var(--tree-indent-base) + (var(--tree-depth, 0) - 0.5) * var(--tree-indent-step) +
        var(--tree-guide-width) / 2 - var(--tree-active-bar-width) / 2
    );
  }

  /* Depth 0 has no parent and therefore no guide, so the marker hugs the rail edge.
     A child selector separating root from non-root — NOT a depth count, so it does
     not reintroduce the nesting cap --tree-depth exists to remove. */
  .tree > .tree-branch > .tree-row.sidebar-item-active::before,
  .tree > li > .tree-row.sidebar-item-active::before {
    left: 0;
  }

  /* ===== TOUCH TARGETS =====
     accessibility.md requires 44 x 44px minimum for interactive elements on
     touch. Several small controls are deliberately smaller than that: .btn-sm
     is 32px, .toggle-chip ~26px, .chip-close 16px. Growing their boxes would
     change the height of every row they compose into — the exact class of
     regression issue 150 was filed about.

     So the HIT AREA grows and the control does not. A centred ::after overlay
     claims 44 x 44 without participating in layout, so rendered geometry is
     unchanged to the pixel and only the touch region differs.

     Gated on (pointer: coarse), not a width breakpoint: this is a property of
     the input device, not the viewport. A touch laptop needs the larger target
     at any width, and a narrow mouse-driven window does not.

     Known trade-off: where two of these sit closer than ~19px apart their hit
     areas overlap and the one later in DOM order wins the tap. That is still
     better than a 26px target, but it is a reason not to pack them tighter. */
  @media (pointer: coarse) {
    .btn-xs,
    .btn-sm,
    .icon-toggle,
    .chip-close,
    .chip-bar-add,
    .toggle-chip,
    .tree-disclosure,
    .datepicker-day,
    .datepicker-nav-btn,
    button.datepicker-month-label {
      position: relative;
    }

    .btn-xs::after,
    .btn-sm::after,
    .icon-toggle::after,
    .chip-close::after,
    .chip-bar-add::after,
    .toggle-chip::after,
    .tree-disclosure::after,
    .datepicker-day::after,
    .datepicker-nav-btn::after,
    button.datepicker-month-label::after {
      content: "";
      position: absolute;
      top: 50%;
      left: 50%;
      translate: -50% -50%;
      width: 100%;
      height: 100%;
      min-width: 44px;
      min-height: 44px;
    }

    /* .tree-disclosure's expanded region is 44px wide against a 16px column, so it
       spills ~14px past the column and overlaps the leading edge of .tree-link,
       which sits immediately after it in the row. .tree-link is NOT in the list
       above — it has no ::after of its own and no `position`, so this is one
       control's grown region over a plain in-flow sibling, not the two-overlay
       case the caveat above describes. Which of the two takes a tap there is
       decided by paint order, which follows position and stacking rather than
       anything stated. The z-index makes the disclosure's claim on that strip
       explicit rather than positional, so it survives a future change to either
       element's position or stacking context. */
    .tree-disclosure::after {
      z-index: 1;
    }

    /* Measured before adding the date picker here, because the overlap caveat above bites
       hardest in a 7-column grid where a mistaken tap picks the WRONG DATE rather than
       merely missing:
       - .datepicker-day is 40x40 on a 42px pitch, so a 44px region spills exactly 1px each
         side — into the 2px gutter, which is dead space today. No live target is stolen;
         the gutter simply stops being a hole and resolves to the later cell.
       - .datepicker-nav-btn is 28x28 with 51px of clear air to the header label on both
         sides. Growing it 8px per side leaves 43px of clearance.
       - button.datepicker-month-label is 134x35, so only its height is short; ~4px of
         vertical growth fits inside the header's 12px margin.
       .datepicker-period is deliberately absent: with the min-width above it renders ~93x44
       and already passes on both axes, and at its 39px pitch a 44px region WOULD have
       overlapped its neighbours by ~2.5px of live cell. */
  }

}

/*
 * Text color utilities.
 *
 * Single-property classes that set `color` to a semantic text-role token.
 * Place in @layer utilities so they win over component-level color rules
 * (e.g., .btn sets its own color; if a consumer puts .text-foreground-muted on a
 * button, the utility takes precedence — matching Tailwind v4's convention).
 *
 * Pair with modifiers: `hover:text-foreground`, `focus:text-foreground-muted`, etc.,
 * work automatically through Tailwind v4's variant system when consumers
 * build their CSS entry with @import "tailwindcss".
 *
 * `.display` (in @layer base above) is intentionally NOT here — it's a
 * composite typography role (family + weight + leading + letter-spacing
 * + color), not a color-only utility. Different category of thing.
 */
@layer utilities {
  /*
   * Screen-reader-only text.
   *
   * Shipped here rather than borrowed from Tailwind because components in this
   * library generate their own text alternatives — Distribution describes its
   * split in words so the meaning does not depend on colour — and those have to
   * stay hidden for EVERY consumer. Tailwind's `.sr-only` covers the Tailwind
   * path only; a plain-CSS consumer (see tests/fixtures/) would have rendered
   * the "hidden" description in full view.
   *
   * Both definitions are identical and both sit in @layer utilities, so the two
   * paths coexist — whichever the cascade picks computes the same.
   *
   * clip-path, not `clip`: the latter is deprecated.
   */
  .sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border-width: 0;
  }

  .text-foreground   { color: var(--color-foreground); }
  .text-foreground-secondary { color: var(--color-foreground-secondary); }
  .text-foreground-body      { color: var(--color-foreground-body); }
  .text-foreground-muted     { color: var(--color-foreground-muted); }
  .text-foreground-subtle    { color: var(--color-foreground-subtle); }
  .text-foreground-disabled  { color: var(--color-foreground-disabled); }
  .text-foreground-ghost     { color: var(--color-foreground-ghost); }

  /*
   * Status text utilities. Tailwind v4 auto-generates these from the
   * --color-{success,warning,error,info} tokens for consumers who pull in
   * @import "tailwindcss" + @import "diannara/theme". Shipping them hand-rolled
   * here gives non-Tailwind consumers (plain HTML, PHP, Python templates) the
   * same ergonomics — same class names, same resolution. The two paths coexist
   * because both compute to var(--color-<status>) — whichever the cascade picks
   * is the same value.
   */
  .text-success { color: var(--color-success); }
  .text-warning { color: var(--color-warning); }
  .text-error   { color: var(--color-error); }
  .text-info    { color: var(--color-info); }
}

/*
 * Sonner integration — non-layered overrides.
 *
 * Sonner ships its own CSS that is NOT inside any @layer. Per the CSS
 * cascade, non-layered rules win over @layer rules at the same specificity.
 * So if we put these overrides inside @layer components, Sonner's defaults
 * (white background, dark text, its own gray border) would win and the
 * .toast class would never apply. Keeping these rules at the top level
 * puts them on equal footing with Sonner's, where our higher specificity
 * (and/or definition order) lets them win.
 *
 * Strategy: override Sonner's CSS variables at the [data-sonner-toaster]
 * root so Sonner's own internal rules paint with Diannara tokens. Add a
 * separate set of selectors for the per-type left stripe that Sonner
 * doesn't have a native concept for.
 */
/* Match Sonner's `[data-sonner-toaster][data-sonner-theme='*']` selector
   specificity (0,2,0) and add a `:root` ancestor to push past it (0,2,1)
   so our token overrides win regardless of which sheet loads last. */
:root [data-sonner-toaster][data-sonner-theme='light'],
:root [data-sonner-toaster][data-sonner-theme='dark'],
:root [data-sonner-toaster] {
  --normal-bg: var(--toast-bg);
  --normal-text: var(--color-foreground-body);
  --normal-border: var(--toast-border);
  --success-bg: var(--toast-bg);
  --success-text: var(--color-success);
  --success-border: var(--toast-border);
  --error-bg: var(--toast-bg);
  --error-text: var(--color-error);
  --error-border: var(--toast-border);
  --warning-bg: var(--toast-bg);
  --warning-text: var(--color-warning);
  --warning-border: var(--toast-border);
  --info-bg: var(--toast-bg);
  --info-text: var(--color-info);
  --info-border: var(--toast-border);
  --border-radius: var(--toast-radius);
}

/* Per-type left stripe — mirrors the .toast-{type} variants the plain-HTML
   chips ship. Sonner sets data-type on each toast based on toast.success(),
   toast.error(), etc. `:root` prefix bumps specificity to win over Sonner's
   own `[data-sonner-toast][data-styled='true']` border rule (0,2,0 → 0,2,1). */
:root [data-sonner-toast][data-type="success"] { border-left: var(--toast-stripe-width) solid var(--color-success); }
:root [data-sonner-toast][data-type="error"]   { border-left: var(--toast-stripe-width) solid var(--color-error); }
:root [data-sonner-toast][data-type="warning"] { border-left: var(--toast-stripe-width) solid var(--color-warning); }
:root [data-sonner-toast][data-type="info"]    { border-left: var(--toast-stripe-width) solid var(--color-info); }
