/* Reference component implementations. Every value comes from var(--ds-*). */
/* Copied from the design-system skill assets by scaffold_design_system.py. */
/* Extend per project; keep token discipline. No hex/rgb literals in this file. */

/* ---------- Text rendering ---------- */

/* Grayscale antialiasing, shipped rather than left to the specimen.
 *
 * This was set in the specimen's own stylesheet and nowhere else, which meant
 * the reference rendered every glyph differently from every consumer built on
 * it: grayscale in the specimen, the platform default — subpixel on macOS —
 * in the product. Same family, same weight, same metrics, visibly different
 * text, and no token could explain the difference because none was involved.
 * Reported as body copy looking heavier in the app than in the specimen; the
 * measured advance width was identical in both, which is what ruled out the
 * font and left the rasteriser.
 *
 * The choice matters at this weight. The brand's copy weight is 500 and its
 * display weight is the axis floor, and subpixel rendering thickens both — it
 * is the difference between the thin headline reading as drawn and reading as
 * merely light. Shipping it puts the reference and the product on the same
 * rasteriser, which is the only way a specimen can be evidence about weight.
 *
 * It belongs here rather than in theme.css because theme.css is generated from
 * tokens and this is not a token: it is a rendering instruction, with no value
 * to resolve and nothing to rebrand. */
/* text-wrap belongs to the same category and inherits, so one declaration at the
 * root reaches every wrapped string in the system — including components that do
 * not exist yet, which is the point. A last line holding one word is a widow,
 * and it is not a copy problem: the same sentence widows or does not depending
 * on the measure, the family, and the size, so it survives review under one
 * brand and appears under the next. Onyx's Tooltip is where it showed (F47).
 *
 * `pretty`, not `balance`. Measured on a real tooltip across 15 measures from
 * 260px down to 120px: shipped wrapping produced 4 widows, `balance` 3, `pretty`
 * 0. `balance` evens every line, so it also reflows text that was already fine,
 * and engines abandon it past roughly six lines — which is exactly where the
 * long cases widowed. `pretty` reconsiders only the end of the block: every
 * good case above stayed byte-identical and only the bad ones moved.
 *
 * Unsupported engines ignore it and wrap as before, so this can never be worse
 * than shipping nothing. */
html {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-wrap: pretty;
}

/* ---------- Theme switching ---------- */

/* Set this attribute on the root for the duration of a theme change, then
   remove it on the frame after.
 *
 * Every transition that names a colour property animates when the theme flips,
 * because the token underneath it changed value rather than the element
 * changing state. That is never wanted: a theme change is a repaint of the
 * whole page, not a transition on any one part of it, and elements that
 * transition one colour but not another visibly come apart while it runs. The
 * clearest case is an element carrying border and background on the same token
 * and transitioning only the border — the background lands on the new theme at
 * once while the border spends the duration arriving from the old one, so the
 * previous theme's surface reads as an outline around it until the transition
 * finishes.
 *
 * The transitions themselves are correct; they exist for hover and focus, where
 * an element really is changing state. This suppresses them for the swap alone.
 * A host that switches themes should set the attribute, change the theme, and
 * clear the attribute after two animation frames — the first so the suppression
 * is in effect for the recalc the theme change triggers, the second so the
 * removal lands after that has painted. */
[data-theme-switching],
[data-theme-switching] *,
[data-theme-switching] *::before,
[data-theme-switching] *::after {
  transition: none !important;
}

/* ---------- Layout helpers ---------- */

/* The resolved page edge is document geometry, not a Grid implementation
   detail. Full-bleed regions (Topbar, Banner, top/bottom Sheet) consume the
   same active gutter as .ds-grid, so a breakpoint moves every page-aligned
   edge together. A narrower `data-grid-root="main"` remains free to resolve
   its own region tier in layer 2. */
:root { --ds-page-gutter: var(--ds-grid-margin-base); }
@media (min-width: 640px) { :root { --ds-page-gutter: var(--ds-grid-margin-sm); } }
@media (min-width: 768px) { :root { --ds-page-gutter: var(--ds-grid-margin-md); } }
@media (min-width: 1024px) { :root { --ds-page-gutter: var(--ds-grid-margin-lg); } }

.ds-container {
  max-width: var(--ds-container-xl);
  margin-inline: auto;
  padding-inline: var(--ds-space-4);
}
/* Full-bleed: no max, keeps the side padding so content never kisses the
   viewport edge — drop the padding per-surface when a background must. */
.ds-container--full { max-width: var(--ds-container-full); }

/* ---------- Layout primitives ---------- */

/* `hidden` is a semantic state, not a component variant. Any component may
   establish its own display mode, so the system reset must beat those rules
   without asking every root to remember a component-specific exception. */
:where([hidden]) { display: none !important; }

/* Framework-free composition primitives: gap and padding always come from
   the space scale, so arbitrary margins never appear. The prop-driven
   Stack / Inline / Box arrive at layer 2; these prove the tokens compose
   and give that layer a CSS baseline to wrap. Sizing follows the sm/md/lg
   dialect — md is the default, sm/lg step the gap or inset. */
.ds-stack { display: flex; flex-direction: column; gap: var(--ds-space-4); }
.ds-stack--sm { gap: var(--ds-space-2); }
.ds-stack--lg { gap: var(--ds-space-6); }

.ds-inline { display: flex; flex-direction: row; align-items: center; flex-wrap: wrap; gap: var(--ds-space-4); }
.ds-inline--sm { gap: var(--ds-space-2); }
.ds-inline--lg { gap: var(--ds-space-6); }
/* Wraps by default (a row should never overflow its container); opt out
   when the row is a fixed pair that must stay on one line. */
.ds-inline--nowrap { flex-wrap: nowrap; }
/* Centered is right for a row of like-sized children (a button bar, a chip
   row). It is wrong the moment one child is taller than the rest: an icon
   beside copy that wraps to two lines ends up floating at the middle of the
   block. Then the row aligns to its first line. */
.ds-inline--start { align-items: flex-start; }

.ds-box { padding: var(--ds-space-4); }
.ds-box--sm { padding: var(--ds-space-2); }
.ds-box--lg { padding: var(--ds-space-6); }

/* Surface context: place this on any non-neutral region after mapping the
   on-surface.* role set to that surface's paired foreground and state hues.
   Components use the local aliases with canvas-role fallbacks, so existing
   neutral consumers remain unchanged. */
.ds-on-surface {
  color: var(--ds-on-surface-primary);
  --ds-context-text-primary: var(--ds-on-surface-primary);
  --ds-context-text-secondary: var(--ds-on-surface-secondary);
  --ds-context-text-muted: var(--ds-on-surface-muted);
  --ds-context-control-hover: var(--ds-on-surface-hover);
  --ds-context-control-active: var(--ds-on-surface-active);
  --ds-context-avatar-bg: var(--ds-on-surface-hover);
  --ds-context-avatar-text: var(--ds-on-surface-primary);
  --ds-context-avatar-border: var(--ds-on-surface-muted);
}

/* Complete semantic type recipes. Consumers choose the semantic class and
   the family, size, line-height, weight, and tracking move together. */
.ds-text-display { font-family: var(--ds-font-role-display); font-size: var(--ds-font-size-display); line-height: var(--ds-font-line-none); font-weight: var(--ds-font-weight-display); letter-spacing: var(--ds-font-tracking-tighter); }
.ds-text-heading-3xl { font-family: var(--ds-font-role-heading); font-size: var(--ds-font-size-heading-3xl); line-height: var(--ds-font-line-none); font-weight: var(--ds-font-weight-heading-3xl); letter-spacing: var(--ds-font-tracking-tighter); }
.ds-text-heading-2xl { font-family: var(--ds-font-role-heading); font-size: var(--ds-font-size-heading-2xl); line-height: var(--ds-font-line-tight); font-weight: var(--ds-font-weight-heading-2xl); letter-spacing: var(--ds-font-tracking-tight); }
.ds-text-heading-xl { font-family: var(--ds-font-role-heading); font-size: var(--ds-font-size-heading-xl); line-height: var(--ds-font-line-tight); font-weight: var(--ds-font-weight-heading-xl); letter-spacing: var(--ds-font-tracking-tight); }
.ds-text-heading-lg { font-family: var(--ds-font-role-heading); font-size: var(--ds-font-size-heading-lg); line-height: var(--ds-font-line-snug); font-weight: var(--ds-font-weight-heading-lg); letter-spacing: var(--ds-font-tracking-tight); }
.ds-text-copy-lg { font-family: var(--ds-font-role-body); font-size: var(--ds-font-size-copy-lg); line-height: var(--ds-font-line-snug); font-weight: var(--ds-font-weight-copy-lg); letter-spacing: var(--ds-font-tracking-normal); }
.ds-text-copy-md { font-family: var(--ds-font-role-body); font-size: var(--ds-font-size-copy-md); line-height: var(--ds-font-line-normal); font-weight: var(--ds-font-weight-copy-md); letter-spacing: var(--ds-font-tracking-normal); }
.ds-text-copy-sm { font-family: var(--ds-font-role-body); font-size: var(--ds-font-size-copy-sm); line-height: var(--ds-font-line-normal); font-weight: var(--ds-font-weight-copy-sm); letter-spacing: var(--ds-font-tracking-normal); }
.ds-text-caption { font-family: var(--ds-font-role-body); font-size: var(--ds-font-size-caption); line-height: var(--ds-font-line-normal); font-weight: var(--ds-font-weight-caption); letter-spacing: var(--ds-font-tracking-normal); }
.ds-text-button-lg { font-family: var(--ds-font-role-ui); font-size: var(--ds-font-size-button-lg); line-height: var(--ds-font-line-none); font-weight: var(--ds-font-weight-button-lg); letter-spacing: var(--ds-font-tracking-normal); }
.ds-text-button-md { font-family: var(--ds-font-role-ui); font-size: var(--ds-font-size-button-md); line-height: var(--ds-font-line-none); font-weight: var(--ds-font-weight-button-md); letter-spacing: var(--ds-font-tracking-normal); }
.ds-text-button-sm { font-family: var(--ds-font-role-ui); font-size: var(--ds-font-size-button-sm); line-height: var(--ds-font-line-none); font-weight: var(--ds-font-weight-button-sm); letter-spacing: var(--ds-font-tracking-normal); }
.ds-text-label { font-family: var(--ds-font-role-ui); font-size: var(--ds-font-size-label); line-height: var(--ds-font-line-none); font-weight: var(--ds-font-weight-label); letter-spacing: var(--ds-font-tracking-wide); }
.ds-text-label-sm, .ds-text-eyebrow { font-family: var(--ds-font-role-ui); font-size: var(--ds-font-size-label-sm); line-height: var(--ds-font-line-none); font-weight: var(--ds-font-weight-label-sm); letter-spacing: var(--ds-font-tracking-wide); }

/* ---------- Brand assets ---------- */

/* BRAND_ASSET_GENERATED_START — source: brand/brand.json */
/* External SVGs render through a pseudo-element so the asset box can
   query its own width and select a small optical cut automatically. */
.ds-brand-asset {
  display: inline-block;
  flex: none;
  overflow: hidden;
  color: inherit;
  background: currentColor;
  vertical-align: middle;
  container: ds-brand-asset / inline-size;
  mask: var(--ds-brand-active-mask, var(--ds-brand-mask)) center / contain no-repeat;
  -webkit-mask: var(--ds-brand-active-mask, var(--ds-brand-mask)) center / contain no-repeat;
}
.ds-brand-asset__art {
  display: block;
  width: 100%;
  height: 100%;
  background: currentColor;
  mask: var(--ds-brand-active-mask, var(--ds-brand-mask)) center / contain no-repeat;
  -webkit-mask: var(--ds-brand-active-mask, var(--ds-brand-mask)) center / contain no-repeat;
}
.ds-brand-asset:has(> .ds-brand-asset__art) {
  background: none;
  mask: none;
  -webkit-mask: none;
}
.ds-brand-asset--mark {
  --ds-brand-aspect-ratio: 1;
  --ds-brand-mask: url(../brand/mark.svg);
  width: calc(var(--ds-space-8) * 1);
  height: var(--ds-space-8);
  aspect-ratio: var(--ds-brand-aspect-ratio);
}
.ds-brand-asset--wordmark {
  --ds-brand-aspect-ratio: 4.6875;
  --ds-brand-mask: url(../brand/wordmark.svg);
  width: calc(var(--ds-space-6) * 4.6875);
  height: var(--ds-space-6);
  aspect-ratio: var(--ds-brand-aspect-ratio);
}
.ds-brand-asset--wordmark-stacked {
  --ds-brand-aspect-ratio: 1.5;
  --ds-brand-mask: url(../brand/wordmark-stacked.svg);
  width: calc(calc(var(--ds-space-6) * 2) * 1.5);
  height: calc(var(--ds-space-6) * 2);
  aspect-ratio: var(--ds-brand-aspect-ratio);
}
.ds-brand-asset--lockup {
  --ds-brand-aspect-ratio: 5.9375;
  --ds-brand-mask: url(../brand/lockup.svg);
  width: calc(var(--ds-space-6) * 5.9375);
  height: var(--ds-space-6);
  aspect-ratio: var(--ds-brand-aspect-ratio);
}
[data-theme="dark"] .ds-brand-asset:not(.ds-brand-asset--on-light),
.ds-brand-asset--on-dark {
  --ds-brand-active-mask: var(--ds-brand-mask-on-dark, var(--ds-brand-mask));
}
.ds-brand-asset--on-light {
  --ds-brand-active-mask: var(--ds-brand-mask);
}
/* BRAND_ASSET_GENERATED_END */

/* ---------- Column grid ---------- */

/* Page-level scaffolding: columns, gutter, and margin come from the grid
   tokens and step at breakpoints (4 → 8 → 12). Children place with
   grid-column: span N. The media-query boundaries are the one place a
   token can't reach — the px literals mirror breakpoint.md / breakpoint.lg.
   For component-level layout reach for the primitives above, not this. */
.ds-grid {
  --ds-grid-cols: var(--ds-grid-columns-base);
  --ds-grid-gutter: var(--ds-grid-gutter-base);
  --ds-grid-margin: var(--ds-page-gutter);
  display: grid;
  grid-template-columns: repeat(var(--ds-grid-cols), minmax(0, 1fr));
  gap: var(--ds-grid-gutter);
  max-width: var(--ds-grid-max);
  margin-inline: auto;
  padding-inline: var(--ds-grid-margin);
}
@media (min-width: 640px) { /* breakpoint.sm */
  .ds-grid {
    --ds-grid-cols: var(--ds-grid-columns-sm);
    --ds-grid-gutter: var(--ds-grid-gutter-sm);
  }
}
@media (min-width: 768px) { /* breakpoint.md */
  .ds-grid {
    --ds-grid-cols: var(--ds-grid-columns-md);
    --ds-grid-gutter: var(--ds-grid-gutter-md);
  }
}
@media (min-width: 1024px) { /* breakpoint.lg */
  .ds-grid {
    --ds-grid-cols: var(--ds-grid-columns-lg);
    --ds-grid-gutter: var(--ds-grid-gutter-lg);
  }
}

/* ---------- Icons ---------- */

.ds-icon {
  width: var(--ds-size-icon-md);
  height: var(--ds-size-icon-md);
  flex: none;
}
.ds-icon--xs { width: var(--ds-size-icon-xs); height: var(--ds-size-icon-xs); }
.ds-icon--sm { width: var(--ds-size-icon-sm); height: var(--ds-size-icon-sm); }
.ds-icon--lg { width: var(--ds-size-icon-lg); height: var(--ds-size-icon-lg); }

/* ---------- Focus ring (one recipe, everywhere) ---------- */

:where(.ds-button, .ds-control, .ds-link, .ds-checkbox, .ds-radio, .ds-toggle, .ds-select__trigger, .ds-select__option,
       .ds-chip, .ds-chip__dismiss, .ds-tab, .ds-tabs__tab, .ds-menu__item, .ds-command__item, .ds-command__clear, .ds-pagination__page, .ds-crumbs__link, .ds-card--interactive, .ds-sidebar-nav__link, .ds-topbar__link, .ds-sidenav a, .ds-toast__dismiss):focus-visible,
.demo-focus {
  outline: var(--ds-focus-width) solid var(--ds-focus-ring);
  outline-offset: var(--ds-focus-offset);
}

/* Text-entry exception: focus on a text-entry control is a sustained engaged
   mode, and browsers show these controls' focus on pointer click too (the
   :focus-visible heuristic) — an offset ring would be a permanent fixture
   crowding the label above. The emphasis moves onto the border instead: the
   focus.ring color at the resting width — a crisp color change to full-
   contrast ink, no extra weight next to a text-selection highlight. Same
   focus color everywhere; only the placement differs. The transparent outline
   is invisible here but painted by forced-colors mode, where a border color
   change alone would flatten away. The Select trigger is a button — its
   focus is transient — and keeps the ring. */
input.ds-control:focus-visible, textarea.ds-control:focus-visible,
input.ds-control.demo-focus, textarea.ds-control.demo-focus {
  outline: var(--ds-focus-width) solid transparent;
  outline-offset: 0;
  border-color: var(--ds-focus-ring);
}
/* Engaged invalid keeps the error signal on the border. */
input.ds-control[aria-invalid="true"]:focus-visible, textarea.ds-control[aria-invalid="true"]:focus-visible,
input.ds-control[aria-invalid="true"].demo-focus, textarea.ds-control[aria-invalid="true"].demo-focus {
  border-color: var(--ds-status-error-border);
}

/* ---------- Button ---------- */

.ds-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--ds-space-2);
  height: var(--ds-size-button-md);
  /* One step below the old space.4, which puts a default Button's label on the
     same column as .ds-control's. They already shared a height token and the
     control radius role and disagreed only here, so a Button beside an Input
     read as misaligned, and a Menu anchored under a Button trigger put its
     rows 3px left of the label that opened them (F59). */
  padding: 0 var(--ds-space-3);
  /* Transparent, not none: --secondary paints a border and every other variant
     does not, so with `border: none` here the secondary sat 2px wider with its
     label 1px right of its peers — a variant changing geometry, which this
     system forbids outright. Reserving the border in the base makes the box
     model identical across variants and puts a Button's label on exactly the
     column .ds-control's sits on, border included (F59). */
  border: var(--ds-border-thin) solid transparent;
  border-radius: var(--ds-radius-control);
  font-size: var(--ds-font-size-button-md);
  font-weight: var(--ds-font-weight-medium);
  line-height: var(--ds-font-line-none);
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--ds-motion-duration-fast) var(--ds-motion-easing-standard);
}
.ds-button--sm { height: var(--ds-size-button-sm); padding: 0 var(--ds-space-2); font-size: var(--ds-font-size-button-sm); }
.ds-button--lg { height: var(--ds-size-button-lg); padding: 0 var(--ds-space-4); font-size: var(--ds-font-size-button-lg); }
/* Iconed side tightens exactly one step below the base padding (sm 1·2,
   md 2·3, lg 3·4) — the icon carries its own whitespace, so the visual margin
   it needs is smaller than a text edge's. This is a control's rule and stays
   one: a pill is self-contained, so the step costs nothing. It does not
   transfer to a list, where the rows share a rail (F54). */
.ds-button--leading-icon { padding-left: var(--ds-space-2); }
.ds-button--trailing-icon { padding-right: var(--ds-space-2); }
.ds-button--sm.ds-button--leading-icon { padding-left: var(--ds-space-1); }
.ds-button--sm.ds-button--trailing-icon { padding-right: var(--ds-space-1); }
.ds-button--lg.ds-button--leading-icon { padding-left: var(--ds-space-3); }
.ds-button--lg.ds-button--trailing-icon { padding-right: var(--ds-space-3); }

/* The monochrome Primary answers hover the same structural way as Danger:
   remove the fill and use the text-grade primary foreground for both ink and
   inset outline. This is not inversion — action.primary.text never becomes a
   background. A branded system may deliberately rebind this Button recipe to
   its filled action.primary.hover step when that better suits its accent and
   surface. :active follows :hover and restores the filled ladder. */
.ds-button--primary { background: var(--ds-action-primary-bg); color: var(--ds-action-primary-text); }
.ds-button--primary:hover, .ds-button--primary.demo-hover {
  background: transparent;
  color: var(--ds-action-primary-fg);
  box-shadow: inset 0 0 0 var(--ds-border-thin) var(--ds-action-primary-fg);
}
.ds-button--primary:active, .ds-button--primary.demo-active {
  background: var(--ds-action-primary-active);
  color: var(--ds-action-primary-text);
  box-shadow: none;
}

.ds-button--secondary {
  background: var(--ds-action-secondary-bg);
  color: inherit;
  /* Colour only — the base reserves the width. */
  border-color: color-mix(in srgb, currentColor 45%, transparent);
}
/* Secondary inherits the parent's paired foreground. Its outline is that same
   hue at lower presence, rises to full currentColor on hover, and returns to
   rest on press — the Link rule translated from underline to border. */
.ds-button--secondary:hover, .ds-button--secondary.demo-hover { border-color: currentColor; }
.ds-button--secondary:active, .ds-button--secondary.demo-active { background: var(--ds-action-secondary-bg); border-color: color-mix(in srgb, currentColor 45%, transparent); }
.ds-button--secondary:focus-visible { outline: var(--ds-focus-width) solid currentColor; }

/* Ghost is the quietest button, and it was not behaving like it: full-strength
   ink at rest and a control.bg-hover plate under the pointer, which read
   heavier than secondary's outline-only hover — the weight inversion the
   roadmap logged. It now rests at secondary ink and *lights up* to primary on
   hover, with a plate mixed from its own colour at low presence rather than a
   fixed neutral fill. Deriving from currentColor is what keeps it honest on a
   colored surface, where a neutral hover fill would be the wrong hue. */
.ds-button--ghost { background: transparent; color: var(--ds-context-text-secondary, var(--ds-text-secondary)); }
.ds-button--ghost:hover, .ds-button--ghost.demo-hover {
  background: color-mix(in srgb, currentColor var(--ds-action-ghost-hover), transparent);
  color: var(--ds-context-text-primary, var(--ds-text-primary));
}
.ds-button--ghost:active, .ds-button--ghost.demo-active { background: color-mix(in srgb, currentColor var(--ds-action-ghost-active), transparent); }

.ds-button--danger { background: var(--ds-action-danger-bg); color: var(--ds-action-danger-text); }
/* Danger hover drops the fill entirely — red outline + red text on the
   bare canvas; press returns this family to its filled treatment. */
.ds-button--danger:hover, .ds-button--danger.demo-hover {
  background: transparent;
  color: var(--ds-action-danger-bg);
  box-shadow: inset 0 0 0 var(--ds-border-thin) var(--ds-action-danger-bg);
}
.ds-button--danger:active, .ds-button--danger.demo-active {
  background: var(--ds-action-danger-bg);
  color: var(--ds-action-danger-text);
  box-shadow: none;
}

/* Disabled flattens color but preserves the variant's weight ladder:
   filled variants keep a grey fill, secondary keeps only its outline
   (disabled.bg matches secondary's rest fill, so a border alone cannot
   distinguish them), ghost keeps bare text — disabling must never add
   or equalize visual weight. */
.ds-button:disabled, .ds-button[aria-disabled="true"] {
  background: var(--ds-action-disabled-bg);
  color: var(--ds-action-disabled-text);
  border-color: transparent;
  box-shadow: none;
  cursor: not-allowed;
}
.ds-button--secondary:disabled, .ds-button--secondary[aria-disabled="true"] { background: transparent; color: inherit; border-color: currentColor; opacity: 0.45; }
.ds-button--ghost:disabled, .ds-button--ghost[aria-disabled="true"] { background: transparent; }

/* Icon-only button: accessible name required in markup. */
.ds-button--icon { width: var(--ds-size-button-md); padding: 0; }
.ds-button--icon.ds-button--sm { width: var(--ds-size-button-sm); }
.ds-button--icon.ds-button--lg { width: var(--ds-size-button-lg); }

/* Shape: pill for labeled buttons, circle when combined with --icon. */
.ds-button--rounded { border-radius: var(--ds-radius-full); }

/* The button owns icon sizing: labeled buttons pair icon.sm at sm/md and
   icon.md at lg; icon-only buttons track the control size (sm/md/lg).
   Explicit .ds-icon--* modifiers inside a button are deliberately inert. */
.ds-button .ds-icon { width: var(--ds-size-icon-sm); height: var(--ds-size-icon-sm); }
.ds-button--lg .ds-icon, .ds-button--icon .ds-icon { width: var(--ds-size-icon-md); height: var(--ds-size-icon-md); }
.ds-button--icon.ds-button--sm .ds-icon { width: var(--ds-size-icon-sm); height: var(--ds-size-icon-sm); }
.ds-button--icon.ds-button--lg .ds-icon { width: var(--ds-size-icon-lg); height: var(--ds-size-icon-lg); }

/* Loading: aria-busy keeps the button focusable and announces the busy
   state. The spinner joins as a prefix and replaces any icons; the label
   stays visible. currentColor keeps the spinner on-contract per variant. */
.ds-button[aria-busy="true"] { cursor: progress; }
.ds-button[aria-busy="true"] .ds-icon { display: none; }
/* The spinner is the leading glyph, so busy layout follows the
   leading-icon rules regardless of icon modifiers: iconed-side padding
   left, base padding right, spinner sized like a labeled icon. */
.ds-button[aria-busy="true"]:not(.ds-button--icon) { padding: 0 var(--ds-space-3) 0 var(--ds-space-2); }
.ds-button--sm[aria-busy="true"]:not(.ds-button--icon) { padding: 0 var(--ds-space-2) 0 var(--ds-space-1); }
.ds-button--lg[aria-busy="true"]:not(.ds-button--icon) { padding: 0 var(--ds-space-4) 0 var(--ds-space-3); }
.ds-button[aria-busy="true"]::before {
  content: "";
  flex: none;
  width: var(--ds-size-icon-sm);
  height: var(--ds-size-icon-sm);
  border: var(--ds-border-medium) solid currentColor;
  border-top-color: transparent;
  border-radius: var(--ds-radius-full);
  animation: ds-spin calc(var(--ds-motion-duration-slow) * 3) linear infinite;
}
.ds-button--lg[aria-busy="true"]::before { width: var(--ds-size-icon-md); height: var(--ds-size-icon-md); }
@keyframes ds-spin { to { transform: rotate(360deg); } }

/* ---------- Loading indicators ---------- */

.ds-spinner {
  display: inline-grid;
  place-items: center;
  flex: none;
  width: var(--ds-size-icon-md);
  height: var(--ds-size-icon-md);
  box-sizing: border-box;
  vertical-align: text-bottom;
}
.ds-spinner::before {
  content: "";
  display: block;
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  border: var(--ds-border-medium) solid currentColor;
  border-top-color: transparent;
  border-radius: var(--ds-radius-full);
  animation: ds-spin calc(var(--ds-motion-duration-slow) * 3) linear infinite;
}
.ds-spinner--sm { width: var(--ds-size-icon-sm); height: var(--ds-size-icon-sm); }
.ds-spinner--lg { width: var(--ds-size-icon-lg); height: var(--ds-size-icon-lg); }

/* A dot is a quarter of the box; the derivation var is the one indirection
   kept, re-anchored per size so the ratio is stated once per rule. */
.ds-loading-dots {
  --ds-loading-dot-size: calc(var(--ds-size-icon-md) / 4);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--ds-space-1);
  width: var(--ds-size-icon-md);
  height: var(--ds-size-icon-md);
  color: currentColor;
  vertical-align: middle;
}
.ds-loading-dots--sm { width: var(--ds-size-icon-sm); height: var(--ds-size-icon-sm); --ds-loading-dot-size: calc(var(--ds-size-icon-sm) / 4); }
.ds-loading-dots--lg { width: var(--ds-size-icon-lg); height: var(--ds-size-icon-lg); --ds-loading-dot-size: calc(var(--ds-size-icon-lg) / 4); }
.ds-loading-dots__dot {
  display: block;
  flex: none;
  width: var(--ds-loading-dot-size);
  height: var(--ds-loading-dot-size);
  border-radius: var(--ds-radius-full);
  background: currentColor;
  opacity: 0.32;
  animation-duration: calc(var(--ds-motion-duration-slow) * 6);
  animation-timing-function: var(--ds-motion-easing-standard);
  animation-iteration-count: infinite;
}
.ds-loading-dots__dot:nth-child(1) { animation-name: ds-dot-sweep-1; }
.ds-loading-dots__dot:nth-child(2) { animation-name: ds-dot-sweep-2; }
.ds-loading-dots__dot:nth-child(3) { animation-name: ds-dot-sweep-3; }
@keyframes ds-dot-sweep-1 {
  0%, 12%, 64%, 100% { opacity: 0.32; }
  24% { opacity: 1; }
  36% { opacity: 0.68; }
  48% { opacity: 0.46; }
}
@keyframes ds-dot-sweep-2 {
  0%, 12%, 76%, 100% { opacity: 0.32; }
  24% { opacity: 0.46; }
  36% { opacity: 1; }
  48% { opacity: 0.68; }
  60% { opacity: 0.46; }
}
@keyframes ds-dot-sweep-3 {
  0%, 24%, 88%, 100% { opacity: 0.32; }
  36% { opacity: 0.46; }
  48% { opacity: 1; }
  60% { opacity: 0.68; }
  72% { opacity: 0.46; }
}

/* ---------- Progress ---------- */

/* Determinate only. The fill's width is the value, so a bar with nothing to
   report is a Spinner's job, not this component's — that boundary is the
   whole reason both exist. Height is the only dimension a bar has, so it is
   the only thing the size axis moves; it is set per size rather than through
   a derivation var so the contract's height binding names a real token. */
.ds-progress {
  display: block;
  width: 100%;
  height: var(--ds-space-1-5);
  overflow: hidden;
  background: var(--ds-control-bg);
  border-radius: var(--ds-radius-full);
}
.ds-progress--sm { height: var(--ds-space-1); }
.ds-progress--lg { height: var(--ds-space-2); }

/* The host writes the width, because the number lives in aria-valuenow and
   CSS cannot read an attribute — the two must be set together or the bar and
   the screen reader disagree. Its own radius is stated rather than inherited
   so the fill stays a pill if a consumer ever drops the track's overflow. */
.ds-progress__fill {
  display: block;
  width: 0;
  height: 100%;
  background: var(--ds-action-primary-bg);
  border-radius: var(--ds-radius-full);
  transition: width var(--ds-motion-duration-base) var(--ds-motion-easing-standard);
}
@media (prefers-reduced-motion: reduce) {
  .ds-progress__fill { transition: none; }
}

/* ---------- Link ---------- */

/* Context owns contrast: the default inherits its parent's paired foreground,
   so it is dark on a light surface, light on a dark one, and hue-matched on a
   status surface. --ds-link-color is the explicit opt-in for a branded accent
   on surfaces where that color has been contrast-gated. The rest underline is
   the same ink at lower presence; hover raises that ink to full strength. */
.ds-link {
  color: var(--ds-link-color, inherit);
  text-decoration: underline;
  text-underline-offset: 0.2em;
  text-decoration-color: color-mix(in srgb, currentColor 45%, transparent);
  border-radius: var(--ds-radius-small);
}
.ds-link:hover, .ds-link.demo-hover { text-decoration-color: currentColor; }

/* ---------- Form controls ---------- */

.ds-control {
  height: var(--ds-size-button-md);
  padding: 0 var(--ds-space-3);
  border: var(--ds-border-thin) solid var(--ds-border-default);
  border-radius: var(--ds-radius-control);
  background: var(--ds-surface-page);
  color: var(--ds-text-primary);
  font-size: var(--ds-font-size-button-md);
  line-height: var(--ds-font-line-none);
  width: 100%;
  transition: border-color var(--ds-motion-duration-fast) var(--ds-motion-easing-standard);
}
.ds-control:hover, .ds-control.demo-hover { border-color: var(--ds-border-hover); }
.ds-control.demo-active { border-color: var(--ds-border-active); }
.ds-control::placeholder { color: var(--ds-text-muted); }
.ds-control:disabled { background: var(--ds-action-disabled-bg); color: var(--ds-action-disabled-text); cursor: not-allowed; }
/* [readonly] attribute, not :read-only — the pseudo-class also matches
   disabled controls and would override their background. */
.ds-control[readonly] { background: var(--ds-surface-canvas); }
.ds-control[aria-invalid="true"] { border-color: var(--ds-status-error-border); color: var(--ds-status-error-fg); }

/* No user resize: the drag handle can pull the control out of its container,
   and the need it serves (see more of your own text) belongs to auto-grow. */
textarea.ds-control { height: auto; min-height: calc(var(--ds-size-control-md) * 3); padding: var(--ds-space-2) var(--ds-space-3); resize: none; line-height: var(--ds-font-line-normal); }

/* Checkbox and radio share geometry. */
.ds-checkbox, .ds-radio {
  appearance: none;
  width: var(--ds-size-icon-md);
  height: var(--ds-size-icon-md);
  margin: 0;
  border: var(--ds-border-thin) solid var(--ds-border-strong);
  background: var(--ds-surface-page);
  display: inline-grid;
  place-content: center;
  /* Pin the replaced control's margin box to the surrounding line. Without
     this, an empty box and a checked box expose different inline baselines
     when ::after appears, which makes table rows jump on selection. */
  vertical-align: middle;
  cursor: pointer;
  flex: none;
}
/* Capped radius: square is the semantics (multi-select) and a circle is
   Radio — a round brand can soften the corners but never past a quarter of
   the box, so the shape distinction survives any rebrand. */
.ds-checkbox { border-radius: min(var(--ds-radius-small), 25%); }
.ds-radio { border-radius: var(--ds-radius-full); }
.ds-checkbox:checked, .ds-radio:checked { background: var(--ds-action-primary-bg); border-color: var(--ds-action-primary-bg); }
.ds-checkbox:checked::after {
  content: "";
  width: 0.35em;
  height: 0.6em;
  border: solid var(--ds-action-primary-text);
  border-width: 0 var(--ds-border-medium) var(--ds-border-medium) 0;
  /* The rotated check's ink centroid sits ~0.1em below geometric center, so
     it needs an upward optical correction — as a transform, because grid
     centering treats margins as part of the box and eats half a margin nudge. */
  transform: translateY(-0.1em) rotate(45deg);
}
.ds-radio:checked::after {
  content: "";
  width: 0.55em;
  height: 0.55em;
  border-radius: var(--ds-radius-full);
  background: var(--ds-action-primary-text);
}
.ds-checkbox:checked:not(:indeterminate):not(:disabled):not([aria-invalid="true"]):is(:hover, .demo-hover),
.ds-field__row:hover .ds-checkbox:checked:not(:indeterminate):not(:disabled):not([aria-invalid="true"]),
.ds-radio:checked:not(:disabled):not([aria-invalid="true"]):is(:hover, .demo-hover),
.ds-field__row:hover .ds-radio:checked:not(:disabled):not([aria-invalid="true"]) {
  background: var(--ds-action-primary-hover);
  border-color: var(--ds-action-primary-hover);
}
.ds-checkbox:disabled, .ds-radio:disabled { background: var(--ds-action-disabled-bg); border-color: var(--ds-border-default); cursor: not-allowed; }
.ds-checkbox[aria-invalid="true"], .ds-radio[aria-invalid="true"] { border-color: var(--ds-status-error-border); }
/* An unchecked hover previews the selection mark without claiming the filled
   state: primary mark + border on the unfilled surface. Selection reverses
   the mark onto the primary fill, whose hover stays filled and steps along
   the action.primary triad. The whole label row remains the pointer target. */
.ds-checkbox:not(:checked):not(:indeterminate):not(:disabled):not([aria-invalid="true"]):hover,
.ds-checkbox:not(:checked):not(:indeterminate):not(:disabled):not([aria-invalid="true"]).demo-hover,
.ds-field__row:hover .ds-checkbox:not(:checked):not(:indeterminate):not(:disabled):not([aria-invalid="true"]),
.ds-radio:not(:checked):not(:disabled):not([aria-invalid="true"]):hover,
.ds-radio:not(:checked):not(:disabled):not([aria-invalid="true"]).demo-hover,
.ds-field__row:hover .ds-radio:not(:checked):not(:disabled):not([aria-invalid="true"]) {
  background: var(--ds-surface-page);
  border-color: var(--ds-action-primary-bg);
}
.ds-checkbox:not(:checked):not(:indeterminate):not(:disabled):not([aria-invalid="true"]):is(:hover, .demo-hover)::after,
.ds-field__row:hover .ds-checkbox:not(:checked):not(:indeterminate):not(:disabled):not([aria-invalid="true"])::after {
  content: "";
  width: 0.35em;
  height: 0.6em;
  border: solid var(--ds-action-primary-bg);
  border-width: 0 var(--ds-border-medium) var(--ds-border-medium) 0;
  transform: translateY(-0.1em) rotate(45deg);
}
.ds-radio:not(:checked):not(:disabled):not([aria-invalid="true"]):is(:hover, .demo-hover)::after,
.ds-field__row:hover .ds-radio:not(:checked):not(:disabled):not([aria-invalid="true"])::after {
  content: "";
  width: 0.55em;
  height: 0.55em;
  border-radius: var(--ds-radius-full);
  background: var(--ds-action-primary-bg);
}
/* Indeterminate: a tri-state parent ("some children checked"). No HTML
   attribute exists — JS sets el.indeterminate; :indeterminate draws a dash
   in place of the check. Ordered after :checked so it wins if both are set. */
.ds-checkbox:indeterminate { background: var(--ds-action-primary-bg); border-color: var(--ds-action-primary-bg); }
.ds-checkbox:indeterminate::after {
  content: "";
  width: 0.5em;
  height: var(--ds-border-medium);
  border-radius: var(--ds-radius-full);
  background: var(--ds-action-primary-text);
}
.ds-checkbox:indeterminate:not(:disabled):not([aria-invalid="true"]):is(:hover, .demo-hover),
.ds-field__row:hover .ds-checkbox:indeterminate:not(:disabled):not([aria-invalid="true"]) {
  background: var(--ds-action-primary-hover);
  border-color: var(--ds-action-primary-hover);
}

/* Toggle — sliding on/off control (Geist calls this "Toggle"; role=switch).
   Track height drives everything: width is a fixed ~1.8:1 stretch, the thumb
   is the track height minus its inset on both sides, and travel is whatever
   room is left (width - thumb - 2·inset) so the thumb lands flush on the far
   edge at any size. Neutral proportions on purpose — a brand nudges toward an
   iOS look by widening the ratio, it isn't baked in. */
.ds-toggle {
  --_thumb: calc(var(--ds-size-icon-md) - var(--ds-border-medium) * 2);
  --_travel: calc(var(--ds-size-icon-lg) + var(--ds-space-3) - var(--ds-size-icon-md));
  appearance: none;
  width: calc(var(--ds-size-icon-lg) + var(--ds-space-3));
  height: var(--ds-size-icon-md);
  margin: 0;
  border: none;
  border-radius: var(--ds-radius-full);
  background: var(--ds-border-strong);
  position: relative;
  cursor: pointer;
  transition: background-color var(--ds-motion-duration-base) var(--ds-motion-easing-standard);
  flex: none;
}
.ds-toggle::after {
  content: "";
  position: absolute;
  inset: var(--ds-border-medium);
  width: var(--_thumb);
  border-radius: var(--ds-radius-full);
  background: var(--ds-surface-page);
  transition: translate var(--ds-motion-duration-base) var(--ds-motion-easing-standard);
}
.ds-toggle:checked { background: var(--ds-action-primary-bg); }
.ds-toggle:checked::after { translate: var(--_travel) 0; }
.ds-toggle:disabled { background: var(--ds-action-disabled-bg); cursor: not-allowed; }
/* Sizes share the one dialect; height carries the icon token, width holds the
   ~1.8:1 stretch, and thumb + travel (= width − height, the insets cancel)
   follow from those two so proportions stay constant across sizes. */
.ds-toggle--sm {
  width: calc(var(--ds-size-icon-md) + var(--ds-space-2));
  height: var(--ds-size-icon-sm);
  --_thumb: calc(var(--ds-size-icon-sm) - var(--ds-border-medium) * 2);
  --_travel: calc(var(--ds-size-icon-md) + var(--ds-space-2) - var(--ds-size-icon-sm));
}
.ds-toggle--lg {
  width: calc(var(--ds-size-control-md) + var(--ds-space-1));
  height: var(--ds-size-icon-lg);
  --_thumb: calc(var(--ds-size-icon-lg) - var(--ds-border-medium) * 2);
  --_travel: calc(var(--ds-size-control-md) + var(--ds-space-1) - var(--ds-size-icon-lg));
}

/* Segmented control — pick-one-of-N as connected segments. A radiogroup of
   visually-hidden native radios: the browser gives arrow-key nav + single
   select for free, so the L1 reference is fully functional with no JS. Outlined
   style (light in dark UI): the track is transparent with a hairline border and
   the selected segment is a raised bordered cell (surface.raised + border), not
   a filled groove. Each segment reserves a transparent border so selecting adds
   no layout shift; :has() reads the label's own radio for its state. */
.ds-segmented {
  display: inline-flex;
  align-items: stretch;
  height: var(--ds-size-control-md);
  padding: var(--ds-space-1);
  background: transparent;
  border: var(--ds-border-thin) solid var(--ds-border-default);
  border-radius: var(--ds-radius-control);
}
.ds-segmented--sm { height: var(--ds-size-control-sm); }
.ds-segmented--lg { height: var(--ds-size-control-lg); }
.ds-segmented__segment {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--ds-space-2);
  padding: 0 var(--ds-space-3);
  border: var(--ds-border-thin) solid transparent;
  border-radius: max(0px, calc(var(--ds-radius-control) - var(--ds-space-1)));
  font-size: var(--ds-font-size-button-md);
  font-weight: var(--ds-font-weight-medium);
  line-height: var(--ds-font-line-none);
  color: var(--ds-text-secondary);
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--ds-motion-duration-fast) var(--ds-motion-easing-standard),
              background-color var(--ds-motion-duration-fast) var(--ds-motion-easing-standard),
              border-color var(--ds-motion-duration-fast) var(--ds-motion-easing-standard);
}
.ds-segmented--sm .ds-segmented__segment { font-size: var(--ds-font-size-button-sm); }
.ds-segmented--lg .ds-segmented__segment { font-size: var(--ds-font-size-button-lg); }
/* The real radio: focusable and clickable, but painted by its label. */
.ds-segmented__segment input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  opacity: 0;
}
.ds-segmented__segment:hover, .ds-segmented__segment.demo-hover { color: var(--ds-text-primary); }
.ds-segmented__segment:has(:checked) {
  background: var(--ds-selection-bg);
  color: var(--ds-text-primary);
  border-color: var(--ds-selection-bg);
}
.ds-segmented__segment:has(:focus-visible) {
  outline: var(--ds-focus-width) solid var(--ds-focus-ring);
  outline-offset: var(--ds-focus-offset);
}
.ds-segmented__segment:has(:disabled) { color: var(--ds-action-disabled-text); cursor: not-allowed; }

/* Slider — pick one value from a continuous range.

   Native input[type=range] is the only L1-honest base: it ships dragging,
   arrow/Home/End keys, and the value announcement with no runtime at all,
   which is exactly the layer test. A div-and-JS slider would be L2 by
   definition.

   The cost is that the track and thumb are vendor pseudo-elements, so every
   declaration block is written twice. WebKit and Firefox selectors cannot
   share one selector list — an engine drops the whole rule when any selector
   in the list is unknown to it — so the duplication is structural, not
   laziness. Change the pair together. */
.ds-slider {
  /* The filled portion. It defaults to 0%, so a consumer with no runtime gets
     a plain, honest track rather than a fill frozen at the wrong value; L2
     writes this percentage on input, beside the value it already owns. */
  --ds-slider-fill: 0%;
  /* The rail height, in one place. Every rail sets only this, and the track's
     height and the thumb's centring offset both derive from it — so a rail can
     never move without the knob following. Written as arithmetic per rail, the
     thumb drifted off-centre the moment a height changed. */
  --ds-slider-rail: var(--ds-space-1-5);
  /* The handle's width, declared rather than written into the two engine
     rules, because the fill has to know it: a range thumb travels inset by
     half its own width at each end, while a gradient runs the full track. The
     two only agree at the midpoint unless the fill is corrected by this. */
  --ds-slider-thumb: var(--ds-space-2);
  -webkit-appearance: none;
  appearance: none;
  display: block;
  width: 100%;
  /* The box is the thumb's bounding box, not a control-height row. At
     size.control.md it stood 40px tall around a 6px groove, so 17px of
     invisible box sat above the track and a label stacked over a slider read
     ~25px away while the same stack over a Progress bar read 8px — the same
     token, two different optical gaps. Sized to the thumb instead, the two
     bars stack alike. The thick rail is the one case where the rail is taller
     than the thumb, so it sets its own box. */
  height: var(--ds-size-icon-lg);
  margin: 0;
  background: transparent;
  cursor: pointer;
}
.ds-slider:disabled { cursor: not-allowed; }

/* Track. The fill is a gradient hard stop rather than a second element,
   because a pseudo-element cannot host a child. Its geometry matches
   Progress: the two read as the same bar doing different jobs. */
.ds-slider::-webkit-slider-runnable-track {
  height: var(--ds-slider-rail);
  border-radius: var(--ds-radius-full);
  background: linear-gradient(to right, var(--ds-action-primary-bg) var(--ds-slider-fill), var(--ds-control-bg) var(--ds-slider-fill));
}
.ds-slider::-moz-range-track {
  height: var(--ds-slider-rail);
  border-radius: var(--ds-radius-full);
  background: linear-gradient(to right, var(--ds-action-primary-bg) var(--ds-slider-fill), var(--ds-control-bg) var(--ds-slider-fill));
}

/* Rail is the one axis: how thick the groove is. The thumb is deliberately
   constant across all three — a handle that changed size with its rail would
   make the axis mean two things. Thin and default keep the pill radius; thick
   is tall enough that radius.full would bend it into a lozenge, so it takes
   radius.lg and reads as the rounded container it is. */
.ds-slider--thin { --ds-slider-rail: var(--ds-space-1); }
/* Thick matches the small button's height, so a slider and an sm Button sit on
   one row without either setting the rhythm. The rail is taller than the thumb
   here, so it also sets the box, and takes the button's radius with it. */
.ds-slider--thick { --ds-slider-rail: var(--ds-size-control-sm); height: var(--ds-size-control-sm); }
.ds-slider--thick::-webkit-slider-runnable-track { border-radius: var(--ds-radius-control); }
.ds-slider--thick::-moz-range-track { border-radius: var(--ds-radius-control); }
/* At this size a full-strength fill is a slab: it covers most of the control
   and outweighs the knob that carries the value. A fifth of the fill's
   presence against the groove keeps it readable while the handle stays the mark —
   the same reason chart.series.1 is a mid step rather than an extreme. */
.ds-slider--thick::-webkit-slider-runnable-track {
  background: linear-gradient(to right, color-mix(in srgb, var(--ds-action-primary-bg) 20%, var(--ds-control-bg)) var(--ds-slider-fill), var(--ds-control-bg) var(--ds-slider-fill));
}
.ds-slider--thick::-moz-range-track {
  background: linear-gradient(to right, color-mix(in srgb, var(--ds-action-primary-bg) 20%, var(--ds-control-bg)) var(--ds-slider-fill), var(--ds-control-bg) var(--ds-slider-fill));
}

/* Thumb. The fill rides the generic control triad because a slider handle is
   exactly what control.bg names; the ring stays action.primary.bg so the
   handle keeps its weight against both halves of the track. WebKit gives the
   thumb no alignment of its own, so it is centred by hand against the track
   height; Firefox centres it already, which is the one place the two rules
   legitimately differ. */
.ds-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  box-sizing: border-box;
  width: var(--ds-slider-thumb);
  height: var(--ds-size-icon-lg);
  margin-top: calc((var(--ds-slider-rail) - var(--ds-size-icon-lg)) / 2);
  border-radius: var(--ds-radius-full);
  background: var(--ds-action-primary-bg);
}
.ds-slider::-moz-range-thumb {
  box-sizing: border-box;
  width: var(--ds-slider-thumb);
  height: var(--ds-size-icon-lg);
  border: none;
  border-radius: var(--ds-radius-full);
  background: var(--ds-action-primary-bg);
}
/* The handle and Button share one primary triad, so a rebrand chooses one
   contrast-safe state direction for every primary fill. */
.ds-slider:not(:disabled):hover::-webkit-slider-thumb,
.ds-slider:not(:disabled).demo-hover::-webkit-slider-thumb { background: var(--ds-action-primary-hover); }
.ds-slider:not(:disabled):hover::-moz-range-thumb,
.ds-slider:not(:disabled).demo-hover::-moz-range-thumb { background: var(--ds-action-primary-hover); }
.ds-slider:not(:disabled):active::-webkit-slider-thumb,
.ds-slider:not(:disabled).demo-active::-webkit-slider-thumb { background: var(--ds-action-primary-active); }
.ds-slider:not(:disabled):active::-moz-range-thumb,
.ds-slider:not(:disabled).demo-active::-moz-range-thumb { background: var(--ds-action-primary-active); }

/* Focus placement exception, for the text-entry rule's reason: the ring
   belongs on the thing that moves, not on a full-width box that would fence
   the whole row. Same focus color and width everywhere; only the placement
   differs. The input's own outline is suppressed so the two never stack. */
.ds-slider:focus-visible, .ds-slider.demo-focus { outline: none; }
.ds-slider:focus-visible::-webkit-slider-thumb,
.ds-slider.demo-focus::-webkit-slider-thumb {
  outline: var(--ds-focus-width) solid var(--ds-focus-ring);
  outline-offset: var(--ds-focus-offset);
}
.ds-slider:focus-visible::-moz-range-thumb,
.ds-slider.demo-focus::-moz-range-thumb {
  outline: var(--ds-focus-width) solid var(--ds-focus-ring);
  outline-offset: var(--ds-focus-offset);
}

/* Disabled drops the filled track to a mid neutral. It used to keep painting
   action.primary.bg, so a disabled slider read louder than its own flattened
   handle — the fill was the most emphatic thing on an inert control.
   border.strong lands about halfway between the primary fill and the groove
   in both modes, which is the intent. action.disabled.bg cannot do this job:
   it resolves to the same value as control.bg, so the fill would vanish into
   the groove entirely rather than quieten. A border role used as a fill
   follows Skeleton, which already paints border.subtle. */
.ds-slider:disabled::-webkit-slider-runnable-track {
  background: linear-gradient(to right, var(--ds-border-strong) var(--ds-slider-fill), var(--ds-control-bg) var(--ds-slider-fill));
}
.ds-slider:disabled::-moz-range-track {
  background: linear-gradient(to right, var(--ds-border-strong) var(--ds-slider-fill), var(--ds-control-bg) var(--ds-slider-fill));
}

/* Disabled flattens the handle to a mid neutral rather than the disabled fill:
   with no ring left to carry its shape, action.disabled.bg would leave the
   handle nearly invisible against the groove it sits on. */
.ds-slider:disabled::-webkit-slider-thumb { background: var(--ds-border-strong); }
.ds-slider:disabled::-moz-range-thumb { background: var(--ds-border-strong); }

/* Field wrapper */
.ds-field { display: grid; gap: var(--ds-space-2); }
.ds-field-row {
  display: grid;
  gap: var(--ds-space-5);
}
/* One aligned row is exactly one field pair. At md and wider, the pair
   shares three natural-height tracks: label, control, and optional message.
   An absent message leaves its cell empty; no fixed caption height is needed.
   Below md this media rule does not apply, so the same source order becomes
   the ordinary space.5 field stack defined above. */
@media (min-width: 768px) { /* breakpoint.md */
  .ds-field-row {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-template-rows: repeat(3, auto);
    column-gap: var(--ds-space-5);
    row-gap: var(--ds-space-2);
  }
  .ds-field-row > .ds-field {
    grid-row: 1 / span 3;
    grid-template-rows: subgrid;
  }
}
.ds-choice-group {
  box-sizing: border-box;
  display: grid;
  min-inline-size: 0;
  margin: 0;
  padding: var(--ds-space-4) var(--ds-space-5) var(--ds-space-5);
  gap: var(--ds-space-4);
  border: var(--ds-border-thin) solid var(--ds-border-subtle);
  border-radius: var(--ds-radius-control);
  background: transparent;
}
.ds-choice-group__legend {
  margin-inline-start: calc(-1 * var(--ds-space-1));
  padding-inline: var(--ds-space-1);
  color: var(--ds-text-secondary);
  font-size: var(--ds-font-size-label);
  font-weight: var(--ds-font-weight-medium);
  line-height: var(--ds-font-line-none);
}
.ds-choice-group__options {
  display: grid;
  gap: var(--ds-space-3);
}
.ds-choice-group__options--nested {
  gap: var(--ds-space-2);
  padding-inline-start: var(--ds-space-6);
}
.ds-choice-group__help,
.ds-choice-group__error {
  font-size: var(--ds-font-size-caption);
  line-height: var(--ds-font-line-normal);
}
.ds-choice-group__help { color: var(--ds-text-secondary); }
.ds-choice-group__error {
  display: inline-flex;
  align-items: flex-start;
  gap: var(--ds-space-1);
  color: var(--ds-status-error-fg);
}
.ds-choice-group[aria-invalid="true"] { border-color: var(--ds-status-error-border); }
.ds-choice-group[aria-invalid="true"] > .ds-choice-group__legend { color: var(--ds-status-error-fg); }
.ds-field__label { font-size: var(--ds-font-size-label); font-weight: var(--ds-font-weight-medium); color: var(--ds-text-secondary); line-height: var(--ds-font-line-none); }
.ds-field__help { font-size: var(--ds-font-size-caption); color: var(--ds-text-secondary); line-height: var(--ds-font-line-normal); }
.ds-field__error {
  font-size: var(--ds-font-size-caption);
  color: var(--ds-status-error-fg);
  line-height: var(--ds-font-line-normal);
  display: inline-flex;
  align-items: flex-start;
  gap: var(--ds-space-1);
}
/* Caption copy resolves to an 18px line box while its leading icon is 16px.
   Keep the row anchored to the first line for wrapping messages, then lower
   only the icon by the 1px half-difference so it is optically centred on that
   line instead of riding against its top edge. */
.ds-field__error > .ds-icon:first-child,
.ds-choice-group__error > .ds-icon:first-child {
  margin-block-start: var(--ds-border-thin);
}
/* The display above would otherwise defeat the hidden attribute — the
   idiomatic way to clear the message when the field turns valid. */
.ds-field__error[hidden] { display: none; }
/* Invalid colors the whole block: the control already reddens its value and
   border, so the label joins via :has() on the same aria-invalid that drives
   everything else — no extra wiring. Clearing aria-invalid clears it all. */
.ds-field:has([aria-invalid="true"]) .ds-field__label { color: var(--ds-status-error-fg); }
.ds-field__row:has([aria-invalid="true"]) { color: var(--ds-status-error-fg); }
/* An error under a row describes the row's control, and its icon matches the
   16px control exactly — so the caption adopts the row's gap and its text
   aligns with the label column. Stacked errors keep the tight phrase gap. */
.ds-field__row + .ds-field__error { gap: var(--ds-space-2); }
.ds-field__row { display: inline-flex; align-items: center; gap: var(--ds-space-2); font-size: var(--ds-font-size-copy-sm); cursor: pointer; }

/* ---------- Select ---------- */

/* The trigger wears the shared ds-control recipe (border states, focus
   ring, invalid, disabled all inherit from it); the menu and options wear
   the shared popover recipe grouped in the Menu section below. This block
   holds only what makes a Select a Select. */
.ds-select {
  position: relative;
  width: 100%;
}
.ds-select__trigger {
  appearance: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--ds-space-2);
  height: var(--ds-size-button-md);
  padding: 0 var(--ds-space-2) 0 var(--ds-space-3);
  font-size: var(--ds-font-size-button-md);
  text-align: left;
  cursor: pointer;
}
/* The open trigger is the border.active consumer: engaged-but-not-focus-
   ring — the ring stays reserved for :focus-visible. */
.ds-select__trigger[aria-expanded="true"] { border-color: var(--ds-border-active); }
/* No value chosen yet: the shown text is placeholder-grade, same contract
   as Input's ::placeholder — a hint, never essential. */
.ds-select__trigger[data-placeholder] { color: var(--ds-text-muted); }
/* The select owns its chevron size, like the button owns its icons;
   inline ds-icon--* modifiers on the chevron are deliberately inert. */
.ds-select__trigger .ds-icon {
  width: var(--ds-size-icon-sm);
  height: var(--ds-size-icon-sm);
  transition: transform var(--ds-motion-duration-fast) var(--ds-motion-easing-standard);
}
.ds-select__trigger[aria-expanded="true"] .ds-icon {
  transform: rotate(180deg);
}
/* The listbox spans the trigger; [hidden] works natively (no display set).
   It caps at eight menu-height rows and scrolls past that — a bounded
   list is short, but nothing guarantees it fits the viewport. */
.ds-select__listbox {
  position: absolute;
  top: calc(100% + var(--ds-space-1));
  left: 0;
  margin: 0;
  list-style: none;
  width: 100%;
  min-width: 0;
  max-height: calc(var(--ds-size-control-sm) * 8);
  overflow-y: auto;
}
/* Selected is marked by a check alone — no fill, no weight change. The
   highlight (hover/keyboard) owns the background, so selection and
   highlight never fight for the same channel; a selected-and-highlighted
   row reads as one highlighted row wearing its check.
   The mark and its slot are Menu's, declared with Menu's below: an option is
   a menu row (F52), so its selection marker is a menu row's. This used to
   hand-draw Checkbox's rotated border instead, which put a 5x8px em-derived
   glyph where the system's check is a 16px icon, and reserved nothing on the
   unselected rows (F56). Checkbox draws its tick in CSS because a native
   input cannot hold a child element — that is a constraint, not a style, and
   it does not transfer to an element that can. */

/* ---------- Card ---------- */

.ds-card {
  display: block;
  background: var(--ds-surface-panel);
  color: var(--ds-text-primary);
  border: var(--ds-border-thin) solid var(--ds-border-subtle);
  border-radius: var(--ds-radius-container);
  padding: var(--ds-space-5);
}
.ds-card--raised { background: var(--ds-surface-raised); box-shadow: var(--ds-shadow-low); }
/* The interactive path is an anchor root for one destination, never a div
   with a click handler and never a container for nested controls. Border is
   the state channel; raised remains an independent surface choice. */
.ds-card--interactive { text-decoration: none; cursor: pointer; }
.ds-card--interactive:hover { border-color: var(--ds-border-hover); }
.ds-card--interactive:active { border-color: var(--ds-border-active); }
/* Card owns the surface and inset, never the rhythm between its regions.
   Stack / Inline compose those; the named parts supply only their own voice
   and, for the footer, the stable action alignment. */
.ds-card__header { min-width: 0; }
/* Parts pin their own type: host prose rules must not resize content inside a
   component. No trailing margins — the surrounding primitive owns spacing. */
.ds-card__title { margin: 0; color: inherit; font-size: var(--ds-font-size-copy-md); line-height: var(--ds-font-line-snug); font-weight: var(--ds-font-weight-semibold); }
.ds-card__body { margin: 0; min-width: 0; color: var(--ds-text-secondary); font-size: var(--ds-font-size-copy-sm); line-height: var(--ds-font-line-normal); }
.ds-card__footer { display: flex; align-items: center; justify-content: flex-end; flex-wrap: wrap; gap: var(--ds-space-2); margin-top: var(--ds-space-4); }

/* ---------- Badge ---------- */

.ds-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--ds-space-1);
  padding: 0 var(--ds-space-2);
  height: var(--ds-size-icon-lg);
  border-radius: var(--ds-radius-full);
  font-size: var(--ds-font-size-caption);
  font-weight: var(--ds-font-weight-medium);
  background: var(--ds-metadata-bg);
  color: var(--ds-metadata-text);
}
.ds-badge--sm { height: var(--ds-size-icon-md); }
.ds-badge--lg { height: var(--ds-size-control-sm); padding: 0 var(--ds-space-3); font-size: var(--ds-font-size-copy-sm); }
/* Icon-side padding sits one spacing step below the text side (space.1-5
   under space.2; space.2 under space.3) — still pulled in, but with more air
   than the geometric inset because the pill's round cap eats into the
   perceived space next to the glyph. */
.ds-badge--leading-icon { padding-left: var(--ds-space-1-5); }
.ds-badge--trailing-icon { padding-right: var(--ds-space-1-5); }
.ds-badge--lg.ds-badge--leading-icon { padding-left: var(--ds-space-2); }
.ds-badge--lg.ds-badge--trailing-icon { padding-right: var(--ds-space-2); }
/* Badge and Chip share a compact pill-icon ladder. It rises 12/14/16 rather
   than the general icon scale's 12/16/20, keeping metadata glyphs subordinate
   to their labels; icons and spinners ignore inline size modifiers. */
.ds-badge .ds-icon, .ds-badge .ds-spinner { width: var(--ds-size-pill-icon-md); height: var(--ds-size-pill-icon-md); }
.ds-badge--sm .ds-icon, .ds-badge--sm .ds-spinner { width: var(--ds-size-pill-icon-sm); height: var(--ds-size-pill-icon-sm); }
.ds-badge--lg .ds-icon, .ds-badge--lg .ds-spinner { width: var(--ds-size-pill-icon-lg); height: var(--ds-size-pill-icon-lg); }
.ds-badge--neutral { background: var(--ds-metadata-bg); color: var(--ds-metadata-text); }
.ds-badge--success { background: var(--ds-status-success-bg); color: var(--ds-status-success-text); }
.ds-badge--warning { background: var(--ds-status-warning-bg); color: var(--ds-status-warning-text); }
.ds-badge--error   { background: var(--ds-status-error-bg);   color: var(--ds-status-error-text); }
.ds-badge--info    { background: var(--ds-status-info-bg);    color: var(--ds-status-info-text); }
.ds-badge--accent  { background: var(--ds-category-accent-bg); color: var(--ds-category-accent-text); }
.ds-badge--inverted { background: var(--ds-text-primary); color: var(--ds-text-inverse); }
/* Solid emphasis: same intents, prominent tier. Subtle (the plain intent
   variant) is the default; add --solid for moments that must not be missed. */
.ds-badge--success.ds-badge--solid { background: var(--ds-status-success-solid-bg); color: var(--ds-status-success-solid-text); }
.ds-badge--warning.ds-badge--solid { background: var(--ds-status-warning-solid-bg); color: var(--ds-status-warning-solid-text); }
.ds-badge--error.ds-badge--solid   { background: var(--ds-status-error-solid-bg);   color: var(--ds-status-error-solid-text); }
.ds-badge--info.ds-badge--solid    { background: var(--ds-status-info-solid-bg);    color: var(--ds-status-info-solid-text); }
.ds-badge--accent.ds-badge--solid  { background: var(--ds-category-accent-solid-bg); color: var(--ds-category-accent-solid-text); }

/* ---------- Chip ---------- */

/* Badge's pill made interactive, which is exactly why it is a separate
   component rather than a Badge variant: interactivity changes the element,
   the states, and the keyboard map. It matches Badge's scale exactly —
   same three heights, same paddings, same glyph ladder — because chips and
   badges share rows in dense interfaces and two pill scales in one table read
   as a mistake. It departs on colour only: the generic control triad, because
   a chip is precisely the "generic control" those roles name.

   The scale has an accessibility floor worth knowing. WCAG 2.5.8 asks for a
   24px minimum target: md meets it exactly, sm (20px) does not and leans on
   the spacing exception. The contract carries the rule; this note exists so
   nobody tidies sm smaller. */
.ds-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--ds-space-1);
  height: var(--ds-size-icon-lg);
  padding: 0 var(--ds-space-2);
  border: none;
  border-radius: var(--ds-radius-full);
  font-family: inherit;
  font-size: var(--ds-font-size-caption);
  font-weight: var(--ds-font-weight-medium);
  line-height: var(--ds-font-line-none);
  background: var(--ds-control-bg);
  color: var(--ds-text-secondary);
  cursor: pointer;
}
/* The chip sizes its own glyph, Badge's rule — markup never carries an icon
   size modifier. Every value below is Badge's, line for line. */
.ds-chip .ds-icon { width: var(--ds-size-pill-icon-md); height: var(--ds-size-pill-icon-md); }
.ds-chip--leading-icon { padding-left: var(--ds-space-1-5); }
.ds-chip--sm { height: var(--ds-size-icon-md); }
.ds-chip--sm .ds-icon { width: var(--ds-size-pill-icon-sm); height: var(--ds-size-pill-icon-sm); }
.ds-chip--lg { height: var(--ds-size-control-sm); padding: 0 var(--ds-space-3); font-size: var(--ds-font-size-copy-sm); }
.ds-chip--lg .ds-icon { width: var(--ds-size-pill-icon-lg); height: var(--ds-size-pill-icon-lg); }
.ds-chip--lg.ds-chip--leading-icon { padding-left: var(--ds-space-2); }

.ds-chip:not(:disabled):not([aria-disabled="true"]):not([aria-pressed="true"]):hover,
.ds-chip:not(:disabled):not([aria-disabled="true"]):not([aria-pressed="true"]).demo-hover { background: var(--ds-control-bg-hover); }
.ds-chip:not(:disabled):not([aria-disabled="true"]):not([aria-pressed="true"]):active,
.ds-chip:not(:disabled):not([aria-disabled="true"]):not([aria-pressed="true"]).demo-active { background: var(--ds-control-bg-active); }

/* Selected takes the inverted pair rather than selection.bg. selection.bg and
   control.bg resolve to the same neutral step in dark mode, so a selected chip
   would be invisible against an unselected one exactly where a filter row most
   needs to be read at a glance.

   The unselected hover above is deliberately excluded from the pressed state:
   at (0,4,0) it outranked this rule at (0,2,0), so hovering a selected filter
   dropped it to the grey control fill and read as already un-pressed. */
.ds-chip[aria-pressed="true"] {
  background: var(--ds-action-primary-bg);
  color: var(--ds-action-primary-text);
}
.ds-chip[aria-pressed="true"]:not(:disabled):hover,
.ds-chip[aria-pressed="true"]:not(:disabled).demo-hover { background: var(--ds-action-primary-hover); }
.ds-chip[aria-pressed="true"]:not(:disabled):active,
.ds-chip[aria-pressed="true"]:not(:disabled).demo-active { background: var(--ds-action-primary-active); }

.ds-chip:disabled, .ds-chip[aria-disabled="true"] {
  background: var(--ds-action-disabled-bg);
  color: var(--ds-action-disabled-text);
  cursor: not-allowed;
}

/* Removable: the root cannot be a button, because the dismiss control inside
   it is one and a button inside a button is invalid. The root becomes a span,
   the X owns the whole interaction, and the label is inert text — which is why
   the removable shape has no pressed state and no hover on the pill itself. */
.ds-chip--removable {
  padding-right: var(--ds-space-1);
  cursor: default;
}
.ds-chip--removable:hover, .ds-chip--removable.demo-hover { background: var(--ds-control-bg); }
.ds-chip__dismiss {
  display: inline-grid;
  place-items: center;
  flex: none;
  width: var(--ds-size-icon-md);
  height: var(--ds-size-icon-md);
  padding: 0;
  border: none;
  border-radius: var(--ds-radius-full);
  background: transparent;
  color: inherit;
  cursor: pointer;
}
/* Presence, not fill: the X darkens to the primary ink rather than growing a
   plate inside a pill that is already a fill — Toast's dismiss reasoning. */
.ds-chip__dismiss:hover, .ds-chip__dismiss.demo-hover { color: var(--ds-text-primary); }
.ds-chip__dismiss .ds-icon { width: var(--ds-size-icon-xs); height: var(--ds-size-icon-xs); }
/* The dismiss steps with the pill, always 4px inside its height. */
.ds-chip--sm .ds-chip__dismiss { width: var(--ds-size-icon-sm); height: var(--ds-size-icon-sm); }
.ds-chip--lg .ds-chip__dismiss { width: var(--ds-size-icon-lg); height: var(--ds-size-icon-lg); }
.ds-chip--lg .ds-chip__dismiss .ds-icon { width: var(--ds-size-icon-sm); height: var(--ds-size-icon-sm); }

/* ---------- Alert ---------- */

.ds-alert {
  /* Toast's shape: a flex row of icon and content, always top-aligned. CSS
     cannot count wrapped lines, and a centred icon beside a body that wraps
     floats at the middle of the block instead of sitting with the line it
     labels. Top alignment costs nothing on one line — the glyph is
     size.icon.md against a copy-sm line box, within a pixel of centred — and
     is the only correct answer once the text wraps. (Toast can't top-align
     its single line the same way: its X's icon.lg box sets the row height,
     so that case centres instead.) */
  display: flex;
  align-items: flex-start;
  gap: var(--ds-space-3);
  padding: var(--ds-space-4);
  border-radius: var(--ds-radius-control);
  border: var(--ds-border-thin) solid var(--ds-border-subtle);
  font-size: var(--ds-font-size-copy-sm);
}
/* The row is the icon and one content block, so title and body must be
   wrapped — as bare children they become sibling flex items and the text stops
   stacking. Grows into the row; min-width:0 lets long words wrap instead of
   widening the alert past its container. */
.ds-alert__content { display: grid; gap: var(--ds-space-1); flex: 1 1 0; min-width: 0; }
/* The alert owns its icon size — inline ds-icon--* modifiers are inert, as in
   Button and Toast — so a status glyph can never arrive at the wrong scale. */
.ds-alert .ds-icon { width: var(--ds-size-icon-md); height: var(--ds-size-icon-md); }
/* Size and leading are pinned, not inherited: as p elements these sit one
   specificity step below a page's own `section p` rule, which reached in and
   rendered them at copy-md with the page's leading. A component states its own
   type or the host restyles it. */
.ds-alert__title { margin: 0; color: inherit; font-size: var(--ds-font-size-copy-sm); line-height: var(--ds-font-line-normal); font-weight: var(--ds-font-weight-semibold); }
/* Title and body share the surface's foreground on every variant — weight,
   not color, separates them. On a status variant a second grey inside the
   tinted fill would break the pair and drop contrast off its gate (Toast's
   rule); on neutral the title is optional, so the body is often the whole
   message, and text.secondary read thin beside the full-strength pairs. */
.ds-alert__body { margin: 0; color: inherit; font-size: var(--ds-font-size-copy-sm); line-height: var(--ds-font-line-normal); }
.ds-alert--success { background: var(--ds-status-success-bg); color: var(--ds-status-success-text); border-color: transparent; }
.ds-alert--warning { background: var(--ds-status-warning-bg); color: var(--ds-status-warning-text); border-color: transparent; }
.ds-alert--error   { background: var(--ds-status-error-bg);   color: var(--ds-status-error-text);   border-color: transparent; }
.ds-alert--info    { background: var(--ds-status-info-bg);    color: var(--ds-status-info-text);    border-color: transparent; }

/* ---------- Banner ---------- */

/* The Band anatomy: the root paints edge to edge and owns the fill, while
   __content spans the same width and supplies the edge gutter for its row.
   There is no container cap: the message takes the available space and the
   dismiss reaches the active page gutter. In normal flow with no z-index —
   a host that wants it sticky sets position and z.sticky from its own CSS. */
.ds-banner {
  /* Badge and Tooltip's inverted pair: dark charcoal in light UI, near-white
     in dark UI, with the issued inverse foreground. The fill defines every
     edge by itself, so the old bottom hairline is gone. */
  background: var(--ds-text-primary);
  color: var(--ds-text-inverse);
}
/* The full-width content row. min-height matches the bar below so a one-line
   band and the topbar read as one stack of chrome. */
.ds-banner__content {
  display: flex;
  align-items: center;
  gap: var(--ds-space-3);
  min-height: var(--ds-size-control-lg);
  padding-inline: var(--ds-page-gutter);
  font-size: var(--ds-font-size-copy-sm);
}
/* The banner owns its icon size — inline ds-icon--* modifiers are inert
   (Button's rule). */
.ds-banner .ds-icon { width: var(--ds-size-icon-md); height: var(--ds-size-icon-md); }
/* Takes the row's free space; min-width:0 lets a long message wrap rather
   than push the dismiss off the row. Size and leading are pinned for the
   reason Alert pins them: as a p it sits below a host's own `section p` rule. */
.ds-banner__message {
  flex: 1 1 0;
  min-width: 0;
  margin: 0;
  color: inherit;
  font-size: var(--ds-font-size-copy-sm);
  line-height: var(--ds-font-line-normal);
}
.ds-banner__action { flex: none; display: inline-flex; align-items: center; }
/* Toast's dismiss, unchanged — including the presence-not-fill hover: no fixed
   state role sits on all five banner surfaces. It ends the full-width content
   row at the active responsive page gutter. */
.ds-banner__dismiss {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--ds-size-icon-lg);
  height: var(--ds-size-icon-lg);
  padding: 0;
  border: 0;
  border-radius: var(--ds-radius-small);
  background: transparent;
  color: inherit;
  opacity: 0.7;
  cursor: pointer;
}
.ds-banner__dismiss:hover { opacity: 1; }
.ds-banner__dismiss:focus-visible {
  outline: var(--ds-focus-width) solid currentColor;
  outline-offset: var(--ds-focus-offset);
}
.ds-banner .ds-link:focus-visible { outline-color: currentColor; }
.ds-banner__dismiss .ds-icon { width: var(--ds-size-icon-sm); height: var(--ds-size-icon-sm); }
/* Status variants take the solid pair, like Toast and solid Badge. A Banner is
   page-level chrome, so its status forms must carry the same authority as the
   inverted neutral default; Alert keeps the subtle tier for in-flow conditions. */
.ds-banner--success { background: var(--ds-status-success-solid-bg); color: var(--ds-status-success-solid-text); }
.ds-banner--warning { background: var(--ds-status-warning-solid-bg); color: var(--ds-status-warning-solid-text); }
.ds-banner--error   { background: var(--ds-status-error-solid-bg);   color: var(--ds-status-error-solid-text); }
.ds-banner--info    { background: var(--ds-status-info-solid-bg);    color: var(--ds-status-info-solid-text); }

/* ---------- Tabs ---------- */

/* The default strip is bare: labels on a gap, the selected underline hugging
   its own letters (no inline padding — the gap alone separates labels, so the
   hit area is the word; an accepted trade at button-md type). The full-width
   hairline is the --divided opt-in for strips that need anchoring against
   busy content; the tabs' negative margin only matters there. */
.ds-tabs__list, .ds-tabs [role="tablist"] {
  display: flex;
  gap: var(--ds-space-6);
}
.ds-tabs--divided .ds-tabs__list, .ds-tabs--divided [role="tablist"] {
  border: var(--ds-border-thin) solid var(--ds-border-subtle);
  border-width: 0 0 var(--ds-border-thin);
}
.ds-tabs__tab, .ds-tabs .ds-tab {
  border: var(--ds-border-medium) solid transparent;
  border-width: 0 0 var(--ds-border-medium);
  background: transparent;
  padding: var(--ds-space-2) 0;
  margin-bottom: calc(var(--ds-border-medium) * -1);
  font-size: var(--ds-font-size-button-md);
  font-weight: var(--ds-font-weight-medium);
  line-height: var(--ds-font-line-none);
  color: var(--ds-text-secondary);
  cursor: pointer;
}
.ds-tabs__tab:hover, .ds-tabs .ds-tab:hover, .ds-tabs__tab.demo-hover, .ds-tabs .ds-tab.demo-hover { color: var(--ds-text-primary); }
.ds-tabs__tab[aria-selected="true"], .ds-tabs .ds-tab[aria-selected="true"] { color: var(--ds-text-primary); border-color: transparent transparent var(--ds-action-primary-bg); }
.ds-tabs__tab:disabled, .ds-tabs .ds-tab:disabled { color: var(--ds-action-disabled-text); cursor: not-allowed; }
.ds-tabs__panel, .ds-tabs [role="tabpanel"] { padding: var(--ds-space-4) 0; font-size: var(--ds-font-size-copy-sm); }

/* ---------- Dialog ---------- */

.ds-dialog {
  border: none;
  border-radius: var(--ds-radius-container);
  background: var(--ds-surface-panel);
  color: var(--ds-text-primary);
  box-shadow: var(--ds-shadow-high);
  padding: var(--ds-space-6);
  max-width: calc(var(--ds-container-sm) - (var(--ds-space-16) * 2));
  width: calc(100% - var(--ds-space-8));
}
.ds-dialog::backdrop { background: var(--ds-surface-overlay); }
.ds-dialog__title { margin: 0 0 var(--ds-space-6); font-size: var(--ds-font-size-heading-lg); font-weight: var(--ds-font-weight-semibold); line-height: var(--ds-font-line-snug); letter-spacing: var(--ds-font-tracking-tight); }
.ds-dialog__body { margin: 0; color: var(--ds-text-secondary); font-size: var(--ds-font-size-copy-md); line-height: var(--ds-font-line-normal); }
.ds-dialog__content { display: grid; gap: var(--ds-space-6); }
.ds-dialog__scroll {
  display: grid;
  gap: var(--ds-space-3);
  max-height: calc(var(--ds-size-control-md) * 5);
  margin-right: calc(-1 * var(--ds-space-5));
  overflow-y: auto;
  padding-right: var(--ds-space-5);
  scroll-padding-block: var(--ds-space-2);
}
.ds-dialog__footer { display: flex; justify-content: flex-end; gap: var(--ds-space-2); margin-top: var(--ds-space-6); }

/* ---------- Sheet ---------- */

/* Sheet is the canonical edge-anchored modal surface. "Drawer" is its bottom
   usage, not a second component: drag, swipe, and snap points need L2 runtime.
   The native dialog owns top-layer semantics; only the body scrolls, keeping
   the title, close action, and optional footer stable. */
.ds-sheet {
  position: fixed;
  box-sizing: border-box;
  grid-template-rows: auto minmax(0, 1fr) auto;
  max-width: none;
  max-height: none;
  margin: 0;
  padding: 0;
  overflow: hidden;
  border: none;
  background: var(--ds-surface-panel);
  color: var(--ds-text-primary);
  box-shadow: var(--ds-shadow-high);
}
.ds-sheet[open] { display: grid; }
.ds-sheet::backdrop { background: var(--ds-surface-overlay); }
.ds-sheet__header {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: start;
  gap: var(--ds-space-4);
}
.ds-sheet__heading { display: grid; gap: var(--ds-space-1); min-width: 0; }
.ds-sheet__close {
  margin-top: calc(-1 * var(--ds-space-2));
  margin-right: calc(-1 * var(--ds-space-2));
}
.ds-sheet__title { margin: 0; font-size: var(--ds-font-size-heading-lg); font-weight: var(--ds-font-weight-semibold); line-height: var(--ds-font-line-snug); letter-spacing: var(--ds-font-tracking-tight); }
.ds-sheet__description { margin: 0; color: var(--ds-text-secondary); font-size: var(--ds-font-size-copy-sm); line-height: var(--ds-font-line-normal); }
.ds-sheet__body { min-height: 0; overflow-y: auto; scrollbar-gutter: stable; }
.ds-sheet__footer {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--ds-space-2);
}
.ds-sheet--right .ds-sheet__header, .ds-sheet--left .ds-sheet__header,
.ds-sheet--right .ds-sheet__body, .ds-sheet--left .ds-sheet__body {
  padding: var(--ds-space-6);
}
.ds-sheet--right .ds-sheet__footer, .ds-sheet--left .ds-sheet__footer {
  padding: var(--ds-space-4) var(--ds-space-6) var(--ds-space-6);
}
.ds-sheet--top .ds-sheet__header, .ds-sheet--bottom .ds-sheet__header,
.ds-sheet--top .ds-sheet__body, .ds-sheet--bottom .ds-sheet__body {
  padding-block: var(--ds-space-6);
  padding-inline: var(--ds-page-gutter);
}
.ds-sheet--top .ds-sheet__footer, .ds-sheet--bottom .ds-sheet__footer {
  padding-block: var(--ds-space-4) var(--ds-space-6);
  padding-inline: var(--ds-page-gutter);
}
.ds-sheet--right, .ds-sheet--left {
  top: 0;
  bottom: 0;
  width: min(calc(100vw - var(--ds-space-8)), calc(var(--ds-container-sm) - (var(--ds-space-16) * 3)));
  height: 100dvh;
  border: var(--ds-border-thin) solid var(--ds-border-subtle);
  border-width: 0;
}
.ds-sheet--right { right: 0; left: auto; border-left-width: var(--ds-border-thin); }
.ds-sheet--left { right: auto; left: 0; border-right-width: var(--ds-border-thin); }
.ds-sheet--top, .ds-sheet--bottom {
  right: 0;
  left: 0;
  width: auto;
  height: min(calc(100dvh - var(--ds-space-8)), calc(var(--ds-size-control-md) * 8));
  border: var(--ds-border-thin) solid var(--ds-border-subtle);
  border-width: 0;
}
.ds-sheet--top { top: 0; bottom: auto; border-bottom-width: var(--ds-border-thin); }
.ds-sheet--bottom { top: auto; bottom: 0; border-top-width: var(--ds-border-thin); }
.ds-sheet--right[open] { animation: ds-sheet-in-right var(--ds-motion-duration-base) var(--ds-motion-easing-standard); }
.ds-sheet--left[open] { animation: ds-sheet-in-left var(--ds-motion-duration-base) var(--ds-motion-easing-standard); }
.ds-sheet--top[open] { animation: ds-sheet-in-top var(--ds-motion-duration-base) var(--ds-motion-easing-standard); }
.ds-sheet--bottom[open] { animation: ds-sheet-in-bottom var(--ds-motion-duration-base) var(--ds-motion-easing-standard); }
@keyframes ds-sheet-in-right { from { transform: translateX(100%); } }
@keyframes ds-sheet-in-left { from { transform: translateX(-100%); } }
@keyframes ds-sheet-in-top { from { transform: translateY(-100%); } }
@keyframes ds-sheet-in-bottom { from { transform: translateY(100%); } }
@media (prefers-reduced-motion: reduce) {
  .ds-sheet[open] { animation: none; }
}

/* ---------- Accordion ---------- */

/* Native details/summary owns disclosure state and keyboard activation.
   Repeating a non-empty details name makes a group exclusive; omitting it
   keeps independent expansion, so neither behavior needs L2 scripting. */
.ds-accordion {
  border: var(--ds-border-thin) solid var(--ds-border-subtle);
  border-width: var(--ds-border-thin) 0 0;
}
.ds-accordion__item {
  border: var(--ds-border-thin) solid var(--ds-border-subtle);
  border-width: 0 0 var(--ds-border-thin);
}
.ds-accordion--outlined {
  display: grid;
  gap: var(--ds-space-3);
  border: 0;
}
.ds-accordion--outlined .ds-accordion__item {
  border: var(--ds-border-thin) solid var(--ds-border-default);
  border-radius: var(--ds-radius-container);
}
.ds-accordion--bare {
  display: grid;
  gap: var(--ds-space-0);
  border: 0;
}
.ds-accordion--bare .ds-accordion__item { border: 0; }
.ds-accordion--bare .ds-accordion__summary {
  padding-block: var(--ds-space-3);
}
.ds-accordion--bare .ds-accordion__content {
  padding-bottom: var(--ds-space-4);
}
.ds-accordion__summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--ds-space-4);
  min-height: var(--ds-size-control-lg);
  padding: var(--ds-space-4);
  background: transparent;
  color: var(--ds-text-primary);
  font-size: var(--ds-font-size-copy-md);
  font-weight: var(--ds-font-weight-medium);
  line-height: var(--ds-font-line-normal);
  list-style: none;
  cursor: pointer;
}
.ds-accordion__summary::-webkit-details-marker { display: none; }
.ds-accordion__summary::marker { content: ""; }
.ds-accordion__summary:focus-visible {
  outline: var(--ds-focus-width) solid var(--ds-focus-ring);
  outline-offset: var(--ds-focus-offset);
}
.ds-accordion__icon.ds-icon {
  flex: none;
  width: var(--ds-size-icon-sm);
  height: var(--ds-size-icon-sm);
  color: var(--ds-text-secondary);
  transition: color var(--ds-motion-duration-fast) var(--ds-motion-easing-standard),
              transform var(--ds-motion-duration-fast) var(--ds-motion-easing-standard);
}
.ds-accordion__summary:hover .ds-accordion__icon.ds-icon,
.ds-accordion__summary:active .ds-accordion__icon.ds-icon,
.ds-accordion__summary:focus-visible .ds-accordion__icon.ds-icon { color: var(--ds-text-primary); }
.ds-accordion__item[open] > .ds-accordion__summary .ds-accordion__icon { transform: rotate(180deg); }
.ds-accordion__content {
  padding: 0 var(--ds-space-4) var(--ds-space-5);
  color: var(--ds-text-secondary);
  font-size: var(--ds-font-size-copy-sm);
  line-height: var(--ds-font-line-normal);
}
.ds-accordion__content > :first-child { margin-top: 0; }
.ds-accordion__content > :last-child { margin-bottom: 0; }
@media (prefers-reduced-motion: reduce) {
  .ds-accordion__icon { transition: none; }
}

/* ---------- Command menu ---------- */

.ds-command {
  width: min(calc(100vw - var(--ds-space-8)), var(--ds-container-sm));
  max-height: min(calc(100vh - var(--ds-space-12)), calc(var(--ds-size-control-sm) * 18));
  padding: 0;
  border: var(--ds-border-thin) solid var(--ds-border-subtle);
  border-radius: var(--ds-radius-container);
  background: var(--ds-surface-panel);
  color: var(--ds-text-primary);
  box-shadow: var(--ds-shadow-high);
  overflow: hidden;
}
.ds-command::backdrop { background: var(--ds-surface-overlay); }
.ds-command__search {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  align-items: center;
  gap: var(--ds-space-3);
  height: var(--ds-size-control-lg);
  padding: 0 var(--ds-space-4);
  border-bottom: var(--ds-border-thin) solid var(--ds-border-subtle);
}
.ds-command__search > .ds-icon {
  width: var(--ds-size-icon-sm);
  height: var(--ds-size-icon-sm);
  color: var(--ds-text-secondary);
}
.ds-command__input {
  width: 100%;
  min-width: 0;
  padding: 0;
  border: none;
  outline: none;
  background: transparent;
  color: var(--ds-text-primary);
  font-size: var(--ds-font-size-button-md);
  line-height: var(--ds-font-line-none);
}
.ds-command__input::placeholder { color: var(--ds-text-muted); }
/* The native search-cancel is engine-drawn chrome — unstylable, UA-tinted,
   absent in Firefox — so it is suppressed and the sprite's i-x carries the
   reset affordance identically everywhere. Toast's dismiss recipe: icon.lg
   box, icon.sm glyph. Visibility is pure CSS (hidden until the field has a
   query); clearing itself is host/L2 behavior. */
.ds-command__input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }
.ds-command__clear {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--ds-size-icon-lg);
  height: var(--ds-size-icon-lg);
  padding: 0;
  border: none;
  background: transparent;
  border-radius: var(--ds-radius-small);
  color: var(--ds-text-secondary);
  cursor: pointer;
}
.ds-command__clear:hover { color: var(--ds-text-primary); }
.ds-command__clear .ds-icon { width: var(--ds-size-icon-sm); height: var(--ds-size-icon-sm); color: inherit; }
.ds-command__search:has(.ds-command__input:placeholder-shown) .ds-command__clear { visibility: hidden; }
.ds-command__list {
  display: grid;
  --ds-panel-inset: var(--ds-space-1);
  gap: var(--ds-space-1);
  max-height: calc(var(--ds-size-control-sm) * 14);
  margin: 0;
  padding: var(--ds-space-1);
  overflow-y: auto;
  scroll-padding-block: var(--ds-space-1);
}
/* Command items wear the shared popover item recipe (grouped in the Menu
   section below); only the scroll behavior is theirs. */
.ds-command__item { scroll-margin-block: var(--ds-space-1); }
.ds-command__label {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ds-command__meta {
  flex: none;
  margin-inline-start: auto;
  color: var(--ds-text-secondary);
  font-family: var(--ds-font-role-ui);
  font-size: var(--ds-font-size-caption);
  line-height: var(--ds-font-line-none);
  font-weight: var(--ds-font-weight-regular);
  white-space: nowrap;
}
.ds-command__empty {
  padding: var(--ds-space-8) var(--ds-space-3);
  color: var(--ds-text-secondary);
  font-size: var(--ds-font-size-copy-sm);
  text-align: center;
}

/* ---------- Menu / dropdown ---------- */

/* One popover recipe: the dropdown menu and the Select listbox share the
   surface and item looks via grouped selectors — same roles, one rule.
   (A menu holds actions; a listbox holds options — different ARIA, one
   look.) Popovers sit at panel tone and separate from the page with a
   border, not a lighter fill (a raised surface would double-encode
   elevation and read too bright on dark). The shadow.medium slot is
   consumed but flat in the base — a brand opts in at the token. */
.ds-menu, .ds-select__listbox {
  min-width: calc(var(--ds-container-sm) / 4);
  background: var(--ds-surface-panel);
  border: var(--ds-border-thin) solid var(--ds-border-subtle);
  box-shadow: var(--ds-shadow-medium);
  /* Named so the rows can subtract it. A row's padding is measured from the
     panel's content box, but everything a row lines up with is measured from
     the panel's border box, and the inset is the whole difference. */
  --ds-panel-inset: var(--ds-space-1);
  padding: var(--ds-space-1);
  z-index: var(--ds-z-popover);
}
/* Grid keeps adjacent vertical margins from collapsing, so the space.2 above
   the first label and above every post-divider label renders identically.
   Width is content-driven: the longest row sets the preferred width, the
   positioning layer can supply the anchor width as a floor, and a viewport
   gutter caps the result. At that cap the label truncates rather than wraps.
   The panel's outer curve is concentric with its row curve, which means the
   row's radius IS the panel's minus the space.1 inset — not a step chosen to
   look close. Select's listbox and CommandMenu's list are the same shape and
   carry the same relationship. On the default scale lg - space.1 lands exactly
   on md, so the pairing looks like a property of these components; it is a
   property of the radius and spacing scales lining up, and a brand that retunes
   one without the other breaks all three at once. That is now gated rather than
   asserted here — see the concentric radius rule in component-css-patterns.md. */
.ds-menu {
  display: grid;
  width: max-content;
  min-width: max(var(--ds-menu-anchor-width, 0px), calc(var(--ds-container-sm) / 4));
  max-width: calc(100vw - var(--ds-space-8));
  border-radius: var(--ds-radius-container);
}
.ds-select__listbox { border-radius: var(--ds-radius-container); }
/* Menu actions, Select options, and Command results express different
   semantics through ARIA and runtime behavior, but all are dense application
   choices. They therefore share SidebarNav's row geometry and typography. */
.ds-select__option, .ds-command__item,
.ds-menu__item {
  display: flex;
  align-items: center;
  width: 100%;
  border: none;
  background: transparent;
  text-align: left;
  font-weight: var(--ds-font-weight-regular);
  line-height: var(--ds-font-line-none);
  /* A command label is one line. The row's line-height is 1, so a wrap puts
     two lines hard against each other inside a min-height that was never sized
     for them — the panel is what widens to fit the longest label, not the row
     that breaks. */
  white-space: nowrap;
  color: var(--ds-text-primary);
  cursor: pointer;
  gap: var(--ds-space-2);
  min-height: var(--ds-size-control-sm);
  padding: var(--ds-space-1-5) var(--ds-space-3);
  border-radius: var(--ds-radius-nested);
  font-size: var(--ds-font-size-copy-sm);
}
.ds-command__item, .ds-menu__item {
  min-width: 0;
}
/* Every row in an inset panel indents to space.3 from the PANEL's edge, not from
   its own, so it spends what the panel inset already spent. Select is what made
   this visible — its listbox is contracted to span its trigger, so an option's
   label sits directly under the trigger's and the inset was pushing it 4px right
   (F50) — but the correction belongs to all three popovers, because a menu row
   and a select option are the same row and must read identically (F52).
   SidebarNav keeps the plain value: its container carries a different inset and
   nothing outside it to align to, so parity there is row rhythm — height, type,
   block padding, radius — not an identical padding string.
   Must sit after the shared row rule: same specificity, so source order decides.
   One rail, no per-row exceptions: a row that leads with an icon indents the
   same as a row that leads with text. Button steps its iconed edge in by one
   because a pill is self-contained and the step buys optical balance inside
   it; a list has no such freedom — its rows share a vertical edge, and any
   per-row nudge is visible as a broken rail long before it reads as optical
   compensation (F54). Measured on the real specimen, the step also cannot do
   the job it claims: the ink of a 16px icon starts 0.9–2.2px inside its box
   and the amount is a property of the individual glyph, so no single token
   step aligns a set of icons — the box edge is the only rail that holds. */
.ds-select__option, .ds-command__item, .ds-menu__item {
  padding-inline: calc(var(--ds-space-3) - var(--ds-panel-inset, 0px));
}
.ds-menu__label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Row shortcut metadata is semantic kbd content, not a keycap chip. Menu and
   Command rows share the UI face at 14px: monospace caption text made ⌘ and
   ⇧ optically tiny and gave the hints a foreign typographic voice.
   Secondary color supplies hierarchy. */
.ds-menu__shortcut, .ds-command__shortcut {
  flex: none;
  margin-inline-start: auto;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--ds-text-secondary);
  font-family: var(--ds-font-role-ui);
  font-size: var(--ds-font-size-copy-sm);
  line-height: var(--ds-font-line-none);
  font-weight: var(--ds-font-weight-regular);
  letter-spacing: var(--ds-font-tracking-wider);
  white-space: nowrap;
}
/* Escape is one named key, not a chord: the widened tracking that separates
   shortcut symbols would pull its letters apart. It gets the familiar quiet
   keycap silhouette instead, at caption size with normal tracking. */
.ds-command__escape {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  height: var(--ds-size-icon-md);
  margin-inline-start: auto;
  padding-inline: var(--ds-space-1);
  border: var(--ds-border-thin) solid var(--ds-border-default);
  border-radius: var(--ds-radius-small);
  background: transparent;
  color: var(--ds-text-secondary);
  font-family: var(--ds-font-role-ui);
  font-size: var(--ds-font-size-caption);
  line-height: var(--ds-font-line-none);
  font-weight: var(--ds-font-weight-regular);
  letter-spacing: var(--ds-font-tracking-normal);
  white-space: nowrap;
}
.ds-menu__item[aria-disabled="true"] .ds-menu__shortcut { color: inherit; }
/* Auto margin aligns the shortcut column; the label margin plus the row's
   space.2 gap establishes a space.6 minimum between command and shortcut. */
.ds-menu__item:has(.ds-menu__shortcut) .ds-menu__label { margin-inline-end: var(--ds-space-4); }
/* The panel edge is something that follows the label too. Every other neighbour
   a label can have is held off by space.4 — a shortcut by this rule, a check by
   its space.2 end margin plus the row's space.2 gap — but a row that ends in its
   label had nothing between the text and the rail, so it fell back to the rail
   itself (space.3, counting the row's padding plus the panel inset). The panel
   shrink-wraps, so on exactly the menus whose longest row ends in a label the
   text landed flush against the edge while a menu with a trailing slot kept
   37-95px of air (F55). Tops the rail up to the same minimum instead. */
.ds-menu__label:last-child { margin-inline-end: max(0px, calc(var(--ds-space-4) - var(--ds-space-3))); }
/* Checked menu items are whole-row controls, never rows containing Checkbox
   or Radio controls. Every peer carries the fixed state slot so labels stay
   aligned; the state attribute alone reveals the decorative check.
   Select declares with Menu rather than beside it: one check, one slot, one
   reservation rule. A row that reserves the slot only when it is filled has
   no column at all — the mark appears out of nowhere and the label's width
   changes with the state (F56). */
.ds-menu__item--check .ds-menu__label,
.ds-select__option .ds-select__label { margin-inline-end: var(--ds-space-2); }
.ds-menu__check, .ds-select__check {
  display: inline-flex;
  flex: none;
  width: var(--ds-size-icon-sm);
  height: var(--ds-size-icon-sm);
  margin-inline-start: auto;
  color: inherit;
  opacity: 0;
}
.ds-menu__item[aria-checked="true"] .ds-menu__check,
.ds-select__option[aria-selected="true"] .ds-select__check { opacity: 1; }
.ds-select__label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ds-menu__choice-group { display: grid; }
.ds-menu__group, .ds-command__group {
  /* A section label sits on the rail of the rows it labels — one value, because
     the rows are one rail (F54). It rides the same correction: the inline indent
     is measured from the panel's edge, and the panel inset already spent part of
     it (F53). Naming the rail twice is what let the label drift; the label and
     the row must always read as the same expression. */
  padding: var(--ds-space-1) calc(var(--ds-space-3) - var(--ds-panel-inset, 0px));
  color: var(--ds-text-secondary);
  font-size: var(--ds-font-size-label-sm);
  line-height: var(--ds-font-line-none);
  font-weight: var(--ds-font-weight-regular);
  letter-spacing: var(--ds-font-tracking-wide);
}
/* Anchored Menu stays compact: the label's own bottom padding plus the panel
   grid gap separates it from the first action. */
.ds-menu__group { margin: var(--ds-space-2) 0 0; }
/* CommandMenu mirrors SidebarNav's navigation hierarchy even though its
   labels and results are flat siblings rather than nested group/list grids.
   The bottom margin raises label-to-row separation from 8px to SidebarNav's
   12px; every label uses 12px top margin so later groups reach SidebarNav's
   20px separation and the first group gets the same breathing room beneath
   the search divider as a SidebarNav label gets beneath its container edge. */
.ds-command__group { margin: var(--ds-space-3) 0 var(--ds-space-1); }
/* Dense rows rest transparent on the panel, so they take the ghost tint rather
   than control.bg-hover. That token is the second rung of a filled control's
   ladder — a Chip rests on control.bg and steps one down to control.bg-hover —
   and a transparent row starting at the panel takes the whole two-step drop at
   once. Mixing from currentColor keeps the step at one rung on any surface,
   including a dark band, where a fixed neutral fill would be the wrong hue. */
.ds-menu__item:hover, .ds-menu__item[data-highlighted],
.ds-select__option:hover, .ds-select__option[data-highlighted],
.ds-command__item:hover, .ds-command__item[data-highlighted], .ds-command__item[aria-selected="true"] { background: color-mix(in srgb, currentColor var(--ds-action-ghost-hover), transparent); }
/* The item owns its icon size (Button's rule) — inline ds-icon--* modifiers
   inside a menu item are deliberately inert. Covers trailing icons too. */
.ds-menu__item .ds-icon, .ds-command__item .ds-icon,
.ds-select__option .ds-icon { width: var(--ds-size-icon-sm); height: var(--ds-size-icon-sm); }
.ds-menu__item[aria-disabled="true"],
.ds-select__option[aria-disabled="true"] { color: var(--ds-action-disabled-text); cursor: not-allowed; background: transparent; }
/* A menu trigger's trailing chevron flips while the menu is open — the same
   cue as Select's trigger, same motion recipe. A leading identity icon and an
   icon-only overflow trigger stay fixed. */
.ds-button[aria-haspopup="menu"].ds-button--trailing-icon > .ds-icon:last-child {
  transition: transform var(--ds-motion-duration-fast) var(--ds-motion-easing-standard);
}
.ds-button[aria-haspopup="menu"][aria-expanded="true"].ds-button--trailing-icon > .ds-icon:last-child { transform: rotate(180deg); }
/* Destructive item: danger-on-surface — red text at rest, the danger tint as
   its highlight. Icons ride along via currentColor. */
.ds-menu__item--danger { color: var(--ds-action-danger-fg); }
.ds-menu__item--danger:hover, .ds-menu__item--danger[data-highlighted] { background: var(--ds-action-danger-bg-subtle); }
/* Disabled flattens even danger (Button's rule: disabled flattens color).
   Without this, --danger ties the disabled selector and wins on order, and
   its hover tint would fire on an inert row. */
.ds-menu__item--danger[aria-disabled="true"] { color: var(--ds-action-disabled-text); }
.ds-menu__item--danger[aria-disabled="true"]:hover,
.ds-menu__item--danger[aria-disabled="true"][data-highlighted] { background: transparent; }
/* Trailing slot, right-aligned: a per-row icon (drill-in chevron, lock) or
   a short hint. Named like Button's icon axis — leading/trailing is the
   cross-component vocabulary; CommandMenu's __meta keeps kbd hints. */
.ds-menu__trailing {
  flex: none;
  margin-inline-start: auto;
  color: var(--ds-text-secondary);
  font-family: var(--ds-font-role-code);
  font-size: var(--ds-font-size-caption);
}
/* On a disabled row the trailing icon matches the flattened text — a
   secondary-toned lock on grey text would read as still-active. */
.ds-menu__item[aria-disabled="true"] .ds-menu__trailing { color: inherit; }
/* Hairline between regions; spans the panel's inset padding.
   The line is the boundary signal and adds no air of its own — block margin
   equals the panel inset, so the next item sits as far from the line as
   from the panel edges. Generous section spacing belongs to labeled groups. */
.ds-menu__separator {
  height: var(--ds-border-thin);
  margin: var(--ds-space-1) calc(-1 * var(--ds-space-1));
  background: var(--ds-border-subtle);
}

/* ---------- Tooltip ---------- */

.ds-tooltip {
  display: inline-block;
  /* Width rides the content so host absolute-positioning can't collapse
     the pill to the trigger's width; the cap is Toast's measure. */
  width: max-content;
  max-width: var(--ds-tooltip-max-width);
  padding: var(--ds-space-1-5) var(--ds-space-2);
  border-radius: var(--ds-radius-control);
  background: var(--ds-text-primary);
  color: var(--ds-text-inverse);
  font-size: var(--ds-font-size-caption);
  line-height: var(--ds-font-line-normal);
  box-shadow: var(--ds-shadow-low);
  z-index: var(--ds-z-tooltip);
}
/* Side modifiers position the pill against the nearest positioned ancestor
   — the trigger wrapper. The side is the author's call at L1; flipping on
   viewport collision is L2 measurement. The unmarked pill stays in-flow. */
.ds-tooltip--top, .ds-tooltip--bottom, .ds-tooltip--left, .ds-tooltip--right { position: absolute; }
.ds-tooltip--top { bottom: calc(100% + var(--ds-space-1)); left: 50%; translate: -50% 0; }
.ds-tooltip--bottom { top: calc(100% + var(--ds-space-1)); left: 50%; translate: -50% 0; }
.ds-tooltip--left { right: calc(100% + var(--ds-space-1)); top: 50%; translate: 0 -50%; }
.ds-tooltip--right { left: calc(100% + var(--ds-space-1)); top: 50%; translate: 0 -50%; }
/* Alignment swaps the centered cross axis for the anchor's edge. */
.ds-tooltip--top.ds-tooltip--align-start, .ds-tooltip--bottom.ds-tooltip--align-start { left: 0; translate: 0 0; }
.ds-tooltip--top.ds-tooltip--align-end, .ds-tooltip--bottom.ds-tooltip--align-end { left: auto; right: 0; translate: 0 0; }
.ds-tooltip--left.ds-tooltip--align-start, .ds-tooltip--right.ds-tooltip--align-start { top: 0; translate: 0 0; }
.ds-tooltip--left.ds-tooltip--align-end, .ds-tooltip--right.ds-tooltip--align-end { top: auto; bottom: 0; translate: 0 0; }

/* ---------- Table ---------- */

.ds-table__frame {
  border: var(--ds-border-thin) solid var(--ds-border-subtle);
  border-radius: var(--ds-radius-control);
  padding: 0 var(--ds-space-4);
  overflow-x: auto;
}
.ds-table {
  width: 100%;
  min-width: 0;
  border-collapse: collapse;
  table-layout: fixed;
  font-family: var(--ds-font-role-body);
  font-size: var(--ds-font-size-copy-sm);
  line-height: var(--ds-font-line-normal);
  font-weight: var(--ds-font-weight-regular);
  letter-spacing: var(--ds-font-tracking-normal);
  color: var(--ds-text-primary);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .ds-table th,
  :root:not([data-theme="light"]) .ds-table__header { background: var(--ds-color-neutral-800); }
  :root:not([data-theme="light"]) .ds-table--row-action tbody tr:not([aria-disabled="true"]):hover td,
  :root:not([data-theme="light"]) .ds-table--row-action tbody tr:has(+ tr:not([aria-disabled="true"]):hover) td { border-bottom-color: transparent; }
}
[data-theme="dark"] .ds-table th,
[data-theme="dark"] .ds-table__header { background: var(--ds-color-neutral-800); }
[data-theme="dark"] .ds-table--row-action tbody tr:not([aria-disabled="true"]):hover td,
[data-theme="dark"] .ds-table--row-action tbody tr:has(+ tr:not([aria-disabled="true"]):hover) td { border-bottom-color: transparent; }
.ds-table th,
.ds-table__header {
  text-align: left;
  font-family: var(--ds-font-role-body);
  font-size: var(--ds-font-size-copy-sm);
  line-height: var(--ds-font-line-normal);
  font-weight: var(--ds-font-weight-semibold);
  letter-spacing: var(--ds-font-tracking-normal);
  color: var(--ds-text-primary);
  padding: var(--ds-space-2) var(--ds-space-3);
  /* Pinned to the ramp in both modes (the dark pin came first): control.bg
     is one step off surface.canvas and the header merges with the page. */
  background: var(--ds-color-neutral-200);
}
.ds-table td,
.ds-table__cell {
  padding: var(--ds-space-3);
  border-bottom: var(--ds-border-thin) solid var(--ds-border-subtle);
  font-family: var(--ds-font-role-body);
  font-size: var(--ds-font-size-copy-sm);
  line-height: var(--ds-font-line-normal);
  font-weight: var(--ds-font-weight-regular);
  letter-spacing: var(--ds-font-tracking-normal);
  color: var(--ds-text-primary);
}
.ds-table th.ds-table__number,
.ds-table td.ds-table__number,
.ds-table__number {
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.ds-table th.ds-table__select,
.ds-table td.ds-table__select,
.ds-table__select {
  width: calc(var(--ds-size-icon-md) + var(--ds-space-3));
  padding-left: var(--ds-space-3);
  padding-right: 0;
  text-align: left;
  vertical-align: middle;
}
.ds-table tbody tr:last-child td,
.ds-table tbody tr:last-child .ds-table__cell { border-bottom: 0; }
.ds-table--divided td,
.ds-table--divided .ds-table__cell { border-bottom-color: var(--ds-border-subtle); }
.ds-table--plain td,
.ds-table--plain .ds-table__cell { border-bottom-color: transparent; }
.ds-table--row-action tbody tr,
.ds-table--row-action .ds-table__row { cursor: pointer; }
/* An actionable row is a transparent row, so it takes the ghost tint — but at
   the band strength, not the control strength. A row runs the full width of the
   table, and that area already supplies the presence the dark control step is
   compensating for; at 8% the row reads as a stripe rather than a highlight.
   It used to reach straight off the ramp — neutral.100 in light,
   alpha.light.950 in dark — which is the rule against raw ramp values broken
   twice, and it needed a per-theme override to do it. The band token derives
   both modes from one declaration, so the theme branches below carry only
   borders, and it lands within a point of the ramp values it replaces. */
.ds-table--row-action tbody tr:not([aria-disabled="true"]):hover,
.ds-table--row-action tbody tr:not([aria-disabled="true"]).demo-hover,
.ds-table--row-action .ds-table__row:not([aria-disabled="true"]):hover,
.ds-table--row-action .ds-table__row:not([aria-disabled="true"]).demo-hover { background: color-mix(in srgb, currentColor var(--ds-action-ghost-hover-band), transparent); }
.ds-table--row-action tbody tr:not([aria-disabled="true"]):hover td,
.ds-table--row-action tbody tr:has(+ tr:not([aria-disabled="true"]):hover) td { border-bottom-color: transparent; }
.ds-table--row-action tbody tr:focus-visible,
.ds-table--row-action .ds-table__row:focus-visible {
  outline: var(--ds-focus-width) solid var(--ds-focus-ring);
  outline-offset: calc(-1 * var(--ds-focus-width));
}
.ds-table--row-action tbody tr[aria-disabled="true"],
.ds-table--row-selection tbody tr[aria-disabled="true"],
.ds-table--row-action .ds-table__row[aria-disabled="true"],
.ds-table--row-selection .ds-table__row[aria-disabled="true"] {
  color: var(--ds-action-disabled-text);
  background: transparent;
  cursor: not-allowed;
}

/* ---------- Pagination ---------- */

.ds-pagination { display: inline-flex; gap: var(--ds-space-1); align-items: center; }
.ds-pagination__page {
  min-width: var(--ds-space-6);
  height: var(--ds-space-6);
  padding: 0 var(--ds-space-2);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: var(--ds-border-thin) solid transparent;
  border-radius: var(--ds-radius-nested);
  background: transparent;
  color: var(--ds-text-secondary);
  font-size: var(--ds-font-size-copy-sm);
  line-height: var(--ds-font-line-none);
  cursor: pointer;
}
/* Pagination cells are pure-text ghosts, so they take the ghost tint like every
   other transparent row. Current is persistent content selection rather than
   an interaction step, so it uses selection.bg and holds against hover. */
.ds-pagination__page:hover, .ds-pagination__page.demo-hover { background: color-mix(in srgb, currentColor var(--ds-action-ghost-hover), transparent); }
.ds-pagination__page[aria-current="page"] { background: var(--ds-selection-bg); color: var(--ds-text-primary); }
.ds-pagination__page:disabled { color: var(--ds-action-disabled-text); cursor: not-allowed; background: transparent; }
.ds-pagination__arrow { padding: 0; }
.ds-pagination__arrow .ds-icon { width: var(--ds-size-icon-sm); height: var(--ds-size-icon-sm); }
/* The ellipsis is an inert cell on the page grid — same footprint, so truncation never shifts the row. */
.ds-pagination__ellipsis {
  min-width: var(--ds-space-6);
  height: var(--ds-space-6);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--ds-text-secondary);
  font-size: var(--ds-font-size-copy-sm);
}

/* ---------- Breadcrumbs ---------- */

.ds-crumbs { display: flex; flex-wrap: wrap; align-items: center; gap: var(--ds-space-2); list-style: none; margin: 0; padding: 0; font-size: var(--ds-font-size-copy-sm); }
/* The item binds its link to its trailing separator, so a wrapping trail breaks between items. */
.ds-crumbs__item { display: flex; align-items: center; gap: var(--ds-space-2); color: var(--ds-text-secondary); }
.ds-crumbs__link { color: var(--ds-text-secondary); text-decoration: none; border-radius: var(--ds-radius-small); }
.ds-crumbs__link:hover, .ds-crumbs__link.demo-hover { color: var(--ds-text-primary); }
.ds-crumbs__separator { width: var(--ds-size-icon-sm); height: var(--ds-size-icon-sm); }
/* Current is color-only: a weight step widens the item and shifts the trail when current moves. */
.ds-crumbs__item[aria-current="page"] { color: var(--ds-text-primary); }

/* ---------- Avatar ---------- */

.ds-avatar {
  box-sizing: border-box;
  width: var(--ds-size-control-sm);
  height: var(--ds-size-control-sm);
  border-radius: var(--ds-radius-full);
  background: var(--ds-context-avatar-bg, var(--ds-avatar-bg));
  color: var(--ds-context-avatar-text, var(--ds-avatar-text));
  border: var(--ds-border-thin) solid var(--ds-context-avatar-border, var(--ds-avatar-border));
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: var(--ds-font-size-caption);
  line-height: var(--ds-font-line-none);
  font-weight: var(--ds-font-weight-medium);
  flex: none;
}
.ds-avatar--sm { width: var(--ds-size-icon-lg); height: var(--ds-size-icon-lg); font-size: var(--ds-font-size-scale-2xs); }
.ds-avatar--lg { width: var(--ds-size-control-md); height: var(--ds-size-control-md); font-size: var(--ds-font-size-copy-sm); }
.ds-avatar__image { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; }
/* Fallback glyph rides the icon scale one step light — the base .ds-icon
   default (icon.md) would crowd the md box. The glyph scale bottoms out at
   icon.sm, so --sm shares it. */
.ds-avatar .ds-icon { width: var(--ds-size-icon-sm); height: var(--ds-size-icon-sm); }
.ds-avatar--lg .ds-icon { width: var(--ds-size-icon-md); height: var(--ds-size-icon-md); }

/* ---------- Avatar group ---------- */

.ds-avatar-group {
  display: inline-flex;
  align-items: center;
  /* Own stacking context: the first-on-top ladder below must not compete
     with page z. */
  isolation: isolate;
}
/* First on top: relevance order and paint order agree — each later child
   slides under the previous, and the trailing +N count chip (a plain
   initials-form .ds-avatar) tucks beneath the stack's end. The ladder's six
   slots are the contract's own cap (three to five avatars plus the chip);
   a seventh child degrades to the bottom of the stack. */
.ds-avatar-group .ds-avatar { position: relative; --ds-avatar-group-box: var(--ds-size-control-sm); }
.ds-avatar-group .ds-avatar--sm { --ds-avatar-group-box: var(--ds-size-icon-lg); }
.ds-avatar-group .ds-avatar--lg { --ds-avatar-group-box: var(--ds-size-control-md); }
/* The gap is a true cutout, not a painted ring: a border.medium-wide
   crescent is masked out of each covered avatar where the previous one
   overlaps, so the gap shows whatever the group sits on — any surface,
   both modes, no per-surface knowledge. The notch center is the previous
   avatar's center (one box back, minus the space.1-5 pull); its radius is
   the box radius plus the gap; the 1px stop transition antialiases the
   edge. */
.ds-avatar-group .ds-avatar + .ds-avatar {
  margin-left: calc(-1 * var(--ds-space-1-5));
  -webkit-mask-image: radial-gradient(circle at calc(var(--ds-space-1-5) - var(--ds-avatar-group-box) / 2) 50%, transparent calc(var(--ds-avatar-group-box) / 2 + var(--ds-border-medium) - 0.5px), black calc(var(--ds-avatar-group-box) / 2 + var(--ds-border-medium) + 0.5px));
  mask-image: radial-gradient(circle at calc(var(--ds-space-1-5) - var(--ds-avatar-group-box) / 2) 50%, transparent calc(var(--ds-avatar-group-box) / 2 + var(--ds-border-medium) - 0.5px), black calc(var(--ds-avatar-group-box) / 2 + var(--ds-border-medium) + 0.5px));
}
.ds-avatar-group .ds-avatar:nth-child(1) { z-index: 6; }
.ds-avatar-group .ds-avatar:nth-child(2) { z-index: 5; }
.ds-avatar-group .ds-avatar:nth-child(3) { z-index: 4; }
.ds-avatar-group .ds-avatar:nth-child(4) { z-index: 3; }
.ds-avatar-group .ds-avatar:nth-child(5) { z-index: 2; }
.ds-avatar-group .ds-avatar:nth-child(6) { z-index: 1; }

/* ---------- Code (inline) ---------- */

/* Token and identifier chips: code names read as code everywhere. Inline
   only — a block snippet is a different component (unbuilt, no consumer). */
.ds-code {
  /* inline-block + pinned line-height: one box model in every context. As a
     plain inline, the chip's height came from font metrics; as a flex child
     (the specimen's sp-chips), blockification switched it to line-height ×
     font + padding — same class, two heights. Pinning both closes the gap. */
  display: inline-block;
  font-family: var(--ds-font-role-code);
  /* Tracks its host, floored at the caption step. A chip is part of the
     sentence it sits in, so a fixed 0.75rem read a quarter the size of a
     display heading's text; 0.875em keeps it one notch below whatever prose
     carries it. The floor stops dense contexts — a caption-size table cell —
     from shrinking the identifier below legibility. em, not rem: the point is
     the host's size, not the root's. */
  font-size: max(var(--ds-font-size-caption), 0.875em);
  line-height: var(--ds-font-line-none);
  color: var(--ds-code-text);
  background: var(--ds-code-bg);
  /* The sides keep a space.1 floor and scale above it; the top and bottom are
     a pure ratio, deliberately. A floored vertical inset does not shrink with
     the type, so at caption size the chip stood 20px tall in an 18px line and
     chips on consecutive lines overlapped by 2px. As a ratio the inset tracks
     the chip the way the chip tracks its host. 0.2em over 0.25em: both clear
     every prose size, and the tighter one leaves visibly more air between
     chips stacked on consecutive lines — 4.4px against 3.0px at copy-md,
     1.2px against 0.0px at caption. */
  padding-block: 0.2em;
  padding-inline: max(var(--ds-space-1), 0.25em);
  /* The inset is visual only. As an inline-block the chip's margin box sets
     the line box, so vertical padding pushed prose lines apart — measurably:
     a copy-sm paragraph grew 1.3px per chipped line, and a caption-size cell
     overflowed its 18px line box at 20px. Cancelling the padding with an equal
     negative block margin leaves the chip contributing only its own text
     height, so a paragraph is the same height with chips or without, while the
     fill still paints the full padded box into the line's leading. */
  margin-block: -0.2em;
  /* Scales with the host like the size and the inset do: radius.small is right on
     a 22px chip and reads nearly square on a 63px one, because a fixed corner
     on a growing box flattens. Same floor-then-scale shape as the padding. */
  border-radius: max(var(--ds-radius-small), 0.25em);
  white-space: nowrap;
}
/* ---------- Empty state ---------- */

.ds-empty {
  display: grid;
  justify-items: center;
  gap: var(--ds-space-4);
  padding: var(--ds-space-10) var(--ds-space-6);
  text-align: center;
}
.ds-empty__visual {
  display: grid;
  place-items: center;
  width: var(--ds-size-control-lg);
  height: var(--ds-size-control-lg);
  border-radius: var(--ds-radius-full);
  color: var(--ds-text-secondary);
  background: var(--ds-control-bg);
}
.ds-empty__visual .ds-icon {
  width: var(--ds-size-icon-lg);
  height: var(--ds-size-icon-lg);
}
.ds-empty__content {
  display: grid;
  justify-items: center;
  gap: var(--ds-space-1);
  max-width: calc(var(--ds-container-sm) / 2);
}
.ds-empty__title {
  margin: 0;
  color: var(--ds-text-primary);
  font-size: var(--ds-font-size-copy-md);
  line-height: var(--ds-font-line-snug);
  font-weight: var(--ds-font-weight-semibold);
}
.ds-empty__body {
  margin: 0;
  color: var(--ds-text-secondary);
  font-size: var(--ds-font-size-copy-sm);
  line-height: var(--ds-font-line-normal);
}
.ds-empty__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--ds-space-2);
}

/* ---------- Skeleton ---------- */

/* A content-shaped placeholder. The boundary with the other two waiting
   signals is the shape: Spinner says "wait, duration unknown" and shows
   nothing about what is coming, Progress says how much of a known total is
   done, and Skeleton says what is about to appear and where. Use it only when
   the layout is actually known — a skeleton that guesses wrong is a layout
   shift with extra steps.

   Width is the host's, always: a skeleton stands in for specific content, and
   only the host knows how wide that content is. The shapes below set the one
   dimension the host cannot infer from the layout. */
.ds-skeleton {
  /* The stagger hook. A group of skeletons pulsing in lockstep reads as one
     flashing slab; offsetting each one turns it into a wave and makes the
     group legible as separate pieces of content. The delay is per element, so
     unlike a shimmer sweep it needs no shared coordinate space — the host sets
     an index and nothing here has to know about layout. */
  --ds-skeleton-delay: 0s;
  display: block;
  background: var(--ds-border-strong);
  border-radius: var(--ds-radius-small);
  /* Slow, and on the loop easing rather than the standard one. standard is
     asymmetric — quick away, settling at the end — so a repeating animation
     built on it snaps on the return leg and reads mechanical. */
  animation: ds-pulse calc(var(--ds-motion-duration-slow) * 6) var(--ds-motion-easing-loop) infinite;
  animation-delay: var(--ds-skeleton-delay);
}
/* Text is the default because it is the overwhelming case. The bar takes the
   body size rather than the full line box: the host owns the rhythm between
   lines, the way Stack owns it everywhere else. */
.ds-skeleton--text { height: var(--ds-font-size-copy-md); }
/* A media or card stand-in. The floor keeps it visible when a host forgets to
   set a height; any real use overrides it. */
.ds-skeleton--block {
  min-height: var(--ds-size-control-lg);
  border-radius: var(--ds-radius-control);
}
/* Avatar's lg box. A placeholder avatar carries a whole row on its own, so it
   is sized to be read as one rather than to be the smallest thing that fits. */
.ds-skeleton--circle {
  width: var(--ds-size-control-md);
  height: var(--ds-size-control-md);
  flex: none;
  border-radius: var(--ds-radius-full);
}
/* Colour, not opacity, and across the widest sensible span of the neutral
   border ramp. Opacity dimmed the placeholder towards whatever sat behind it,
   so the pulse got weaker on exactly the busy surfaces where it needed to
   read; a colour step is the same swing everywhere. strong to subtle also
   moves the right way in both modes without a mode-specific rule — the quiet
   end is lighter on a light theme and darker on a dark one, because that is
   what those two roles already mean. */
@keyframes ds-pulse { 50% { background: var(--ds-border-subtle); } }
/* An infinite pulse is exactly the animation reduced-motion exists for, and
   this one ran unguarded from the day it shipped — it was never rendered in
   the specimen, so nothing ever surfaced it. */
@media (prefers-reduced-motion: reduce) {
  .ds-skeleton { animation: none; }
}

/* ---------- Message ---------- */

/* One turn in a conversation. The three roles are not three skins of one
   bubble, which is the single most consequential decision here: an assistant
   answer is a document — prose, headings, lists, code, tables, citations — and
   putting it in a bubble caps how much structure it can carry and makes long
   output feel like shouting. So assistant messages have no fill and no
   boundary; they are page content. A user message is short, discrete, and
   worth separating from the answer around it, so it keeps the bubble. A system
   note is neither a document nor a turn — it is a quiet marker about the
   conversation itself.

   Width belongs to the thread. Message sets no measure, because how wide a
   conversation reads is a property of the surface holding it, not of one turn. */
.ds-message { display: block; }
.ds-message__content { font-size: var(--ds-font-size-copy-sm); line-height: var(--ds-font-line-normal); }
/* Prose inside a turn keeps its own rhythm, but must not push the turn's own
   spacing around — the Card rule, applied to a region that hosts real prose. */
.ds-message__content > :first-child { margin-top: 0; }
.ds-message__content > :last-child { margin-bottom: 0; }
/* Card pins its own named parts, which is enough there because it owns them.
   A turn hosts prose the page authored, so pinning the region alone does
   nothing: a plain `p` rule on the host outranks inheritance and resizes the
   answer while the bubble beside it stays put. Forcing the block children to
   inherit is the smallest rule that keeps every turn on one size without
   dictating anything else about the prose. */
/* Colour leaks exactly like size does, and it is the more visible failure:
   a page that styles paragraphs as secondary copy turned every assistant
   answer grey while the user bubble beside it — a bare text node, not a
   paragraph — stayed primary. Links and code are deliberately absent from
   this list; they carry their own colour. */
.ds-message__content :is(p, ul, ol, li, blockquote, dl, dd, dt) { font-size: inherit; color: inherit; }
/* Rhythm leaks the same way type does, and the consequence is worse: with the
   host's 24px paragraph margins against a 16px thread gap, a paragraph break
   read as a bigger event than a change of speaker, so the bubbles looked glued
   to the answer below them. A turn owns its internal rhythm so the thread's
   gap can always be the larger break. */
.ds-message__content :is(p, ul, ol, blockquote, dl, pre, table) { margin-block: 0 var(--ds-space-3); }

/* The document role: no fill, no boundary, no alignment of its own. */
.ds-message--assistant .ds-message__content { color: var(--ds-text-primary); }

/* The bubble role. fit-content plus an auto start margin does the aligning, so
   nothing here assumes a flex or grid parent.

   The cap is what keeps the role legible. At max-width:100% a long question
   grew to the full thread and became indistinguishable from an assistant
   document — the bubble stopped meaning anything at exactly the length where
   the reader most needs to know who is talking. Two limits, because one is not
   enough: the percentage keeps a gutter on the left at any thread width, and
   the container cap stops a bubble from running to a full reading measure on a
   wide surface. This is the bubble's own shape, not the turn's measure — the
   thread still owns that. */
.ds-message--user .ds-message__content {
  width: fit-content;
  max-width: min(80%, var(--ds-container-sm));
  margin-inline-start: auto;
  padding: var(--ds-space-3) var(--ds-space-4);
  background: var(--ds-control-bg);
  color: var(--ds-text-primary);
  border-radius: var(--ds-radius-container);
}

/* A marker about the conversation, not a turn in it. */
.ds-message--system .ds-message__content {
  color: var(--ds-text-secondary);
  font-size: var(--ds-font-size-caption);
  text-align: center;
}

/* Actions belong to a finished turn. While the answer is still arriving there
   is nothing complete to copy, rate, or regenerate, so the footer is withheld
   rather than shown disabled — a disabled row invites waiting for it. */
.ds-message__actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--ds-space-1);
}

/* Message owns the space between turns, not the thread — which reverses what
   this contract first said, and for a concrete reason: only adjacency knows
   whether the previous turn had the same author, and a flex gap on the thread
   cannot see that. A run from one speaker closes up to space.2; a change of
   speaker keeps the full break. The thread must therefore set no gap of its
   own, or the two add. */
.ds-message + .ds-message { margin-top: var(--ds-space-6); }
.ds-message--user + .ds-message--user,
.ds-message--assistant + .ds-message--assistant { margin-top: var(--ds-space-2); }

/* One footer line under the turn, carrying the metadata and the controls
   together. They share a row rather than stacking because they are one band of
   secondary information: a stamp on its own line pushes the next turn further
   away than the turn's own paragraphs are from each other, which is the
   hierarchy this component just spent a fix getting right. The footer owns the
   alignment, so a stamp under an end-aligned bubble stays with the bubble. */
.ds-message__footer {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--ds-space-2);
  margin-top: var(--ds-space-2);
}
.ds-message--user .ds-message__footer { justify-content: flex-end; }
.ds-message--system .ds-message__footer { justify-content: center; }
.ds-message[aria-busy="true"] .ds-message__footer { display: none; }

/* Streaming text reads as arriving rather than arrived: a band of full
   opacity travels across the answer, so letters light up as it passes.

   Masked, not clipped: a gradient clipped to the glyphs replaces the text's
   colour outright, which breaks selection highlighting, forced-colors mode,
   and anything nested that paints with currentColor. The mask leaves the real
   colour in place and animates only opacity.

   The mask must cover the element at every frame. Sizing it wider than the box
   and sliding it — the obvious way to move a band — leaves part of the text
   with no mask over it, and an unmasked area is not "unaffected", it is fully
   transparent: the answer got visibly cut off at whichever end the gradient
   had left. So the mask stays exactly the element's size and the band moves
   inside it, as an animated stop position. */
@property --ds-message-sweep {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 50%;
}
.ds-message[aria-busy="true"] .ds-message__content {
  color: var(--ds-text-primary);
  -webkit-mask-image: linear-gradient(
    90deg,
    var(--ds-color-alpha-dark-400) calc(var(--ds-message-sweep) - 18%),
    var(--ds-color-alpha-dark-950) var(--ds-message-sweep),
    var(--ds-color-alpha-dark-400) calc(var(--ds-message-sweep) + 18%)
  );
  mask-image: linear-gradient(
    90deg,
    var(--ds-color-alpha-dark-400) calc(var(--ds-message-sweep) - 18%),
    var(--ds-color-alpha-dark-950) var(--ds-message-sweep),
    var(--ds-color-alpha-dark-400) calc(var(--ds-message-sweep) + 18%)
  );
  animation: ds-message-stream var(--ds-message-cycle) linear infinite;
}
/* A spotlight travels at a constant speed, so this one is linear rather than
   eased — motion.easing.loop is for something breathing in place.

   The cycle is one sequence, not two loops: the band crosses the text over the
   first two thirds, then waits off the right edge while the dots run. Two
   infinite animations only stay locked if they share a duration, so the cycle
   is the sweep's own pace plus the tail it hands off into — the sweep keeps
   the 8x-slow travel it had before the two were tied together. The hold also
   hides the loop point, since the band is off the text at both 100% and 0%. */
@keyframes ds-message-stream {
  0% { --ds-message-sweep: -20%; }
  66.6% { --ds-message-sweep: 120%; }
  100% { --ds-message-sweep: 120%; }
}

/* The handoff happens where the band meets the dots, not where it leaves the
   block. The dots sit at the end of the text, inside the sweep's path — in the
   specimen's turn, roughly 70% along the content, which the band reaches about
   43% into the cycle. Waiting for the band to exit at 66.6% fired them long
   after the light had passed. They now rise as it arrives and settle behind
   it, so the spotlight reads as carrying through into them.

   LoadingDots' own keyframes span 76% of their cycle, so no delay can place
   them here — they need timing written against this cycle instead. That makes
   it the one place Message re-times another component's motion, so the shape
   is restated rather than assumed. The dots' order and their 0.32 base are
   LoadingDots'; only the placement on the timeline is ours. */
.ds-message[aria-busy="true"] { --ds-message-cycle: calc(var(--ds-motion-duration-slow) * 12); }
.ds-message[aria-busy="true"] .ds-loading-dots__dot {
  animation-duration: var(--ds-message-cycle);
  animation-timing-function: var(--ds-motion-easing-standard);
}
.ds-message[aria-busy="true"] .ds-loading-dots__dot:nth-child(1) { animation-name: ds-message-dot-1; }
.ds-message[aria-busy="true"] .ds-loading-dots__dot:nth-child(2) { animation-name: ds-message-dot-2; }
.ds-message[aria-busy="true"] .ds-loading-dots__dot:nth-child(3) { animation-name: ds-message-dot-3; }
@keyframes ds-message-dot-1 {
  0%, 40% { opacity: 0.32; }
  48% { opacity: 1; }
  58% { opacity: 0.46; }
  100% { opacity: 0.32; }
}
@keyframes ds-message-dot-2 {
  0%, 48% { opacity: 0.32; }
  56% { opacity: 1; }
  66% { opacity: 0.46; }
  100% { opacity: 0.32; }
}
@keyframes ds-message-dot-3 {
  0%, 56% { opacity: 0.32; }
  64% { opacity: 1; }
  74% { opacity: 0.46; }
  100% { opacity: 0.32; }
}

@media (prefers-reduced-motion: reduce) {
  .ds-message[aria-busy="true"] .ds-message__content {
    animation: none;
    -webkit-mask-image: none;
    mask-image: none;
    color: var(--ds-text-secondary);
  }
}

/* The quiet line: a timestamp, a model name, a token count. Separate from the
   actions row because it is not a control — a row named actions holding a
   timestamp is a lie an agent will follow. Whether it appears at all is the
   thread's call, since grouping decides when a stamp repeats; where it sits is
   this component's. */
.ds-message__meta {
  margin: 0;
  color: var(--ds-text-secondary);
  font-size: var(--ds-font-size-caption);
}

/* ---------- Toast ---------- */

/* One region per app. It exists before any message lands (a live region only
   announces content added after it is present) and carries the z layer and
   the stacking — toasts inside stay static and unaware of position. */
.ds-toast-region {
  position: fixed;
  bottom: var(--ds-space-4);
  right: var(--ds-space-4);
  display: grid;
  justify-items: end;
  gap: var(--ds-space-2);
  z-index: var(--ds-z-toast);
}
/* The region renders the newest five; older toasts stay in the DOM, hidden,
   and re-reveal as visible ones leave. The cap is visual, not destructive —
   dismissing a toast brings the queued one back — and this rule owns it
   entirely: hosts append and remove, never manage visibility. */
.ds-toast-region .ds-toast:nth-last-child(n+6) { display: none; }

.ds-toast {
  display: flex;
  /* Wrap exists for the actions row; the content's zero flex-basis keeps the
     message row itself from ever wrapping (text shrinks, the X stays put). */
  flex-wrap: wrap;
  /* Centred on one line, top-aligned once a body or actions row makes the
     toast multi-line (the :has() below). Unlike Alert, top alignment is not
     free here: the X's size.icon.lg box sets the row height, so a top-aligned
     single line reads visibly high against it. The parts stand in for line
     count — a lone title that wraps stays centred, but a title is a few words
     by contract and half container.sm is room for them. */
  align-items: center;
  gap: var(--ds-space-3);
  /* One fixed width — a stack of toasts aligns instead of raggedly hugging
     each message; content wraps inside it. */
  width: calc(var(--ds-container-sm) / 2);
  padding: var(--ds-space-3);
  border-radius: var(--ds-radius-control);
  background: var(--ds-surface-raised);
  border: var(--ds-border-thin) solid var(--ds-border-subtle);
  box-shadow: var(--ds-shadow-medium);
  color: var(--ds-text-primary);
  font-size: var(--ds-font-size-copy-sm);
}
/* The toast owns its icon size — inline ds-icon--* modifiers are inert (Button's rule). */
.ds-toast .ds-icon { width: var(--ds-size-icon-md); height: var(--ds-size-icon-md); }
/* The extra right margin stacks on the root gap: text keeps space.5 of air from
   the X — the control side needs more separation than the icon's reading side. */
.ds-toast__content { display: grid; gap: var(--ds-space-1); flex: 1 1 0; min-width: 0; margin-right: var(--ds-space-2); }
.ds-toast__dismiss {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: var(--ds-size-icon-lg);
  height: var(--ds-size-icon-lg);
  padding: 0;
  border: none;
  background: transparent;
  border-radius: var(--ds-radius-small);
  color: inherit;
  cursor: pointer;
  /* Pinned to the padding edge (auto), pulled back by the glyph's inset inside
     its box (negative space.1) so the X sits optically on the root's space.3
     padding grid. Centred single-line toasts need no vertical pull; top-aligned
     multi-line ones take it below, to sit level with the icon and title. */
  margin: 0 calc(-1 * var(--ds-space-1)) 0 auto;
  /* Presence, not fill: the X sits on any of the five toast surfaces (raised +
     four solid status fills), so no fixed state role fits — hover steps
     opacity instead. The literal stands until a presence primitive earns one. */
  opacity: 0.7;
}
.ds-toast:has(.ds-toast__body, .ds-toast__actions) { align-items: flex-start; }
.ds-toast:has(.ds-toast__body, .ds-toast__actions) .ds-toast__dismiss { margin-top: calc(-1 * var(--ds-space-1)); }
.ds-toast__dismiss:hover { opacity: 1; }
.ds-toast__dismiss .ds-icon { width: var(--ds-size-icon-sm); height: var(--ds-size-icon-sm); }
/* Actions are ordinary Buttons (sm secondary) in their own full-width row under
   the content, flush right against the toast padding; the root's wrap + gap
   make the row. */
.ds-toast__actions {
  display: flex;
  flex-basis: 100%;
  justify-content: flex-end;
  gap: var(--ds-space-2);
}
/* Explicit sizes, Dialog's precedent: title and body are <p> and page-level
   type rules (the specimen's own `section p`) would override inheritance.
   Colors: the pair lives on the root — icon and title ride it via inherit /
   currentColor; only body diverges (text.secondary) on the neutral surface. */
/* Leading is pinned alongside the size for the same reason Alert pins both:
   undeclared, it fell to whatever the host's prose rules set. A toast escapes
   that today only because the region renders outside any section — the sibling
   inline component did not, and rendered a size larger. */
.ds-toast__title { margin: 0; color: inherit; font-size: var(--ds-font-size-copy-sm); line-height: var(--ds-font-line-normal); font-weight: var(--ds-font-weight-semibold); }
.ds-toast__body { margin: 0; color: var(--ds-text-secondary); font-size: var(--ds-font-size-copy-sm); line-height: var(--ds-font-line-normal); }
/* Status variants take the solid pair, always — emphasis follows lifetime: a
   toast is glanced at in passing, so its status registers via the saturated
   mode-tuned fill; the subtle tint is Alert's (persistent, in-flow). A
   subtle status toast was an Alert with an X — the tiers no longer overlap.
   The hairline drops: the fill defines its own edge. */
.ds-toast--success { background: var(--ds-status-success-solid-bg); color: var(--ds-status-success-solid-text); border-color: transparent; }
.ds-toast--warning { background: var(--ds-status-warning-solid-bg); color: var(--ds-status-warning-solid-text); border-color: transparent; }
.ds-toast--error   { background: var(--ds-status-error-solid-bg);   color: var(--ds-status-error-solid-text);   border-color: transparent; }
.ds-toast--info    { background: var(--ds-status-info-solid-bg);    color: var(--ds-status-info-solid-text);    border-color: transparent; }
.ds-toast--success .ds-toast__body,
.ds-toast--warning .ds-toast__body,
.ds-toast--error .ds-toast__body,
.ds-toast--info .ds-toast__body { color: inherit; }

/* ---------- Divider ---------- */

.ds-divider {
  display: block;
  border: none;
  margin: 0;
  padding: 0;
  flex: none;
  width: 100%;
  height: var(--ds-border-thin);
  background: var(--ds-border-subtle);
}
.ds-divider--strong { background: var(--ds-border-strong); }
.ds-divider--vertical {
  width: var(--ds-border-thin);
  height: auto;
  align-self: stretch;
  min-height: var(--ds-size-icon-lg);
}

/* ---------- Top bar ---------- */

.ds-topbar {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: var(--ds-space-4);
  min-height: var(--ds-size-control-lg);
  padding-inline: var(--ds-page-gutter);
}
.ds-topbar__start {
  display: inline-flex;
  align-items: center;
  gap: var(--ds-space-3);
  min-width: 0;
  grid-column: 1;
  justify-self: start;
}
.ds-topbar__brand {
  flex: none;
  color: var(--ds-context-text-primary, var(--ds-text-primary));
  font-size: var(--ds-font-size-copy-md);
  font-weight: var(--ds-font-weight-semibold);
  line-height: var(--ds-font-line-none);
}
.ds-topbar__context {
  overflow: hidden;
  color: var(--ds-text-secondary);
  font-size: var(--ds-font-size-copy-sm);
  line-height: var(--ds-font-line-none);
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* A box that clips its overflow IS a clip rect, and a clip rect has to be at
   least as tall as the font's own line box. These rows set line-height: none
   for rhythm — min-height owns the row's height, so a 1em line box costs
   nothing right up until something clips against it, and then it costs every
   part of the glyph that falls outside 1em: measured, 14px of box against 18px
   of ink, so 2px off the top and 2px off the bottom. "Sao Paulo (gru1)" lost
   its tilde and the feet of its parentheses (F57).
   `normal` rather than a ratio token, because `normal` is the font's own
   recommendation: any ratio we pick clears one brand's metrics and clips the
   next one's, which is the whole failure again one rebrand later. Row rhythm
   is untouched — min-height still decides the row's height.
   Declared after every one of these components so source order settles it. */
.ds-menu__label, .ds-select__label, .ds-command__label, .ds-topbar__context {
  line-height: normal;
}

.ds-topbar__nav {
  display: flex;
  align-items: center;
  gap: var(--ds-space-1);
  grid-column: 2;
  justify-self: center;
}
.ds-topbar__link {
  display: inline-flex;
  align-items: center;
  min-height: var(--ds-size-control-sm);
  padding: var(--ds-space-1-5) var(--ds-space-3);
  border-radius: var(--ds-radius-nested);
  color: var(--ds-context-text-secondary, var(--ds-text-secondary));
  font-size: var(--ds-font-size-copy-sm);
  line-height: var(--ds-font-line-none);
  text-decoration: none;
}
/* A mapped non-neutral surface still wins: .ds-on-surface hands down hues
   picked for that surface, and those outrank a derived tint. Unmapped, the row
   falls back to the ghost tint like every other transparent row rather than to
   control.bg-hover, which was the two-step drop. */
.ds-topbar__link:hover,
.ds-topbar__link.demo-hover { background: var(--ds-context-control-hover, color-mix(in srgb, currentColor var(--ds-action-ghost-hover), transparent)); color: var(--ds-context-text-primary, var(--ds-text-primary)); }
.ds-topbar__link:active,
.ds-topbar__link.demo-active { background: var(--ds-context-control-active, color-mix(in srgb, currentColor var(--ds-action-ghost-active), transparent)); color: var(--ds-context-text-primary, var(--ds-text-primary)); }
.ds-topbar__link[aria-current="page"] { background: var(--ds-context-control-hover, var(--ds-selection-bg)); color: var(--ds-context-text-primary, var(--ds-text-primary)); }
/* space.1, not space.2: icon-only controls are the common actions cluster,
   and at 8px two ghost icons read as scattered rather than grouped. The gap
   is a misclick buffer and a grouping signal, never zero — touching cells
   are Segmented's silhouette, not independent actions. */
.ds-topbar__actions {
  display: inline-flex;
  align-items: center;
  gap: var(--ds-space-1);
  grid-column: 3;
  justify-self: end;
}

/* ---------- Sidebar navigation ---------- */

.ds-sidebar-nav {
  display: grid;
  align-content: start;
  gap: var(--ds-space-4);
  padding: var(--ds-space-4) var(--ds-space-3);
}
/* The label gets its own air before the list it names; the group-to-group gap
   above stays the larger separation, so the hierarchy reads label < group. */
.ds-sidebar-nav__group { display: grid; gap: var(--ds-space-2); }
/* A group label must read as a label, not as an emphasized destination. Color
   cannot carry that distinction: one step quieter than text.secondary is
   text.muted at 2.56:1 in light mode, far under AA for text that names a
   navigation region. So the separation runs on channels that cost no contrast
   — regular weight at the smaller label-sm size makes it recede instead of
   competing (medium weight was what made it read as the group's loudest row),
   and wide tracking supplies the label register. This is the sentence-case
   label-size use the wide token was kept for, and its first consumer. */
.ds-sidebar-nav__group-label,
.ds-sidebar-nav__group > .ds-sidebar-nav__label {
  margin: 0;
  padding: var(--ds-space-1) var(--ds-space-3);
  color: var(--ds-text-secondary);
  font-size: var(--ds-font-size-label-sm);
  line-height: var(--ds-font-line-none);
  font-weight: var(--ds-font-weight-regular);
  letter-spacing: var(--ds-font-tracking-wide);
}
.ds-sidebar-nav__destination-label { min-width: 0; }
.ds-sidebar-nav__list {
  display: grid;
  gap: var(--ds-space-1);
  margin: 0;
  padding: 0;
  list-style: none;
}
.ds-sidebar-nav__link {
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
  min-height: var(--ds-size-control-sm);
  padding: var(--ds-space-1-5) var(--ds-space-3);
  border-radius: var(--ds-radius-nested);
  /* Destinations sit at full reading strength; the group label above them is
     the quiet one. Rows are what gets scanned, and holding them at
     text.secondary made the nav recede behind its own labels. */
  color: var(--ds-text-primary);
  text-decoration: none;
  font-size: var(--ds-font-size-copy-sm);
  line-height: var(--ds-font-line-none);
}
.ds-sidebar-nav__icon.ds-icon { width: var(--ds-size-icon-sm); height: var(--ds-size-icon-sm); }
.ds-sidebar-nav__trailing { margin-left: auto; }
/* Current is color-only: a weight step widens labels and shifts the row's
   icon/trailing alignment when aria-current moves between destinations. */
/* selection.bg, not control.bg: selection is a state of the content, not an
   interaction with it, and control.bg lands 1.05:1 on surface.canvas — which
   was invisible the moment rows moved to text.primary and the color delta that
   had been carrying the state disappeared. */
.ds-sidebar-nav__link[aria-current="page"] {
  background: var(--ds-selection-bg);
  color: var(--ds-text-primary);
}
/* Transparent rows: the ghost tint, mixed from the hovered colour. Setting
   color in the same rule is what makes the mix resolve against text.primary
   rather than the row's resting secondary — the ghost Button's own pattern. */
.ds-sidebar-nav__link:hover,
.ds-sidebar-nav__link.demo-hover { background: color-mix(in srgb, currentColor var(--ds-action-ghost-hover), transparent); color: var(--ds-text-primary); }
.ds-sidebar-nav__link:active,
.ds-sidebar-nav__link.demo-active { background: color-mix(in srgb, currentColor var(--ds-action-ghost-active), transparent); color: var(--ds-text-primary); }

/* Legacy flat sidenav remains supported; new work uses SidebarNav's explicit
   group/list/link anatomy so contracts can address every part. */
.ds-sidenav { display: grid; gap: var(--ds-space-1); padding: var(--ds-space-3); }
.ds-sidenav a {
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
  padding: var(--ds-space-2) var(--ds-space-3);
  border-radius: var(--ds-radius-nested);
  color: var(--ds-text-secondary);
  text-decoration: none;
  font-size: var(--ds-font-size-button-md);
  line-height: var(--ds-font-line-none);
}
.ds-sidenav a:hover { background: color-mix(in srgb, currentColor var(--ds-action-ghost-hover), transparent); color: var(--ds-text-primary); }
.ds-sidenav a[aria-current="page"] {
  background: var(--ds-selection-bg);
  color: var(--ds-text-primary);
}
