/* ftl-themes: blue-future — generated by scripts/build.sh.
 * Do not edit; edit themes/blue-future/theme.css instead. */
@layer ui {
/* ftl-themes: minimal reset. Loaded once, before core.css. Does not
 * touch color or type — that's the token contract's job (CONTRACT.md). */

*, *::before, *::after {
  box-sizing: border-box;
}

html, body {
  margin: 0;
  padding: 0;
}

h1, h2, h3, h4, h5, h6, p, figure, blockquote {
  margin: 0;
}

img, svg, video {
  display: block;
  max-width: 100%;
}

button, input, select, textarea {
  font: inherit;
  color: inherit;
}

button {
  background: none;
  border: none;
}

/* v5: controls keep the arrow cursor, like an application, rather than the
   web's hand pointer (PLAN.md §4). Text fields keep the I-beam. */
a[href], button, summary, label, select,
[role="button"], [role="tab"], [role="menuitem"], [role="option"], [role="switch"] {
  cursor: var(--cursor-default, default);
}

a {
  color: inherit;
  text-decoration: none;
}

ul, ol {
  margin: 0;
  padding: 0;
  list-style: none;
}

table {
  border-collapse: collapse;
  width: 100%;
}

/* Components set their own display, which would otherwise beat the UA's
   [hidden] rule. until-found is left to the browser: it hides through
   content-visibility so find-in-page can still reveal it. */
[hidden]:not([hidden="until-found"]) {
  display: none !important;
}
/* ftl-themes: core component structure.
 *
 * Every component reads its look from local custom properties with
 * inline fallbacks — `background: var(--btn-bg, var(--surface-2))`
 * — and variants (.btn-danger etc.) set those properties rather than
 * re-declaring background/color. This is load-bearing, not stylistic: a
 * theme scopes its rules as `html[data-theme="x"] .btn` (specificity
 * 0,2,1), which outranks an unscoped `.btn-danger` (0,1,0), so a theme
 * that styled the base button directly used to silently erase every
 * variant's fill — a delete button rendered identically to a normal one.
 *
 * The rule that makes it work: a declaration on the element itself always
 * beats an inherited one, whatever the specificity. So themes set
 * component defaults at the ROOT (`html[data-theme="x" ] { --btn-bg: … }`,
 * inherited), and variants keep winning because their declaration sits on
 * the element. Themes only use element selectors when they mean to target
 * one specific variant. scripts/check.sh enforces this.
 *
 * Load order: reset.css, core.css, then exactly one
 * themes/<name>/theme.css (or the equivalent dist/<name>.css bundle).
 * See CONTRACT.md for the full token list and component vocabulary.
 */

/* --- Token contract: fallback values -----------------------------------
 * Deliberately achromatic: a theme that forgets a token gets an obviously
 * wrong gray rather than a plausible-looking color that happens to be
 * invisible on its palette. scripts/check.sh fails on any missing token. */
:root {
  --bg: #1a1a1a;
  --surface: #242424;
  --surface-2: #303030;
  --border: #555555;
  --hairline: #3a3a3a;
  --text: #e8e8e8;
  --muted: #999999;
  --accent: #888888;
  --accent-2: #aaaaaa;
  --danger: #b04040;
  --success: #40a060;
  --warning: #b08030;
  /* Foreground colors for filled surfaces. Never paint text on an accent
     fill with --bg: that only reads on dark themes, and silently fails
     on light ones. */
  --on-accent: #000000;
  --on-danger: #ffffff;
  --on-success: #000000;
  --on-surface: var(--text);
  --radius: 0.25rem;
  --font: system-ui, sans-serif;
  --font-mono: ui-monospace, Consolas, monospace;
  --flare: #888888;
  /* Focus: themes may recolor (--focus) or add a glow
     (--focus-ring), but the outline below is always present, so a
     theme cannot accidentally leave a control with no focus indicator. */
  --focus: var(--accent);
  --focus-ring: none;
  /* Density multiplier on the paddings that decide how much data fits on a
     screen. A dense console theme sets ~0.8; a chunky touch-first theme
     sets ~1.25. Must have a value here: it is used inside calc(). */
  --density: 1;
}

/* v5 layer scale (PLAN.md §6). Every z-index in core reads one of these, so
   layers stack the same way everywhere and a theme or app can move a whole
   layer at once. Order: raised < sticky < modal < drawer < dropdown/popover
   < toast < tooltip < focused window. */
:root {
  --z-raised: 1;
  --z-sticky: 5;
  --z-modal: 1000;
  --z-drawer: 1040;
  --z-dropdown: 1050;
  --z-toast: 1100;
  --z-tooltip: 1200;
  --z-window: var(--window-focus-z, 1500);
}

html {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
}

a { color: var(--link, var(--accent)); }
/* Links in running text are underlined: colour alone rarely reaches 3:1
   against the text around them (WCAG 1.4.1). Class-less links only; links
   that are controls keep their own shape. */
:where(p, li, dd, td, blockquote, figcaption, small) > a[href]:not([class], [role]) {
  text-decoration-line: underline;
  text-decoration-thickness: max(1px, 0.08em);
  text-underline-offset: 0.18em;
}

:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* --- .btn ----------------------------------------------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5em;
  padding: var(--btn-padding, calc(0.5em * var(--density)) calc(1.1em * var(--density)));
  border: var(--btn-border-width, 1px) solid var(--btn-border, var(--border));
  border-radius: var(--btn-radius, var(--radius));
  background: var(--btn-bg, var(--surface-2));
  color: var(--btn-fg, var(--text));
  font-family: inherit;
  font-size: var(--btn-font-size, 0.9em);
  font-weight: var(--btn-weight, 400);
  letter-spacing: var(--btn-tracking, normal);
  text-transform: var(--btn-transform, none);
  box-shadow: var(--btn-shadow, none);
  transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease, transform 0.1s ease;
}
@media (hover: hover) {
.btn:hover {
  background: var(--btn-bg-hover, var(--btn-bg, var(--surface-2)));
  border-color: var(--btn-border-hover, var(--accent));
  box-shadow: var(--btn-shadow-hover, var(--btn-shadow, none));
}
}
.btn:active { transform: translateY(1px); }
.btn:disabled,
.btn[aria-disabled="true"] { opacity: 0.45; cursor: var(--cursor-not-allowed, not-allowed); transform: none; }
.btn-sm { padding: var(--btn-padding-sm, 0.3em 0.8em); font-size: 0.78em; }

.btn-primary {
  --btn-bg: var(--accent);
  --btn-fg: var(--on-accent);
  --btn-border: var(--accent);
  --btn-weight: 600;
}
.btn-danger {
  --btn-bg: var(--danger);
  --btn-fg: var(--on-danger);
  --btn-border: var(--danger);
  --btn-weight: 600;
}
.btn-success {
  --btn-bg: var(--success);
  --btn-fg: var(--on-success);
  --btn-border: var(--success);
}
.btn-secondary {
  --btn-bg: transparent;
  --btn-fg: var(--accent-text, var(--accent));
  --btn-border: var(--border);
}
/* Tertiary: the quietest action that is still accent-coloured — text with
   no fill or border until hover. */
.btn-tertiary {
  --btn-bg: transparent;
  --btn-fg: var(--accent-text, var(--accent));
  --btn-border: transparent;
  --btn-bg-hover: color-mix(in srgb, var(--accent) 12%, transparent);
  --btn-border-hover: transparent;
}
.btn-ghost {
  --btn-bg: transparent;
  --btn-fg: var(--text);
  --btn-border: transparent;
  --btn-bg-hover: var(--surface-2);
  --btn-border-hover: transparent;
}
/* In a filled table head the button follows the head's ink. */
.table th .btn-ghost { --btn-fg: var(--table-head-fg, var(--text)); }

/* --- Form controls ------------------------------------------------------ */
.input,
.select,
.textarea {
  display: block;
  width: 100%;
  padding: var(--input-padding, 0.5em 0.75em);
  border: var(--input-border-width, 1px) solid var(--input-border, var(--border));
  border-radius: var(--input-radius, var(--radius));
  background: var(--input-bg, var(--surface));
  color: var(--input-fg, var(--text));
  box-shadow: var(--input-shadow, none);
  font-family: inherit;
  font-size: 0.9em;
}
.textarea { min-height: 5rem; resize: vertical; }
/* The outline is never removed here — see --focus in the token block.
   A theme adds its own glow via --focus-ring instead of replacing it. */
.input:focus-visible,
.select:focus-visible,
.textarea:focus-visible {
  border-color: var(--focus);
  box-shadow: var(--focus-ring, var(--input-shadow, none));
}
.input:disabled,
.select:disabled,
.textarea:disabled { opacity: 0.5; cursor: var(--cursor-not-allowed, not-allowed); }

/* Readonly: a value the user can select and copy but not edit. Flatter and
   quieter than an editable field; no not-allowed cursor (it is not
   disabled). --input-readonly-bg / -fg override. */
.input[readonly],
.textarea[readonly] {
  background: var(--input-readonly-bg, var(--surface-2));
  color: var(--input-readonly-fg, var(--input-fg, var(--text)));
}
/* .input.is-mirror — a display-only mirror of another control's value
   (see CONTRACT.md "Mirror input"). Pair with readonly + tabindex="-1". */
.input.is-mirror {
  background: var(--input-mirror-bg, var(--surface-2));
  color: var(--input-mirror-fg, var(--muted));
  cursor: var(--cursor-default, default);
  box-shadow: none;
}
.input.is-mirror:focus-visible { border-color: var(--input-border, var(--border)); box-shadow: none; }

/* Error state: `.is-invalid` or `aria-invalid="true"` on the control. The
   tint mixes danger into the control's own background, never into
   transparent, so it reads the same wherever the field sits. */
:is(.input, .select, .textarea):is(.is-invalid, [aria-invalid="true"]) {
  border-color: var(--danger);
  background-color: var(--input-error-bg, var(--input-bg, var(--surface)));
}
@supports (color: color-mix(in srgb, red 50%, transparent)) {
  :is(.input, .select, .textarea):is(.is-invalid, [aria-invalid="true"]) {
    background-color: var(--input-error-bg, color-mix(in srgb, var(--danger) 12%, var(--input-bg, var(--surface))));
  }
}
:is(.input, .select, .textarea):is(.is-invalid, [aria-invalid="true"]):focus-visible {
  border-color: var(--danger);
  outline-color: var(--danger);
  box-shadow: var(--input-error-ring, var(--input-shadow, none));
}
.input-group > :is(.is-invalid, [aria-invalid="true"]) { z-index: var(--z-raised); }

/* Valid state: `.is-valid` on the control, or native :user-valid inside a
   form marked .is-validated (only required fields go green, so an optional
   blank stays neutral). .is-validated also shows :user-invalid controls in
   the error state once the user has touched them. */
:is(.input, .select, .textarea):is(.is-valid, .is-validated :user-valid[required]) {
  border-color: var(--success);
  background-color: var(--input-valid-bg, var(--input-bg, var(--surface)));
}
@supports (color: color-mix(in srgb, red 50%, transparent)) {
  :is(.input, .select, .textarea):is(.is-valid, .is-validated :user-valid[required]) {
    background-color: var(--input-valid-bg, color-mix(in srgb, var(--success) 10%, var(--input-bg, var(--surface))));
  }
}
:is(.input, .select, .textarea):is(.is-valid, .is-validated :user-valid[required]):focus-visible { border-color: var(--success); outline-color: var(--success); }
.is-validated :is(.input, .select, .textarea):user-invalid {
  border-color: var(--danger);
  background-color: var(--input-error-bg, color-mix(in srgb, var(--danger) 12%, var(--input-bg, var(--surface))));
}

/* Sizes: on .input, .select or .textarea. */
.input-sm {
  padding: var(--input-padding-sm, 0.3em 0.6em);
  font-size: var(--input-font-size-sm, 0.78em);
}
.input-lg {
  padding: var(--input-padding-lg, 0.75em 1em);
  font-size: var(--input-font-size-lg, 1.05em);
}
.select.input-sm,
.select.input-lg { padding-inline-end: 2.2em; }

/* .select's dropdown *list* is OS chrome no CSS can reach, but the
   closed box's arrow was always the browser's own — a border-only
   triangle keeps it colorable without an SVG/data-URI asset. Themes
   that want their real native arrow back can override
   `appearance: auto` on `.select`. */
.select {
  appearance: none;
  padding-inline-end: 2.2em;
  background-image: linear-gradient(45deg, transparent 50%, var(--select-arrow-fg, var(--muted)) 50%),
    linear-gradient(135deg, var(--select-arrow-fg, var(--muted)) 50%, transparent 50%);
  background-position: calc(100% - 1.1em) calc(50% - 3px), calc(100% - 0.75em) calc(50% - 3px);
  background-size: 6px 6px, 6px 6px;
  background-repeat: no-repeat;
}
.select:dir(rtl) {
  background-position: 1.1em calc(50% - 3px), 0.75em calc(50% - 3px);
}

/* Autofill: browsers force their own fill/text color on
   `:-webkit-autofill`, ignoring `background`/`color` outright. The only
   reliable override is this absurdly long transition-delay trick, which
   defers the browser's own paint long enough for ours to stick. */
.input:-webkit-autofill,
.input:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--input-fg, var(--text));
  box-shadow: 0 0 0 1000px var(--input-bg, var(--surface)) inset;
  transition: background-color 600000s 0s, color 600000s 0s;
}
@media (hover: hover) {
.input:-webkit-autofill:hover {
  -webkit-text-fill-color: var(--input-fg, var(--text));
  box-shadow: 0 0 0 1000px var(--input-bg, var(--surface)) inset;
  transition: background-color 600000s 0s, color 600000s 0s;
}
}

.label {
  display: block;
  margin-block-end: 0.35em;
  color: var(--label-fg, var(--muted));
  font-size: 0.82em;
  font-weight: 500;
  letter-spacing: var(--label-tracking, normal);
  text-transform: var(--label-transform, none);
}
.field { margin-block-end: 1rem; }
.field-hint { margin-block-start: 0.3em; font-size: 0.78em; color: var(--muted); }
/* .field-error — the validation message under a control. Give it an id,
   point the control's aria-describedby at it, and (for messages injected
   after load) put role="alert" or aria-live="polite" on it. */
.field-error { margin-block-start: 0.3em; font-size: 0.78em; color: var(--danger-text, var(--danger)); }
.field-error:empty { display: none; }
/* .field-valid — the confirmation under a control, shown only while the
   control is valid (.is-valid, or :user-valid in an .is-validated form). */
.field-valid { display: none; margin-block-start: 0.3em; font-size: 0.78em; color: var(--success-text, var(--success)); }
.field:has(> .is-valid, > * > .is-valid) > .field-valid,
.is-validated .field:has(:user-valid[required]) > .field-valid { display: block; }
/* .is-validated hides a server-rendered .field-error until the control is
   actually invalid. */
.is-validated .field:not(:has(:user-invalid, .is-invalid, [aria-invalid="true"])) > .field-error { display: none; }

/* .field.is-floating — the label sits inside the control and floats up
   once it has focus or a value. The control needs placeholder=" " (a
   space) so CSS can tell when it is empty; a .select always floats.
     <div class="field is-floating">
       <input class="input" id="e" type="email" placeholder=" ">
       <label class="label" for="e">Email</label>
     </div> */
.field.is-floating { position: relative; }
.field.is-floating > :is(.input, .select, .textarea) { padding: 1.5em 0.75em 0.45em; }
.field.is-floating > .label {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
  max-inline-size: 100%;
  margin: 0;
  padding: 1.05em calc(0.75em + var(--input-border-width, 1px));
  overflow: hidden;
  color: var(--label-fg, var(--muted));
  font-size: 0.9em;
  white-space: nowrap;
  text-overflow: ellipsis;
  transform-origin: 0 0;
  pointer-events: none;
  transition: transform var(--motion-duration-s, 120ms) var(--motion-ease, ease);
}
.field.is-floating > :is(.input, .textarea):is(:focus, :not(:placeholder-shown)) + .label,
.field.is-floating > .select + .label { transform: translateY(-0.6em) scale(0.8); }
.field.is-floating > .label:dir(rtl) { transform-origin: 100% 0; }
.field.is-floating > :is(.input, .select, .textarea)::placeholder { color: transparent; }

/* .field-row — a labelled control in a bordered card: the settings-sheet
 * row pattern from the blue-future reference implementation (a recorder's
 * telemetry dashboard). Distinct from .field (a lone input + hint): the
 * row is a destination reviewers scan down, so it carries a surface, a
 * hover state, and ellipsis discipline on the label. */
.field-row {
  display: flex;
  align-items: center;
  gap: 0.8em;
  background: var(--field-row-bg, var(--surface-2));
  border: var(--field-row-border-width, 1px) solid var(--field-row-border, var(--border));
  border-radius: var(--field-row-radius, var(--panel-radius, var(--radius)));
  padding: var(--field-row-padding, 0.55em 0.8em);
  margin-block-end: var(--field-row-gap, 0.5rem);
}
@media (hover: hover) {
.field-row:hover {
  border-color: var(--field-row-border-hover, var(--hairline));
}
}
.field-row > .label {
  flex: 1;
  margin-block-end: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.field-row > .field-hint { margin-block-start: 0; flex: none; }

/* .input-group — an input with joined addons: a unit label, a static
   prefix, or an attached button. Children share one border; only the
   outer ends keep their radius. */
.input-group { display: flex; align-items: stretch; width: 100%; }
.input-group > * { position: relative; }
.input-group > .input,
.input-group > .select { flex: 1; min-width: 0; width: auto; }
.input-group > :not(:first-child) {
  border-start-start-radius: 0;
  border-end-start-radius: 0;
  margin-inline-start: calc(-1 * var(--input-border-width, 1px));
}
.input-group > :not(:last-child) {
  border-start-end-radius: 0;
  border-end-end-radius: 0;
}
.input-group > :focus-visible { z-index: var(--z-raised); }
.input-addon {
  display: inline-flex;
  align-items: center;
  padding: var(--input-padding, 0.5em 0.75em);
  background: var(--input-addon-bg, var(--surface-2));
  color: var(--input-addon-fg, var(--muted));
  border: var(--input-border-width, 1px) solid var(--input-border, var(--border));
  font-size: 0.9em;
  white-space: nowrap;
}


/* Interactive-target sizing scales with --density (default 1, so every
   value below computes byte-identical to its old literal at the default) —
   a chunky touch-first theme at density 1.15+ gets bigger grab targets to
   match its roomier padding, not just more breathing room around the same
   16px thumb. Per-part tokens (--switch-*, --slider-thumb-*) still
   override the computed size same as any other token. */
.checkbox,
.radio {
  accent-color: var(--accent);
  width: calc(1.1em * var(--density));
  height: calc(1.1em * var(--density));
  margin: 0;
  flex: none;
}

/* .check — a checkbox or radio with its text beside it:
   <label class="check"><input class="radio" type="radio" name="x"> Label</label> */
.check {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  color: var(--check-fg, var(--text));
}
.check:has(:disabled) { opacity: 0.5; cursor: var(--cursor-not-allowed, not-allowed); }

/* .radio-group — a <fieldset> of .check rows with a <legend>. */
.radio-group {
  display: flex;
  flex-direction: var(--radio-group-direction, column);
  flex-wrap: wrap;
  gap: 0.5em 1.25em;
  margin: 0 0 1rem;
  padding: 0;
  border: 0;
  min-width: 0;
}
.radio-group > legend {
  padding: 0;
  margin-block-end: 0.4em;
  color: var(--label-fg, var(--muted));
  font-size: 0.82em;
  font-weight: 500;
  letter-spacing: var(--label-tracking, normal);
  text-transform: var(--label-transform, none);
}
.radio-group.is-inline { --radio-group-direction: row; }

.switch {
  position: relative;
  display: inline-flex;
  align-items: center;
  width: calc(2.6em * var(--density));
  height: calc(1.4em * var(--density));
  --switch-shift: calc(1.2em * var(--density));
}
.switch:dir(rtl) { --switch-shift: calc(-1.2em * var(--density)); }
.switch input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}
.switch-track {
  position: absolute;
  inset: 0;
  border-radius: var(--switch-radius, 999px);
  background: var(--switch-bg, var(--surface-2));
  border: var(--switch-border-width, 1px) solid var(--switch-border, var(--border));
  transition: border-color 0.15s ease, background 0.15s ease;
}
.switch-thumb {
  position: absolute;
  top: 2px;
  inset-inline-start: 2px;
  width: calc(1.4em * var(--density) - 6px);
  height: calc(1.4em * var(--density) - 6px);
  border-radius: var(--switch-thumb-radius, 50%);
  background: var(--switch-thumb-bg, var(--muted));
  transition: transform 0.15s ease, background 0.15s ease;
}
.switch input:checked ~ .switch-track,
.switch[aria-checked="true"] .switch-track {
  border-color: var(--switch-border-on, var(--accent));
  background: var(--switch-bg-on, var(--surface-2));
}
.switch input:checked ~ .switch-track .switch-thumb,
.switch[aria-checked="true"] .switch-thumb {
  transform: translateX(var(--switch-shift));
  background: var(--switch-thumb-bg-on, var(--accent));
}
.switch input:focus-visible ~ .switch-track {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* <button class="switch" role="switch" aria-checked="false"> — the same
   switch driven by aria-checked, for a control the app toggles in script
   (the attribute is also what assistive tech reads). The button sizes to
   its content: the track becomes a normal flex child and an optional
   .switch-label sits beside it. */
button.switch {
  appearance: none;
  width: auto;
  height: auto;
  gap: var(--switch-label-gap, 0.6em);
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  color: var(--switch-label-fg, var(--text));
  font: inherit;
  text-align: start;
}
button.switch .switch-track {
  position: relative;
  inset: auto;
  flex: none;
  width: calc(2.6em * var(--density));
  height: calc(1.4em * var(--density));
}
button.switch:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: var(--switch-radius, 999px);
}
button.switch:disabled { opacity: 0.5; cursor: var(--cursor-not-allowed, not-allowed); }
.switch-label { font-size: 0.9em; line-height: 1.2; }

.slider {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: var(--slider-height, 0.35rem);
  border-radius: var(--slider-radius, var(--radius));
  background: var(--slider-bg, var(--surface-2));
  border: var(--slider-border-width, 1px) solid var(--slider-border, var(--border));
}
.slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: var(--slider-thumb-size, calc(1rem * var(--density)));
  height: var(--slider-thumb-size, calc(1rem * var(--density)));
  border-radius: var(--slider-thumb-radius, 50%);
  background: var(--slider-thumb-bg, var(--accent));
  border: 2px solid var(--bg);
  box-shadow: var(--slider-thumb-shadow, none);
}
.slider::-moz-range-thumb {
  width: var(--slider-thumb-size, calc(1rem * var(--density)));
  height: var(--slider-thumb-size, calc(1rem * var(--density)));
  border-radius: var(--slider-thumb-radius, 50%);
  background: var(--slider-thumb-bg, var(--accent));
  border: 2px solid var(--bg);
  box-shadow: var(--slider-thumb-shadow, none);
}

/* .slider.is-vertical — a fader-style range input, up = larger. Length via
   --slider-length; min/max/step stay HTML attributes. Thickness reuses
   --slider-height. */
.slider.is-vertical {
  writing-mode: vertical-lr;
  direction: rtl;
  width: var(--slider-height, 0.35rem);
  height: var(--slider-length, 4.5rem);
  vertical-align: middle;
}

/* --- Surfaces ---------------------------------------------------------- */
.panel {
  background: var(--panel-bg, var(--surface));
  color: var(--on-surface);
  border: var(--panel-border-width, 1px) solid var(--panel-border, var(--border));
  border-radius: var(--panel-radius, var(--radius));
  box-shadow: var(--panel-shadow, none);
  /* --surface-pad is read back by .panel-header's negative margin, so the
     two can't drift apart (v4 paired a density-scaled pad with a fixed
     -1rem pull). */
  --surface-pad: var(--panel-pad, calc(var(--space-m) * var(--density)));
  padding: var(--surface-pad);
}
.panel-header {
  margin: calc(-1 * var(--surface-pad, 1rem)) calc(-1 * var(--surface-pad, 1rem)) var(--space-m);
  padding: var(--panel-header-pad, var(--space-xs) var(--surface-pad, 1rem));
  background: var(--panel-header-bg, transparent);
  color: var(--panel-header-fg, var(--muted));
  border-bottom: 1px solid var(--panel-border, var(--border));
  border-radius: var(--panel-radius, var(--radius)) var(--panel-radius, var(--radius)) 0 0;
  font-weight: 600;
  font-size: 0.85em;
  letter-spacing: var(--label-tracking, normal);
  text-transform: var(--label-transform, none);
}

/* Fixed overlay shipped here rather than left to each app: it's the
   component most likely to diverge, and it needs no per-app knowledge. */
.modal-overlay {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-m);
  background: var(--overlay-bg, rgba(0, 0, 0, 0.6));
  backdrop-filter: var(--overlay-blur, none);
}
.modal {
  background: var(--modal-bg, var(--panel-bg, var(--surface)));
  color: var(--on-surface);
  border: var(--panel-border-width, 1px) solid var(--modal-border, var(--border));
  border-radius: var(--modal-radius, var(--panel-radius, var(--radius)));
  box-shadow: var(--modal-shadow, var(--panel-shadow, none));
  --surface-pad: var(--modal-pad, var(--space-l));
  padding: var(--surface-pad);
  /* width, not just max-width: inside the flex overlay a modal would
     otherwise shrink to its content and change width as it changes. */
  width: min(32rem, calc(100vw - 2rem));
  max-width: min(32rem, calc(100vw - 2rem));
  max-height: calc(100vh - 2rem);
  overflow: auto;
}
.modal-header {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: calc(-1 * var(--surface-pad, 1.5rem)) calc(-1 * var(--surface-pad, 1.5rem)) var(--space-m);
  padding: var(--modal-header-pad, var(--space-xs) var(--space-m));
  background: var(--modal-header-bg, transparent);
  color: var(--modal-header-fg, inherit);
  font-weight: 600;
}
.modal-header > .btn-close {
  /* --btn-close-order: -1 with --btn-close-push: 0 puts it at the start
     (Aqua, Mac OS 9, DOS); the default is the end. */
  order: var(--btn-close-order, 0);
}
/* The caption buttons travel as a group: the first of them takes the push,
   so minimise / maximise / close sit together at the end. */
.modal-header > :is(.btn-min, .btn-max, .btn-close):not(:is(.btn-min, .btn-max, .btn-close) + *) {
  margin-inline-start: var(--btn-close-push, auto);
}
/* A close button on a filled title bar follows the title's colour, not the
   content-area --btn-close-fg (grey on a navy bar is invisible). */
.modal-header > :is(.btn-close, .btn-min, .btn-max):not(:hover) {
  color: var(--modal-header-close-fg, var(--modal-header-fg, var(--btn-close-fg, var(--muted))));
}
/* A heading used as the title takes the header's own type, not the page's
   h1-h6 styling. Two classes, so it outranks a theme's `html[data-theme] h2`. */
.modal .modal-header > :is(h1, h2, h3, h4, h5, h6) {
  margin: 0;
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  text-shadow: inherit;
}
.modal-footer { margin-block-start: 1.5rem; display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 0.5rem; }

/* Native top-layer bridge: <dialog class="modal"> and
   <div class="modal" popover> get the same panel styling from the rule
   above (a plain selector, it already matches), plus a themed ::backdrop
   here — the one piece the browser paints itself that the overlay div
   above doesn't need. */
.modal::backdrop {
  background: var(--overlay-bg, rgba(0, 0, 0, 0.6));
  backdrop-filter: var(--overlay-blur, none);
}
/* No-JS modal: `<div class="modal" popover id="m">` opened by
   `<button popovertarget="m">` (closed by Esc, a click outside, or a
   button with popovertargetaction="hide") sits in the top layer centred
   on the viewport, with the themed ::backdrop above. A <dialog> opened
   with showModal() lands in the same place. .modal is position:relative
   for the overlay-div pattern, so the top-layer forms take it back —
   keyed on the open state so the selector outranks a theme's own
   `html[data-theme] .modal { position: relative }` (several set one for
   pseudo-element decoration). */
.modal[popover]:popover-open,
dialog.modal:modal {
  position: fixed;
  inset: 0;
  margin: auto;
  height: fit-content;
}

/* Size variants — the default width above is the "medium" case. */
.modal-sm { width: min(22rem, calc(100vw - 2rem)); max-width: min(22rem, calc(100vw - 2rem)); }
.modal-lg { width: min(48rem, calc(100vw - 2rem)); max-width: min(48rem, calc(100vw - 2rem)); }
.modal-xl { width: min(64rem, calc(100vw - 2rem)); max-width: min(64rem, calc(100vw - 2rem)); }

/* Window states: maximized, minimized, resizable, focus stacking — the
   window behaviors that decompose into CSS (see CONTRACT.md "Window
   pattern"). Dragging is the one that cannot; it lives in the ~10-line
   assets/js/window.js reference snippet instead. */
.modal { position: relative; }
/* Maximized fills the overlay inset with square corners. One
   declaration, two triggers — the .is-maximized class toggle and the
   checkbox-hack input (which must immediately precede the modal) stay
   in lockstep by construction. */
.modal.is-maximized,
.window-max:checked + .modal {
  width: 100%;
  max-width: none;
  height: 100%;
  max-height: none;
  border-radius: 0;
}
/* Minimized removes the window; the app's tray restores it (a label for
   the same checkbox, flipped visible via a sibling rule — see the
   CONTRACT recipe). Class and checkbox stay paired like maximize. */
.modal.is-minimized,
.window-min:checked + .modal { display: none; }
/* Checkbox-hack state inputs: out of flow and invisible either way, so
   the markup needs no extra hiding (the `hidden` attribute works too). */
.window-max,
.window-min {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
/* Opt-in resizing (overflow is already non-visible above, which resize
   requires) and click-to-focus stacking for sibling windows sharing one
   overlay container. Modals carry buttons by definition, so there is
   always something to take focus; otherwise add tabindex="-1". */
.modal.is-resizable {
  resize: both;
  min-width: min(22rem, calc(100vw - 2rem));
  min-height: 12rem;
}
.modal:focus-within { z-index: var(--z-window); }

/* The dismiss ("x") button every modal header needs. Ghost by default so
   it recedes into the header; hover/focus lift it to full text color.
   Pair with aria-label="Close" (or similar) in markup — the glyph alone
   isn't accessible. */
/* .btn.is-armed — the first tap of a two-tap destructive action: a double
   ring and a danger tint; .btn-armed-label replaces the label while armed
   (put the idle text in .btn-idle-label). A bar along the bottom runs out
   over --armed-ms. Set aria-describedby to a live "Tap again to delete". */
.btn.is-armed {
  --_armed: var(--btn-armed-ring, var(--danger-text, var(--danger)));
  --btn-bg: var(--btn-armed-bg, color-mix(in srgb, var(--_armed) 18%, var(--surface)));
  --btn-fg: var(--btn-armed-fg, var(--text));
  position: relative;
  overflow: hidden;
  background: var(--btn-bg);
  color: var(--btn-fg);
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--_armed);
}
.btn-armed-label, .btn.is-armed .btn-idle-label { display: none; }
.btn.is-armed .btn-armed-label { display: inline; }
.btn.is-armed::after { content: ""; position: absolute; inset: auto 0 0; block-size: 3px; background: var(--_armed); transform-origin: 0 50%; }
@media (prefers-reduced-motion: no-preference) {
  :root:not([data-motion="reduced"], [data-motion="none"]) .btn.is-armed::after { animation: btn-armed-sweep var(--armed-ms, 2500ms) linear forwards; }
}

  :root[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) .btn.is-armed::after { animation: btn-armed-sweep var(--armed-ms, 2500ms) linear forwards; }

@keyframes btn-armed-sweep { to { transform: scaleX(0); } }
/* A toggle whose state is the server's: any .btn with aria-pressed shows it
   pressed (sunken, a ring and a lit dot, so not by colour alone; icon and
   clear buttons keep their own pressed look);
   aria-busy="true" beside it is "sent, waiting": the dot turns into a spinner. */
.btn[aria-pressed="true"]:not(.btn-icon, .btn-clear) {
  box-shadow: var(--btn-pressed-shadow, inset 0 2px 0 rgb(0 0 0 / 0.35));
  outline: 2px solid var(--btn-pressed-ring, currentColor);
  outline-offset: 2px;
}
.btn[aria-pressed]::before {
  content: "";
  flex: none;
  inline-size: 0.55em;
  block-size: 0.55em;
  border-radius: 50%;
  border: 2px solid currentColor;
  opacity: 0.55;
}
.btn[aria-pressed="true"]::before { background: currentColor; box-shadow: 0 0 0.35em currentColor; opacity: 1; }
.btn[aria-pressed][aria-busy="true"]::before { background: none; border-block-start-color: transparent; box-shadow: none; opacity: 1; }
@media (prefers-reduced-motion: no-preference) {
  :root:not([data-motion="reduced"], [data-motion="none"]) .btn[aria-pressed][aria-busy="true"]::before { animation: spinner-rotate 0.7s linear infinite; }
}

  :root[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) .btn[aria-pressed][aria-busy="true"]::before { animation: spinner-rotate 0.7s linear infinite; }

:is(.btn-clear, .btn-icon, .key, .toggle-btn)[aria-pressed]::before { content: none; }

/* .btn-lg — one big primary action; .btn-block fills its row. */
.btn.btn-lg {
  min-block-size: var(--btn-min-height-lg, max(var(--tap-min), 2.9rem));
  padding: var(--btn-padding-lg, calc(0.6em * var(--density)) calc(1.4em * var(--density)));
  font-size: var(--btn-font-size-lg, 1.05rem);
}
.btn.btn-block { display: flex; inline-size: 100%; justify-content: center; }
.btn-close,
.btn-min,
.btn-max {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Size and shape tokens: a theme whose close is a word ("Done", "Back")
     sets --btn-close-width: auto and a --btn-close-pad. */
  width: var(--btn-close-width, 1.8em);
  height: var(--btn-close-height, 1.8em);
  padding: var(--btn-close-pad, 0);
  font-size: var(--btn-close-font-size, inherit);
  white-space: nowrap;
  border: var(--btn-close-border-width, 0px) solid var(--btn-close-border, transparent);
  border-radius: var(--btn-close-radius, var(--radius));
  background: var(--btn-close-bg, transparent);
  color: var(--btn-close-fg, var(--muted));
  transition: background-color 0.15s ease, color 0.15s ease;
}
@media (hover: hover) {
.btn-close:hover,
.btn-min:hover,
.btn-max:hover {
  background: var(--btn-close-bg-hover, var(--surface-2));
  color: var(--btn-close-fg-hover, var(--text));
}
}
.btn-close:focus-visible,
.btn-min:focus-visible,
.btn-max:focus-visible {
  outline: 2px solid var(--focus, var(--accent));
  outline-offset: 2px;
}
.btn-close .icon,
.btn-min .icon,
.btn-max .icon { width: 1em; height: 1em; }
.btn-close::before,
.btn-min::before,
.btn-max::before {
  font-size: var(--btn-close-glyph-size, 1.3em);
  line-height: 1;
}
.btn-close::before { content: var(--btn-close-glyph, "\00d7"); }
/* .btn-close.is-danger: a remove/forget ×. .card-dismiss places a close
   button in the top-end corner of a .card or .panel (RTL-safe), and the
   card's header leaves room for it. */
.btn-close.is-danger { --btn-close-fg: var(--danger-text, var(--danger)); --btn-close-fg-hover: var(--on-danger, #ffffff); --btn-close-bg-hover: var(--danger); }
/* A heading in a panel header takes the header's own colour (a theme's
   heading colour can match the header band). */
.panel > .panel-header > :is(h1, h2, h3, h4, h5, h6) { margin-block: 0; color: inherit; }
:is(.card, .panel):has(> .card-dismiss) { position: relative; }
.card-dismiss { position: absolute; inset-block-start: var(--card-dismiss-inset, 0.4rem); inset-inline-end: var(--card-dismiss-inset, 0.4rem); z-index: var(--z-raised); }
:is(.card, .panel):has(> .card-dismiss) > :is(.panel-header, .card-header, h2, h3, h4):first-of-type { padding-inline-end: calc(var(--tap-min) + 0.25rem); }
/* Min/max double as <label> controls for the checkbox-hack window states
   below as well as plain <button>s — same box, either wiring. */
.btn-min::before { content: var(--btn-min-glyph, "_"); }
.btn-max::before { content: var(--btn-max-glyph, "\25a1"); }
/* If the markup puts an .icon inside instead of relying on the
   generated glyph, hide the glyph so the two don't double up. */
.btn-close:has(.icon)::before,
.btn-min:has(.icon)::before,
.btn-max:has(.icon)::before { content: none; }

.dropdown {
  background: var(--dropdown-bg, var(--surface-2));
  border: var(--dropdown-border-width, 1px) solid var(--dropdown-border, var(--border));
  border-radius: var(--dropdown-radius, var(--radius));
  box-shadow: var(--panel-shadow, none);
  padding: 0.35rem;
  min-width: 10rem;
}
.dropdown-item {
  display: block;
  width: 100%;
  padding: 0.45em 0.7em;
  border-radius: var(--dropdown-radius, var(--radius));
  color: var(--dropdown-item-fg, var(--text));
  text-align: start;
}
.dropdown-item:focus-visible {
  background: var(--dropdown-item-bg-hover, var(--accent));
  color: var(--dropdown-item-fg-hover, var(--on-accent));
}
@media (hover: hover) {
.dropdown-item:hover {
  background: var(--dropdown-item-bg-hover, var(--accent));
  color: var(--dropdown-item-fg-hover, var(--on-accent));
}
}

.toolbar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  padding: var(--toolbar-pad, var(--space-xs));
  background: var(--toolbar-bg, var(--surface));
  border-bottom: 1px solid var(--border);
}

/* --- Instrument primitives ---------------------------------------------- *
 * These exist because the themes this library was built for are control
 * surfaces — a recorder, a show-control console, a starship computer — and a
 * palette alone cannot express that. Each one is a shape every such app
 * otherwise hand-rolls, and each is a place a theme's identity actually
 * shows: a VU meter, a big numeric readout, a transport bar, a lamp.
 * All are app-agnostic: markup + tokens only, no app's class names.
 */

/* .meter — a signal-level meter (audio VU, load, any 0-100% level).
 * Level and peak are supplied as percentages on the element:
 *   <div class="meter" style="--meter-level:72%; --meter-peak:84%">
 *     <div class="meter-fill"></div><div class="meter-peak"></div>
 *   </div>
 * The band thresholds are tokens so a consuming app can drive them live
 * (e.g. from a dB scale) without the theme fighting it. */
.meter {
  position: relative;
  container-type: size;
  overflow: hidden;
  width: 100%;
  height: var(--meter-thickness, 0.6rem);
  background: var(--meter-track, var(--bg));
  border: var(--meter-border-width, 1px) solid var(--meter-border, var(--hairline));
  border-radius: var(--meter-radius, var(--radius));
}
/* .meter.is-solid: one colour (--meter-fill), no level bands: progress-like meters. */
.meter.is-solid > .meter-fill { background: var(--meter-fill, var(--accent)); }
.meter-fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: var(--meter-level, 0%);
  /* One gradient sized to the whole track, then clipped by the fill's width,
     so a given level always lands in the same colour band. */
  background-image: var(--meter-fill-image, linear-gradient(
    90deg,
    var(--meter-low, var(--success)) 0%,
    var(--meter-low, var(--success)) var(--meter-warn-at, 70%),
    var(--meter-mid, var(--warning)) var(--meter-warn-at, 70%),
    var(--meter-mid, var(--warning)) var(--meter-peak-at, 90%),
    var(--meter-high, var(--danger)) var(--meter-peak-at, 90%)
  ));
  /* cqi is the track (the meter is a size container): % here would be the
     fill's own width, squeezing every band into whatever level shows. */
  background-size: var(--meter-span, 100cqi) 100%;
  background-repeat: no-repeat;
  transition: width 0.06s linear;
}
.meter-peak {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--meter-peak, 0%);
  width: var(--meter-peak-width, 2px);
  background: var(--meter-peak-fg, var(--text));
  transform: translateX(-100%);
}
/* Vertical meters: channel strips read bottom-up. The fill and peak switch
   axis, and --meter-span is measured down the track instead of across. */
.meter-v {
  width: var(--meter-thickness, 0.6rem);
  height: 100%;
}
.meter-v .meter-fill {
  inset: auto 0 0 0;
  width: auto;
  height: var(--meter-level, 0%);
  background-image: var(--meter-fill-image-v, linear-gradient(
    0deg,
    var(--meter-low, var(--success)) 0%,
    var(--meter-low, var(--success)) var(--meter-warn-at, 70%),
    var(--meter-mid, var(--warning)) var(--meter-warn-at, 70%),
    var(--meter-mid, var(--warning)) var(--meter-peak-at, 90%),
    var(--meter-high, var(--danger)) var(--meter-peak-at, 90%)
  ));
  background-size: 100% var(--meter-span, 100cqb);
  background-position: bottom;
  transition: height 0.06s linear;
}
.meter-v .meter-peak {
  inset: auto 0 var(--meter-peak, 0%) 0;
  left: auto;
  width: auto;
  height: var(--meter-peak-width, 2px);
  transform: translateY(50%);
}

/* .readout — a large machine value: timecode, counter, channel count.
 * Tabular by construction so it never reflows as it counts. */
.readout {
  display: inline-flex;
  align-items: baseline;
  gap: 0.15em;
  padding: var(--readout-padding, 0.1em 0.35em);
  background: var(--readout-bg, transparent);
  color: var(--readout-fg, var(--accent-text, var(--accent)));
  border: var(--readout-border-width, 0) solid var(--readout-border, transparent);
  border-radius: var(--readout-radius, var(--radius));
  font-family: var(--readout-font, var(--font-mono));
  font-variant-numeric: tabular-nums;
  font-size: var(--readout-size, 1.5rem);
  font-weight: var(--readout-weight, 600);
  letter-spacing: var(--readout-tracking, 0.02em);
  line-height: 1.1;
  text-shadow: var(--readout-glow, none);
}
.readout-lg { --readout-size: 2.75rem; }
.readout-sm { --readout-size: 1rem; }
/* A trailing unit/label rides smaller and dimmer than the value. */
.readout-unit {
  font-size: 0.45em;
  font-weight: 400;
  color: var(--readout-unit-fg, var(--muted));
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* .transport — the bar carrying a surface's primary action(s): GO,
 * record, arm, fire. Deliberately its own component and not a .toolbar:
 * a transport is the one place a control surface is allowed to shout, and
 * every theme here treats it as a signature element. */
.transport {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: var(--transport-padding, 0.6rem 0.9rem);
  background: var(--transport-bg, var(--surface));
  border: var(--transport-border-width, 1px) solid var(--transport-border, var(--border));
  border-inline-start: var(--transport-edge-width, 1px) solid var(--transport-edge, var(--border));
  border-radius: var(--transport-radius, var(--radius));
  box-shadow: var(--transport-shadow, none);
}

/* Transport state modifiers: a transport codes its machine state in its
 * edge accent — record red, play green, pause amber on the reference
 * implementation. Modifiers only retarget the edge token, so themes that
 * ignore machine state keep their uniform edge for free. */
.transport.is-rec { --transport-edge: var(--transport-rec, var(--danger)); }
.transport.is-play { --transport-edge: var(--transport-play, var(--success)); }
.transport.is-pause { --transport-edge: var(--transport-pause, var(--warning)); }

/* The primary trigger. Bigger and louder than .btn-primary on purpose;
 * it is a variant of .btn, so it inherits every button mechanic. */
.btn-go {
  --btn-bg: var(--go-bg, transparent);
  --btn-fg: var(--go-fg, var(--accent));
  --btn-border: var(--go-border, var(--accent));
  --btn-border-width: var(--go-border-width, 2px);
  --btn-padding: var(--go-padding, 0.6em 1.8em);
  --btn-weight: 800;
  --btn-tracking: 0.12em;
  --btn-transform: uppercase;
  --btn-radius: var(--go-radius, var(--radius));
  --btn-shadow: var(--go-shadow, none);
  --btn-bg-hover: var(--go-bg-hover, var(--go-bg, transparent));
  --btn-shadow-hover: var(--go-shadow-hover, var(--go-shadow, none));
  font-size: var(--go-size, 1.05rem);
  /* A fixed floor, not scaled by --density: this is the one control a
     theme's own compression (a dense cue list, a cramped console) must
     never shrink below a reliably hittable target — gloved hands on a dark
     show floor don't get a second chance at a missed trigger. */
  min-width: var(--go-min-target, 44px);
  min-height: var(--go-min-target, 44px);
}

/* .lamp — a hardware indicator light. Distinct from .status, which
 * is a labelled text state: a lamp is a lit or unlit fixture, and reads as
 * off (dim, present) rather than absent. */
.lamp {
  display: inline-block;
  width: var(--lamp-size, 0.7rem);
  height: var(--lamp-size, 0.7rem);
  flex-shrink: 0;
  border-radius: var(--lamp-radius, 50%);
  border: var(--lamp-border-width, 1px) solid var(--lamp-border, var(--hairline));
  background: var(--lamp-off, var(--surface-2));
  box-shadow: none;
}
.lamp.is-on {
  background: var(--lamp-on, var(--success));
  border-color: var(--lamp-on, var(--success));
  box-shadow: var(--lamp-glow, 0 0 0.4rem var(--lamp-on, var(--success)));
}
.lamp.is-warn {
  background: var(--warning);
  border-color: var(--warning);
  box-shadow: var(--lamp-glow, 0 0 0.4rem var(--warning));
}
.lamp.is-error {
  background: var(--danger);
  border-color: var(--danger);
  box-shadow: var(--lamp-glow, 0 0 0.4rem var(--danger));
}
/* "Happening now", as distinct from "happened and succeeded" (.is-on). The
   pulse only runs for users who haven't asked the OS to reduce motion; for
   everyone else the lamp is simply lit in the accent. */
.lamp:is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))) {
  background: var(--lamp-active, var(--accent));
  border-color: var(--lamp-active, var(--accent));
  box-shadow: var(--lamp-glow, 0 0 0.4rem var(--lamp-active, var(--accent)));
}
@media (prefers-reduced-motion: no-preference) {
  .lamp:is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))) { animation: lamp-pulse var(--lamp-pulse-duration, 1.2s) ease-in-out infinite; }
}

  :root[data-motion="always"] .lamp:is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))) { animation: lamp-pulse var(--lamp-pulse-duration, 1.2s) ease-in-out infinite; }

@keyframes lamp-pulse { 50% { opacity: 0.4; } }

/* --- Mixing-console primitives ------------------------------------------ *
 * The shapes every mixing surface shares — Yamaha CL/StageMix, Midas
 * Heritage 3000 / HD96, SSL 4000 — so an audio app doesn't hand-roll them
 * and a theme can restyle them: a channel strip in a scrolling bay, rotary
 * knobs with band-coloured caps, a long-throw fader with a printed dB
 * scale, an LED-ladder meter, a gain-reduction meter, backlit keys, a
 * scribble strip and an EQ-curve thumbnail. Markup + tokens only; the one
 * thing CSS cannot do (read an input's value into a rotation) is
 * assets/js/controls.js, eight lines an app can replace with its own. */

/* .mixer — the console bay: channel strips side by side, scrolling
   sideways like a fader bank rather than wrapping. */
.mixer {
  display: flex;
  align-items: stretch;
  gap: var(--mixer-gap, 2px);
  overflow-x: auto;
  padding: var(--mixer-padding, 0.35rem);
  background: var(--mixer-bg, var(--bg));
  border: var(--mixer-border-width, 1px) solid var(--mixer-border, var(--border));
  border-radius: var(--mixer-radius, var(--radius));
}
/* .strip — one channel, signal flow top to bottom: input, EQ, dynamics,
   pan, keys, fader + meter, scribble strip. */
.strip {
  flex: 0 0 var(--strip-width, 5.5rem);
  display: flex;
  flex-direction: column;
  gap: calc(0.35rem * var(--density));
  min-width: 0;
  padding: calc(0.4rem * var(--density)) 0.3rem;
  background: var(--strip-bg, var(--surface));
  border: 1px solid var(--strip-border, var(--hairline));
  border-radius: var(--strip-radius, calc(var(--radius) * 0.5));
}
.strip.is-master { flex-basis: var(--strip-width-master, 7.5rem); }
.strip.is-selected { border-color: var(--strip-selected, var(--accent)); box-shadow: inset 0 0 0 1px var(--strip-selected, var(--accent)); }
.strip-section {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 0.3rem;
  padding-bottom: calc(0.35rem * var(--density));
  border-bottom: 1px solid var(--strip-rule, var(--hairline));
}
.strip-label {
  flex-basis: 100%;
  text-align: center;
  font-size: 0.58rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--strip-label-fg, var(--muted));
}
/* Fader area: printed scale | fader | meter(s), all the same height. */
.strip-fader {
  display: flex;
  justify-content: center;
  align-items: stretch;
  gap: 0.25rem;
  height: var(--fader-length, 14rem);
  margin-top: auto;
}
.strip-fader .meter-v { height: auto; }

/* .knob — a rotary control. A real range input sits invisibly over the
   dial (drag, arrow keys, screen readers all work); --value (0-1) turns
   the pointer through a 270-degree sweep and fills the value arc.
     <label class="knob knob-hf" style="--value:.62">
       <input type="range" min="-15" max="15" value="3" aria-label="HF gain">
       <span class="knob-dial"></span><span class="knob-label">HF</span>
     </label>
   .is-bipolar fills the arc from 12 o'clock (pan, EQ gain: centre
   detent). Cap colours follow the console convention SSL made standard —
   HF red, HMF green, LMF blue, LF brown — as tokens a theme can repaint. */
.knob {
  position: relative;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 0.15rem;
  --knob-angle: calc(-135deg + var(--value, 0.5) * 270deg);
}
.knob input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: var(--cursor-resize-y, ns-resize);
  /* controls.js turns knobs by vertical drag; this stops a swipe that
     starts on a knob from scrolling the page instead. */
  touch-action: none;
}
/* Values that drag-adjust a control (controls.js): the knob's own label
   and any [data-for] readout. */
:is([data-for], [data-fader-readout]) { cursor: var(--cursor-resize-y, ns-resize); touch-action: none; user-select: none; }
html.is-dragging-value, html.is-dragging-value * { cursor: var(--cursor-resize-y, ns-resize) !important; user-select: none; }
.knob-dial {
  position: relative;
  width: var(--knob-size, calc(2.1rem * var(--density)));
  height: var(--knob-size, calc(2.1rem * var(--density)));
  border-radius: 50%;
}
.knob-sm { --knob-size: calc(1.6rem * var(--density)); }
.knob-lg { --knob-size: calc(3rem * var(--density)); }
/* Value arc: a ring around the cap, track first, filled sweep on top. */
.knob-dial::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background:
    conic-gradient(from 225deg, var(--knob-arc, var(--accent)) 0 calc(var(--value, 0.5) * 270deg), transparent 0),
    conic-gradient(from 225deg, var(--knob-track, var(--surface-2)) 0 270deg, transparent 0);
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - var(--knob-arc-width, 3px)), #000 calc(100% - var(--knob-arc-width, 3px) + 0.5px));
  mask: radial-gradient(farthest-side, transparent calc(100% - var(--knob-arc-width, 3px)), #000 calc(100% - var(--knob-arc-width, 3px) + 0.5px));
}
.knob.is-bipolar .knob-dial::before {
  background:
    conic-gradient(from min(0deg, calc((var(--value, 0.5) - 0.5) * 270deg)), var(--knob-arc, var(--accent)) 0 max(calc((var(--value, 0.5) - 0.5) * 270deg), calc((0.5 - var(--value, 0.5)) * 270deg)), transparent 0),
    conic-gradient(from 225deg, var(--knob-track, var(--surface-2)) 0 270deg, transparent 0);
}
/* The cap and its pointer line, turned as one. */
.knob-dial::after {
  content: "";
  position: absolute;
  inset: calc(var(--knob-arc-width, 3px) + 2px);
  border-radius: 50%;
  background:
    linear-gradient(var(--knob-pointer, var(--text)), var(--knob-pointer, var(--text))) 50% 8% / 2px 38% no-repeat,
    radial-gradient(circle, color-mix(in srgb, var(--knob-color, var(--surface-2)) 80%, #fff) 0, var(--knob-color, var(--surface-2)) 55%, color-mix(in srgb, var(--knob-color, var(--surface-2)) 70%, #000) 100%);
  border: 1px solid var(--knob-border, var(--border));
  box-shadow: var(--knob-shadow, 0 1px 2px rgba(0, 0, 0, 0.45));
  transform: rotate(var(--knob-angle));
}
.knob:has(input:focus-visible) .knob-dial { outline: 2px solid var(--focus); outline-offset: 2px; }
.knob-label {
  font-size: 0.58rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--knob-label-fg, var(--muted));
  line-height: 1;
}
.knob-hf { --knob-color: var(--knob-hf, #c4302b); --knob-pointer: var(--knob-band-pointer, #ffffff); }
.knob-hmf { --knob-color: var(--knob-hmf, #2e8b45); --knob-pointer: var(--knob-band-pointer, #ffffff); }
.knob-lmf { --knob-color: var(--knob-lmf, #2d5cb0); --knob-pointer: var(--knob-band-pointer, #ffffff); }
.knob-lf { --knob-color: var(--knob-lf, #6b4a2e); --knob-pointer: var(--knob-band-pointer, #ffffff); }

/* .fader — the long-throw console fader: a native vertical range input
   drawn as a groove with a ridged cap. The app owns the fader law (which
   position is which dB); .scale prints the legend beside it. */
.fader {
  -webkit-appearance: none;
  appearance: none;
  writing-mode: vertical-lr;
  direction: rtl;
  width: var(--fader-cap-width, 2.2rem);
  height: 100%;
  min-height: 6rem;
  margin: 0;
  /* --fader-track replaces the whole groove (a theme can draw a tube, a
     slot, anything); --value (0-1, kept by assets/js/controls.js) is
     there for a track that shows how far the fader is up. */
  background: var(--fader-track, linear-gradient(90deg,
    transparent calc(50% - var(--fader-slot-width, 4px) / 2),
    var(--fader-slot, var(--bg)) 0 calc(50% + var(--fader-slot-width, 4px) / 2),
    transparent 0));
  border-radius: 2px;
  cursor: var(--cursor-resize-y, ns-resize);
}
.fader::-webkit-slider-runnable-track { background: transparent; }
.fader::-moz-range-track { background: transparent; }
.fader::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: var(--fader-cap-width, 2.2rem);
  height: var(--fader-cap-height, 1.2rem);
  border: 1px solid var(--fader-cap-border, var(--border));
  border-radius: var(--fader-cap-radius, 2px);
  background: var(--fader-cap-bg,
    linear-gradient(var(--fader-cap-line, var(--text)), var(--fader-cap-line, var(--text))) 50% 50% / 100% 2px no-repeat,
    repeating-linear-gradient(0deg, transparent 0 2px, rgba(0, 0, 0, 0.18) 2px 3px),
    linear-gradient(180deg, color-mix(in srgb, var(--fader-cap, var(--muted)) 70%, #fff), var(--fader-cap, var(--muted)) 50%, color-mix(in srgb, var(--fader-cap, var(--muted)) 70%, #000)));
  box-shadow: var(--fader-cap-shadow, 0 3px 5px rgba(0, 0, 0, 0.5));
}
.fader::-moz-range-thumb {
  width: var(--fader-cap-width, 2.2rem);
  height: var(--fader-cap-height, 1.2rem);
  border: 1px solid var(--fader-cap-border, var(--border));
  border-radius: var(--fader-cap-radius, 2px);
  background: var(--fader-cap-bg,
    linear-gradient(var(--fader-cap-line, var(--text)), var(--fader-cap-line, var(--text))) 50% 50% / 100% 2px no-repeat,
    repeating-linear-gradient(0deg, transparent 0 2px, rgba(0, 0, 0, 0.18) 2px 3px),
    linear-gradient(180deg, color-mix(in srgb, var(--fader-cap, var(--muted)) 70%, #fff), var(--fader-cap, var(--muted)) 50%, color-mix(in srgb, var(--fader-cap, var(--muted)) 70%, #000)));
  box-shadow: var(--fader-cap-shadow, 0 3px 5px rgba(0, 0, 0, 0.5));
}
.fader:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

/* .scale — a printed legend beside a fader or meter. Each mark sits at a
   0-1 position (--at) along the travel; the inset keeps the ends aligned
   with a fader cap's centre at the top and bottom of its throw.
     <div class="scale"><span style="--at:.8" class="is-unity">0</span>…</div> */
.scale {
  position: relative;
  width: var(--scale-width, 1.5rem);
  flex: none;
  font-size: 0.55rem;
  font-variant-numeric: tabular-nums;
  color: var(--scale-fg, var(--muted));
  --scale-inset: calc(var(--fader-cap-height, 1.2rem) / 2);
}
.scale.is-meter { --scale-inset: 0px; }
.scale > span {
  position: absolute;
  right: 0.3rem;
  bottom: calc(var(--scale-inset) + var(--at, 0) * (100% - 2 * var(--scale-inset)));
  transform: translateY(50%);
  line-height: 1;
  white-space: nowrap;
}
.scale > span::after {
  content: "";
  position: absolute;
  left: calc(100% + 0.1rem);
  top: 50%;
  width: 0.2rem;
  height: 1px;
  background: currentColor;
}
.scale > .is-unity { color: var(--scale-unity-fg, var(--text)); font-weight: 700; }

/* .meter.is-segmented — the LED ladder: the same level/peak contract as
   .meter, cut into segments, with the unlit segments faintly visible the
   way unlit LEDs are. */
.meter.is-segmented::before,
.meter.is-segmented::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.meter.is-segmented::before {
  opacity: var(--meter-unlit, 0.18);
  background-image: var(--meter-fill-image, linear-gradient(90deg,
    var(--meter-low, var(--success)) 0%, var(--meter-low, var(--success)) var(--meter-warn-at, 70%),
    var(--meter-mid, var(--warning)) var(--meter-warn-at, 70%), var(--meter-mid, var(--warning)) var(--meter-peak-at, 90%),
    var(--meter-high, var(--danger)) var(--meter-peak-at, 90%)));
}
.meter-v.is-segmented::before {
  background-image: var(--meter-fill-image-v, linear-gradient(0deg,
    var(--meter-low, var(--success)) 0%, var(--meter-low, var(--success)) var(--meter-warn-at, 70%),
    var(--meter-mid, var(--warning)) var(--meter-warn-at, 70%), var(--meter-mid, var(--warning)) var(--meter-peak-at, 90%),
    var(--meter-high, var(--danger)) var(--meter-peak-at, 90%)));
}
.meter.is-segmented::after {
  background: repeating-linear-gradient(90deg, transparent 0 var(--meter-seg, 3px), var(--meter-gap, var(--meter-track, var(--bg))) 0 calc(var(--meter-seg, 3px) + var(--meter-seg-gap, 1px)));
}
.meter-v.is-segmented::after {
  background: repeating-linear-gradient(0deg, transparent 0 var(--meter-seg, 3px), var(--meter-gap, var(--meter-track, var(--bg))) 0 calc(var(--meter-seg, 3px) + var(--meter-seg-gap, 1px)));
}
/* .meter.is-gr — gain reduction: fills from the top (or the right) down
   as the compressor pulls level, in its own single colour. */
.meter.is-gr .meter-fill {
  inset: 0 0 0 auto;
  background: var(--meter-gr, var(--warning));
}
.meter-v.is-gr .meter-fill { inset: 0 0 auto 0; }
.meter.is-gr.is-segmented::before { background: var(--meter-gr, var(--warning)); }

/* .key — a backlit console key: dark when off, lit in its function's
   colour when on. Console conventions, as tokens: ON green (Yamaha),
   MUTE red, SOLO/CUE yellow, SEL the accent.
     <label class="key key-mute"><input type="checkbox">Mute</label>
     <button class="key key-sel" aria-pressed="true">Sel</button> */
.key {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: max(var(--key-width, 2.3rem), var(--tap-min));
  height: calc(1.55rem * var(--density));
  padding: 0 0.45em;
  font: inherit;
  font-size: 0.64rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1;
  background: var(--key-bg, var(--surface-2));
  color: var(--key-fg, var(--muted));
  border: 1px solid var(--key-border, var(--border));
  border-radius: var(--key-radius, 3px);
  box-shadow: var(--key-shadow, inset 0 -2px 0 rgba(0, 0, 0, 0.25));
  user-select: none;
}
.key input {
  position: absolute;
  inset: 0;
  margin: 0;
  opacity: 0;
}
.key:has(input:checked),
.key[aria-pressed="true"],
.key.is-on {
  background: var(--key-lit, var(--accent));
  color: var(--key-lit-fg, var(--on-accent));
  border-color: var(--key-lit, var(--accent));
  box-shadow: var(--key-glow, 0 0 0.55rem color-mix(in srgb, var(--key-lit, var(--accent)) 65%, transparent));
}
.key:has(input:focus-visible),
.key:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.key-on { --key-lit: var(--key-on, var(--success)); --key-lit-fg: var(--key-on-fg, var(--on-success)); }
.key-mute { --key-lit: var(--key-mute, var(--danger)); --key-lit-fg: var(--key-mute-fg, var(--on-danger)); }
.key-solo { --key-lit: var(--key-solo, var(--warning)); --key-lit-fg: var(--key-solo-fg, #000000); }
.key-sel { --key-lit: var(--key-sel, var(--accent)); --key-lit-fg: var(--key-sel-fg, var(--on-accent)); }

/* .scribble — the scribble strip: channel number and name under a bar of
   the channel's colour (--strip-color, set per channel by the app). */
.scribble {
  display: flex;
  flex-direction: column;
  min-width: 0;
  padding: 0.2rem 0.35rem;
  background: var(--scribble-bg, var(--surface-2));
  color: var(--scribble-fg, var(--text));
  border-top: var(--scribble-bar-width, 0.3rem) solid var(--strip-color, var(--accent));
  border-radius: var(--scribble-radius, 3px);
  line-height: 1.15;
}
.scribble-num { font-size: 0.58rem; color: var(--scribble-num-fg, var(--muted)); font-variant-numeric: tabular-nums; }
.scribble-name {
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* .eq-curve — the EQ thumbnail StageMix and HD96 show on every strip: a
   frequency grid with the 0 dB line, and the app's own SVG curve on it.
     <div class="eq-curve"><svg viewBox="0 0 100 50" preserveAspectRatio="none"><path d="…"/></svg></div> */
.eq-curve {
  position: relative;
  width: 100%;
  aspect-ratio: var(--eq-aspect, 2 / 1);
  background:
    linear-gradient(0deg, transparent calc(50% - 0.5px), var(--eq-zero, var(--border)) 0 calc(50% + 0.5px), transparent 0),
    repeating-linear-gradient(90deg, transparent 0 calc(33.333% - 1px), var(--eq-grid, var(--hairline)) 0 33.333%),
    var(--eq-bg, var(--bg));
  border: 1px solid var(--eq-border, var(--hairline));
  border-radius: var(--eq-radius, 3px);
  overflow: hidden;
}
.eq-curve svg { position: absolute; inset: 0; width: 100%; height: 100%; }
.eq-curve path {
  fill: var(--eq-fill, color-mix(in srgb, var(--eq-line, var(--accent)) 22%, transparent));
  stroke: var(--eq-line, var(--accent));
  stroke-width: var(--eq-line-width, 1.5px);
  vector-effect: non-scaling-stroke;
}
.eq-curve.is-off path { fill: none; stroke: var(--muted); }

/* .segmented — a segmented control / toggle group: mutually-exclusive
   view switches (list/grid, day/week/month). Distinct from .tabs
   (navigates between different content, usually with a URL) and
   .badge-button (independent, multi-select filter chips). Markup is a
   plain button group; state lives in `.is-active`, same convention as
   every other stateful component here. */
.segmented {
  display: inline-flex;
  padding: var(--segmented-padding, 0.2rem);
  background: var(--segmented-bg, var(--surface-2));
  border: var(--segmented-border-width, 1px) solid var(--segmented-border, var(--border));
  border-radius: var(--segmented-radius, var(--radius));
  gap: 0.15rem;
}
.segmented-item {
  padding: 0.35em 0.9em;
  border: none;
  border-radius: var(--segmented-item-radius, calc(var(--segmented-radius, var(--radius)) * 0.75));
  background: transparent;
  color: var(--segmented-item-fg, var(--muted));
  font: inherit;
  font-size: 0.85em;
  transition: background 0.15s ease, color 0.15s ease;
}
@media (hover: hover) {
.segmented-item:hover { color: var(--segmented-item-fg-hover, var(--text)); }
}
.segmented-item:is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))) {
  background: var(--segmented-item-bg-active, var(--accent));
  color: var(--segmented-item-fg-active, var(--on-accent));
}

/* --- .table -------------------------------------------------------- */
.table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.88em;
}
.table th {
  text-align: start;
  padding: calc(0.5em * var(--density)) calc(0.75em * var(--density));
  background: var(--table-head-bg, transparent);
  color: var(--table-head-fg, var(--muted));
  border-bottom: var(--table-head-rule-width, 1px) solid var(--table-head-rule, var(--border));
  text-transform: var(--table-head-transform, uppercase);
  letter-spacing: var(--table-head-tracking, 0.06em);
  font-size: 0.78em;
}
.table td {
  padding: calc(0.5em * var(--density)) calc(0.75em * var(--density));
  border-bottom: 1px solid var(--table-rule, var(--hairline));
}
/* Row states carry BOTH a background and a left marker, so a theme that
   flattens one language still communicates the state through the other. */
.table tr:is(.is-selected, :is([aria-selected="true"], :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked)))) td {
  background: var(--row-selected-bg, rgba(128, 128, 128, 0.25));
  color: var(--row-selected-fg, inherit);
}
.table tr:is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))) td {
  background: var(--row-active-bg, transparent);
}
/* The marker belongs to the row, so only its leading cell draws it — on
   every cell it read as a column divider repeated across the row. */
.table tr:is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))) td:first-child {
  box-shadow: var(--row-active-marker, inset 0.25rem 0 0 var(--accent));
}
/* A theme with a solid selection fill declares --row-selected-fg for
   it; state-colored text in that row has to follow it too, or a green
   "Connected" lands on a blue highlight at ~1:1. Themes with a translucent
   highlight leave the token unset and keep the state color. */
.table tr:is(.is-selected, :is([aria-selected="true"], :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked)))) .status,
.table tr:is(.is-selected, :is([aria-selected="true"], :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked)))) .stat-trend {
  color: var(--row-selected-fg, var(--state-fg));
}
@supports (color: color-mix(in srgb, red 50%, transparent)) {
  .table tr:is(.is-selected, :is([aria-selected="true"], :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked)))) td {
    background: var(--row-selected-bg, color-mix(in srgb, var(--accent) 18%, transparent));
  }
}

/* Row hover/focus: the pointer's row, or the row holding keyboard focus. */
.table tbody tr:focus-within td {
  background: var(--row-hover-bg, rgba(128, 128, 128, 0.12));
}
@media (hover: hover) {
.table tbody tr:hover td {
  background: var(--row-hover-bg, rgba(128, 128, 128, 0.12));
}
}
@supports (color: color-mix(in srgb, red 50%, transparent)) {
  .table tbody tr:hover td,
  .table tbody tr:focus-within td {
    background: var(--row-hover-bg, color-mix(in srgb, var(--accent) 8%, transparent));
  }
}
/* Selected rows keep their own fill under the pointer. */
.table tbody tr:is(.is-selected, :is([aria-selected="true"], :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked)))):focus-within td {
  background: var(--row-selected-bg, color-mix(in srgb, var(--accent) 18%, transparent));
}
@media (hover: hover) {
.table tbody tr.is-selected:hover td {
  background: var(--row-selected-bg, color-mix(in srgb, var(--accent) 18%, transparent));
}
}

/* Sticky header: opt in per-table with `.table.is-sticky` — a plain
   `<thead>` needs no extra markup, just its own opaque background so
   scrolled rows don't show through. */
.table.is-sticky thead th {
  position: sticky;
  top: 0;
  background: var(--table-head-bg, var(--surface));
  z-index: var(--z-sticky);
}
/* Each sticky cell is its own stacking context, so a column-filter
   dropdown/popover inside one would sit under its neighbours. Lift the
   cell while it holds an open surface. Two rules: a selector the engine
   doesn't know would drop the whole list. */
.table.is-sticky thead th:has(details[open]) { z-index: calc(var(--z-sticky) + 1); }
.table.is-sticky thead th:has(:popover-open) { z-index: calc(var(--z-sticky) + 1); }

/* Zebra striping: opt in with `.table.is-striped`. A dedicated token
   rather than reaching for --surface-2 directly — a theme with a
   translucent surface-2 (glass/gloss themes) can band wrong stacked over
   a table row, so it gets its own knob instead of inheriting one shared
   with card/panel surfaces. */
.table.is-striped tbody tr:nth-child(even) td {
  background: var(--row-alt-bg, var(--surface-2));
}
.table.is-striped tbody tr:nth-child(even):focus-within td {
  background: var(--row-hover-bg, color-mix(in srgb, var(--accent) 8%, transparent));
}
@media (hover: hover) {
.table.is-striped tbody tr:nth-child(even):hover td {
  background: var(--row-hover-bg, color-mix(in srgb, var(--accent) 8%, transparent));
}
}
.table.is-striped tbody tr.is-selected:nth-child(even) td {
  background: var(--row-selected-bg, var(--row-alt-bg, var(--surface-2)));
}
.table.is-striped tbody tr:is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))):nth-child(even) td {
  background: var(--row-active-bg, var(--row-alt-bg, var(--surface-2)));
}

/* .table-blank-rows — continues the zebra stripe below the last row so a
   short table in a tall pane still reads as a ruled sheet. A flex child
   placed after the table in a column container:
     <div class="pane"><table class="table is-striped">…</table>
     <div class="table-blank-rows"></div></div>
   --stripe-height is the row pitch; --stripe-a/--stripe-b override tones. */
.table-blank-rows {
  flex: 1 1 auto;
  min-height: 0;
  border-top: 1px solid var(--table-rule, var(--hairline));
  background-image: repeating-linear-gradient(
    to bottom,
    var(--stripe-a, transparent) 0,
    var(--stripe-a, transparent) var(--stripe-height, 22px),
    var(--stripe-b, var(--stripe-b-default, var(--row-alt-bg, var(--surface-2)))) var(--stripe-height, 22px),
    var(--stripe-b, var(--stripe-b-default, var(--row-alt-bg, var(--surface-2)))) calc(2 * var(--stripe-height, 22px))
  );
}
@supports (color: color-mix(in srgb, red 50%, transparent)) {
  /* Fainter than a data row, so blank rows read as placeholders. */
  .table-blank-rows { --stripe-b-default: color-mix(in srgb, var(--row-alt-bg, var(--surface-2)) 70%, var(--surface)); }
}

/* Sort indicator: the app sets `aria-sort="ascending"/"descending"` on the
   active `<th>` (a native a11y attribute, not an .is-* class, since it's
   already the correct semantic marker) and a clickable header gets a
   cursor and hover tint for free. */
@media (hover: hover) {
.table th[aria-sort]:hover { color: var(--table-head-fg-hover, var(--text)); }
}
.table th[aria-sort]::after {
  margin-inline-start: 0.35em;
  font-size: 0.85em;
  color: var(--table-sort-fg, var(--accent));
}
.table th[aria-sort="ascending"]::after { content: "\2191"; }
.table th[aria-sort="descending"]::after { content: "\2193"; }

/* --- Navigation and tabs ----------------------------------------------- */
.nav {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: var(--nav-pad, 0.6rem var(--space-m));
  background: var(--nav-bg, var(--surface));
  border-bottom: var(--nav-rule-width, 1px) solid var(--nav-rule, var(--border));
  border-radius: var(--nav-radius, 0);
}
.nav-brand {
  font-weight: 700;
  color: var(--nav-brand-fg, var(--accent));
}
.nav-item {
  padding: 0.4em 0.7em;
  border-radius: var(--nav-item-radius, var(--radius));
  background: var(--nav-item-bg, transparent);
  color: var(--nav-item-fg, var(--muted));
}
:is(.nav-item:is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))), .nav-item[aria-current="page"]) {
  background: var(--nav-item-bg-active, var(--surface-2));
  color: var(--nav-item-fg-active, var(--text));
}
@media (hover: hover) {
.nav-item:hover {
  background: var(--nav-item-bg-active, var(--surface-2));
  color: var(--nav-item-fg-active, var(--text));
}
}

/* .tabs.is-sticky stays at the top while the page scrolls (an opaque background). */
.tabs.is-sticky { position: sticky; inset-block-start: var(--sticky-top, 0); z-index: var(--z-sticky); background: var(--tabs-sticky-bg, var(--bg)); }
.tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  border-bottom: 1px solid var(--border);
}
/* Touch tiers: tabs scroll sideways in one row instead of wrapping onto a
   second line (a wrapped tab row reads as two rows of tabs). Inside the
   scroller the focus ring is drawn inset so it isn't clipped. */
@media (max-width: 900px) {
  .tabs:not([aria-orientation="vertical"]) {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
  }
  .tabs:not([aria-orientation="vertical"]) > .tab { flex-shrink: 0; }
  .tabs:not([aria-orientation="vertical"]) > .tab:focus-visible { outline-offset: -2px; }
}
.tab {
  padding: 0.4em 0.9em;
  border: 1px solid transparent;
  border-radius: var(--tab-radius, var(--radius) var(--radius) 0 0);
  background: var(--tab-bg, transparent);
  color: var(--tab-fg, var(--muted));
  font-size: 0.85em;
  letter-spacing: var(--label-tracking, normal);
  text-transform: var(--label-transform, none);
}
@media (hover: hover) {
.tab:hover { color: var(--tab-fg-hover, var(--text)); background: var(--tab-bg-hover, var(--tab-bg, transparent)); border-color: var(--tab-border-hover, transparent); }
}
.tab:is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))) {
  background: var(--tab-bg-active, transparent);
  color: var(--tab-fg-active, var(--accent));
  border-color: var(--tab-border-active, transparent);
  border-bottom: 2px solid var(--tab-underline-active, var(--accent));
}

/* `.tabs[aria-orientation="vertical"]` — the left-hand tab rail a
   settings sheet reaches for: same token vocabulary, but the selection
   marker moves from the block edge (bottom underline) to the inline edge,
   labels lead-align, and the rail gets an inline-end hairline instead of a
   bottom rule so the panes beside it read as belonging to the rail. */
.tabs[aria-orientation="vertical"] {
  flex-direction: column;
  align-items: stretch;
  flex-wrap: nowrap;
  gap: 0.15rem;
  padding-inline-end: 0.25rem;
  border-bottom: 0;
  border-inline-end: 1px solid var(--border);
}
.tabs[aria-orientation="vertical"] > .tab {
  display: block;
  width: 100%;
  text-align: start;
  border-bottom-color: transparent;
  border-radius: var(--tab-radius, var(--radius));
}
.tabs[aria-orientation="vertical"] > .tab:is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))) {
  border-color: var(--tab-border-active, transparent);
  border-inline-end: 2px solid var(--tab-underline-active, var(--accent));
}

/* --- Badges, progress, status ----------------------------------------- */
.badge {
  display: inline-flex;
  align-items: center;
  padding: 0.15em 0.6em;
  border: 1px solid var(--badge-border, transparent);
  border-radius: var(--badge-radius, 999px);
  background: var(--badge-bg, var(--surface-2));
  color: var(--badge-fg, var(--muted));
  font-size: 0.75em;
  font-weight: 600;
}
.badge-accent { --badge-bg: var(--surface-2); --badge-fg: var(--accent-text, var(--accent)); }
.badge-danger { --badge-bg: var(--surface-2); --badge-fg: var(--danger-text, var(--danger)); }
.badge-success { --badge-bg: var(--surface-2); --badge-fg: var(--success-text, var(--success)); }
.badge-warning { --badge-bg: var(--surface-2); --badge-fg: var(--warning-text, var(--warning)); }
/* .badge-solid: a filled chip that reads at distance; .badge-outline: a ring. */
.badge.badge-solid { --badge-bg: var(--accent); --badge-fg: var(--on-accent); }
.badge.badge-solid.badge-danger { --badge-bg: var(--danger); --badge-fg: var(--on-danger, #ffffff); }
.badge.badge-solid.badge-warning { --badge-bg: var(--warning); --badge-fg: var(--on-warning, #000000); }
.badge.badge-solid.badge-success { --badge-bg: var(--success); --badge-fg: var(--on-success, #000000); }
.badge.badge-outline { --badge-bg: transparent; --badge-border: currentColor; }
/* The tint is mixed into the opaque surface, not into transparent: a
   translucent badge inside a tinted row stacked on the row color, so its
   text contrast depended on where the badge happened to sit. */
@supports (color: color-mix(in srgb, red 50%, transparent)) {
  .badge-accent { --badge-bg: color-mix(in srgb, var(--accent) 20%, var(--surface)); }
  .badge-danger { --badge-bg: color-mix(in srgb, var(--danger) 20%, var(--surface)); }
  .badge-success { --badge-bg: color-mix(in srgb, var(--success) 20%, var(--surface)); }
  .badge-warning { --badge-bg: color-mix(in srgb, var(--warning) 20%, var(--surface)); }
}

.progress {
  height: var(--progress-height, 0.5rem);
  border-radius: var(--progress-radius, var(--radius));
  background: var(--progress-bg, var(--surface-2));
  border: var(--progress-border-width, 1px) solid var(--progress-border, var(--border));
  overflow: hidden;
}
.progress-bar {
  height: 100%;
  background: var(--progress-fill, var(--accent));
}
/* .progress-stack — several segments in one bar (disk usage, a budget).
   Each .progress-bar takes its share as --value (a percentage) and a
   chart series colour in turn, or data-color="1..6". Label each segment
   (aria-label + aria-valuenow) or give the stack a legend.
     <div class="progress progress-stack" role="group" aria-label="Storage">
       <div class="progress-bar" role="progressbar" aria-label="Photos" aria-valuenow="40" style="--value: 40%"></div>
       <div class="progress-bar" role="progressbar" aria-label="Apps" aria-valuenow="25" style="--value: 25%"></div>
     </div> */
.progress-stack { display: flex; }
.progress-stack > .progress-bar {
  flex: 0 0 var(--value, 0%);
  background: var(--user-color, var(--_seg, var(--progress-fill, var(--accent))));
}
.progress-stack > .progress-bar + .progress-bar { box-shadow: inset 1px 0 0 var(--progress-bg, var(--surface-2)); }
.progress-stack > :nth-child(6n + 1) { --_seg: var(--chart-series-1); }
.progress-stack > :nth-child(6n + 2) { --_seg: var(--chart-series-2); }
.progress-stack > :nth-child(6n + 3) { --_seg: var(--chart-series-3); }
.progress-stack > :nth-child(6n + 4) { --_seg: var(--chart-series-4); }
.progress-stack > :nth-child(6n + 5) { --_seg: var(--chart-series-5); }
.progress-stack > :nth-child(6n + 6) { --_seg: var(--chart-series-6); }

/* Native <progress class="progress" value="60" max="100">: the value
   lives in attributes, so no inline style is needed. */
progress.progress {
  -webkit-appearance: none;
  appearance: none;
  display: block;
  width: 100%;
  color: var(--progress-fill, var(--accent));
}
progress.progress::-webkit-progress-bar { background: var(--progress-bg, var(--surface-2)); }
progress.progress::-webkit-progress-value { background: var(--progress-fill, var(--accent)); }
progress.progress::-moz-progress-bar { background: var(--progress-fill, var(--accent)); }

/* Native <meter class="meter" min max low high optimum value>: the
   browser picks the band from the attributes; each band gets the theme's
   meter colors. The div-based .meter keeps working unchanged. */
meter.meter {
  -webkit-appearance: none;
  appearance: none;
  display: block;
  background: var(--meter-track, var(--bg));
}
meter.meter::-webkit-meter-bar {
  background: var(--meter-track, var(--bg));
  border: 0;
  border-radius: var(--meter-radius, var(--radius));
  height: 100%;
}
meter.meter::-webkit-meter-optimum-value { background: var(--meter-low, var(--success)); }
meter.meter::-webkit-meter-suboptimum-value { background: var(--meter-mid, var(--warning)); }
meter.meter::-webkit-meter-even-less-good-value { background: var(--meter-high, var(--danger)); }
meter.meter:-moz-meter-optimum::-moz-meter-bar { background: var(--meter-low, var(--success)); }
meter.meter:-moz-meter-sub-optimum::-moz-meter-bar { background: var(--meter-mid, var(--warning)); }
meter.meter:-moz-meter-sub-sub-optimum::-moz-meter-bar { background: var(--meter-high, var(--danger)); }

.status {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  font-size: 0.85em;
}
.status::before {
  content: "";
  width: 0.5em;
  height: 0.5em;
  border-radius: var(--status-dot-radius, 50%);
  background: currentColor;
}
/* Semantic colors used AS TEXT read --<state>-text, falling back to the
   fill color. A fill that works for a lamp or a button background (bright
   yellow, mint, pale pink) is often unreadable as small text on the theme's
   own surface, so a theme sets a text-weight variant where they differ.
   The color travels in --state-fg so a selected table row can swap it
   for --row-selected-fg (see .table) without losing it elsewhere. */
.status-ok { --state-fg: var(--success-text, var(--success)); color: var(--state-fg); }
.status-warn { --state-fg: var(--warning-text, var(--warning)); color: var(--state-fg); }
.status-error { --state-fg: var(--danger-text, var(--danger)); color: var(--state-fg); }
.status-idle { --state-fg: var(--muted); color: var(--state-fg); }
/* .status-rec: recording or on air. A bold danger label whose dot has a
   halo; the dot pulses when motion is allowed. */
.status-rec { --state-fg: var(--danger-text, var(--danger)); color: var(--state-fg); font-weight: 700; }
.status-rec::before { box-shadow: 0 0 0 0.15em color-mix(in srgb, currentColor 35%, transparent); }
@media (prefers-reduced-motion: no-preference) {
  :root:not([data-motion="reduced"], [data-motion="none"]) .status-rec::before { animation: lamp-pulse var(--status-rec-pulse, 1.2s) ease-in-out infinite; }
}

  :root[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) .status-rec::before { animation: lamp-pulse var(--status-rec-pulse, 1.2s) ease-in-out infinite; }


/* --- htmx state ------------------------------------------------------- *
 * htmx sets these classes around every swap. Handling them here means an
 * app gets consistent, themed pending/settling feedback without writing
 * any of it — and each theme can give the indicator its own period look
 * via --indicator-*. */
.indicator {
  display: none;
  width: var(--indicator-size, 1em);
  height: var(--indicator-size, 1em);
  flex-shrink: 0;
  border: 2px solid var(--indicator-track, var(--border));
  border-top-color: var(--indicator-fg, var(--accent));
  border-radius: 50%;
  animation: spinner-rotate 0.7s linear infinite;
}
.htmx-request .indicator,
.indicator.htmx-request { display: inline-block; }

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

/* In-flight requests read as busy and stop taking further clicks — the
   single most commonly hand-rolled piece of htmx app polish. */
.htmx-request.btn,
.htmx-request .btn,
.htmx-request.input,
.htmx-request.select {
  opacity: var(--pending-opacity, 0.8);
  pointer-events: none;
}
.htmx-swapping { opacity: 0; transition: opacity 0.1s ease-out; }
.htmx-added { opacity: 0; }
.htmx-settling { opacity: 1; transition: opacity 0.15s ease-in; }

/* --- v3.4.0 additions ---------------------------------------------------
 * Sourced from real duplication in three consuming apps (CuTePi's context
 * menu and dropzone, PI9696's icon button and settings rows, Playlist-Lab's
 * toast/card/badge-button/empty-state) plus a small curated set of generic
 * primitives the wishlist calls out as universally needed. Same rules as
 * everything above: local custom-property API, tokens fall back to the
 * base palette, no app-specific class names. See CONTRACT.md.
 */

/* .toast — a transient, dismissable notification. Distinct from
   .alert (persistent, inline) and .status (single-word state). */
.toast-region {
  position: fixed;
  top: 1rem;
  inset-inline-end: 1rem;
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  max-width: min(22rem, calc(100vw - 2rem));
}
.toast {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  padding: var(--toast-pad, var(--space-s) var(--space-m));
  background: var(--toast-bg, var(--surface-2));
  color: var(--toast-fg, var(--text));
  border: var(--toast-border-width, 1px) solid var(--toast-border, var(--border));
  border-radius: var(--toast-radius, var(--radius));
  box-shadow: var(--toast-shadow, var(--panel-shadow, 0 2px 12px rgba(0, 0, 0, 0.25)));
  font-size: 0.88em;
}
.toast-success { --toast-border: var(--success); }
.toast-danger { --toast-border: var(--danger); }
.toast-warning { --toast-border: var(--warning); }

/* .spinner — a bare loading indicator for non-htmx use (page-level
   loads, async work outside a swap). .indicator stays the htmx-bound
   one; this is the same visual with no htmx-class dependency. */
.spinner {
  display: inline-block;
  width: var(--spinner-size, 1.2em);
  height: var(--spinner-size, 1.2em);
  flex-shrink: 0;
  border: 2px solid var(--spinner-track, var(--border));
  border-top-color: var(--spinner-fg, var(--accent));
  border-radius: 50%;
  animation: spinner-rotate 0.7s linear infinite;
}
/* .spinner.is-grow — a dot that swells and fades instead of a ring. */
.spinner.is-grow {
  border: 0;
  background: var(--spinner-fg, var(--accent));
  animation: spinner-grow 0.9s ease-in-out infinite;
}
@keyframes spinner-grow {
  0% { transform: scale(0); }
  50% { opacity: 1; transform: none; }
  100% { opacity: 0; }
}

/* .context-menu — same surface language as .dropdown, meant to be
   positioned at a cursor/anchor point by the consuming app's own JS. */
.context-menu {
  position: absolute;
  background: var(--context-menu-bg, var(--dropdown-bg, var(--surface-2)));
  border: var(--context-menu-border-width, 1px) solid var(--context-menu-border, var(--dropdown-border, var(--border)));
  border-radius: var(--context-menu-radius, var(--dropdown-radius, var(--radius)));
  box-shadow: var(--context-menu-shadow, var(--panel-shadow, 0 2px 12px rgba(0, 0, 0, 0.3)));
  padding: 0.35rem;
  min-width: 10rem;
  z-index: var(--z-dropdown);
}
:is(.context-menu-item, .context-menu li > :is(button, a), .menubar li li > :is(button, a)) {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  width: 100%;
  padding: 0.45em 0.7em;
  border-radius: var(--context-menu-radius, var(--radius));
  color: var(--context-menu-item-fg, var(--text));
  text-align: start;
}
:is(.context-menu-item, .context-menu li > :is(button, a)).is-danger { color: var(--danger-text, var(--danger)); }
:is(.context-menu-item, .context-menu li > :is(button, a), .menubar li li > :is(button, a)):focus-visible {
  background: var(--context-menu-item-bg-hover, var(--dropdown-item-bg-hover, var(--accent)));
  color: var(--context-menu-item-fg-hover, var(--dropdown-item-fg-hover, var(--on-accent)));
}
@media (hover: hover) {
:is(.context-menu-item, .context-menu li > :is(button, a), .menubar li li > :is(button, a)):hover {
  background: var(--context-menu-item-bg-hover, var(--dropdown-item-bg-hover, var(--accent)));
  color: var(--context-menu-item-fg-hover, var(--dropdown-item-fg-hover, var(--on-accent)));
}
}
:is(.context-menu-divider, .context-menu li[role="separator"], .menubar li[role="separator"]) {
  height: 1px;
  margin: 0.3rem 0.2rem;
  background: var(--context-menu-border, var(--hairline));
}

/* .dropzone — a drag-and-drop file target. */
.dropzone {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  padding: calc(var(--space-xl) * var(--density));
  border: var(--dropzone-border-width, 2px) dashed var(--dropzone-border, var(--border));
  border-radius: var(--dropzone-radius, var(--radius));
  background: var(--dropzone-bg, transparent);
  color: var(--dropzone-fg, var(--muted));
  text-align: center;
  transition: border-color 0.15s ease, background 0.15s ease;
}
.dropzone.is-dragover {
  border-color: var(--dropzone-border-active, var(--accent));
  background: var(--dropzone-bg-active, var(--surface-2));
  color: var(--dropzone-fg-active, var(--text));
}

/* .field-group — a titled group of .field rows (settings sections). */
.field-group { margin-block-end: 1.5rem; }
.field-group-title {
  margin-block-end: 0.5em;
  padding-bottom: 0.35em;
  border-bottom: 1px solid var(--hairline);
  color: var(--field-group-title-fg, var(--muted));
  font-size: 0.8em;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

/* .btn-icon — a circular icon-only button, layered on .btn. */
.btn-icon {
  --btn-padding: 0;
  --btn-radius: var(--btn-icon-radius, 50%);
  width: var(--btn-icon-size, 2.2em);
  height: var(--btn-icon-size, 2.2em);
  flex-shrink: 0;
}

/* .icon — the shared icon pack (assets/icons/icons.svg), used as
   <svg class="icon"><use href="assets/icons/icons.svg#icon-name"/></svg>.
   One outline set, reskinned per theme entirely through tokens: color
   always inherits (currentColor), so it matches whatever text/fg color
   surrounds it with no per-theme icon variants needed. A theme only sets
   --icon-stroke-width (bolder/thinner strokes) or --icon-fill
   (solid-fill treatments like Windows 95/LEGO) when its signature style
   calls for it — see docs/branding-guide.md. */
.icon {
  width: var(--icon-size, 1.2em);
  height: var(--icon-size, 1.2em);
  /* Floor against the reset's `max-width: 100%`: inside a shrink-to-fit
     button the content width depends on the svg while max-width resolves
     against that same width (a circular percentage), and engines may
     collapse the icon to 0 wide. min-width outranks max-width, so this
     pins the floor at the icon's own declared size — a no-op everywhere
     the width already resolves. */
  min-width: var(--icon-size, 1.2em);
  fill: var(--icon-fill, none);
  stroke: currentColor;
  stroke-width: var(--icon-stroke-width, 2);
  stroke-linecap: round;
  stroke-linejoin: round;
  vertical-align: -0.2em;
  flex-shrink: 0;
}
.btn-icon .icon { width: 1.1em; height: 1.1em; min-width: 1.1em; vertical-align: middle; }

/* .card — a lighter-weight sibling of .panel. Defaults to the same
   token values so a theme with no opinion renders identically; a theme
   that wants a visually distinct card sets --card-* explicitly. */
.card {
  background: var(--card-bg, var(--panel-bg, var(--surface)));
  color: var(--on-surface);
  border: var(--card-border-width, 1px) solid var(--card-border, var(--panel-border, var(--border)));
  border-radius: var(--card-radius, var(--panel-radius, var(--radius)));
  box-shadow: var(--card-shadow, var(--panel-shadow, none));
  --surface-pad: var(--card-pad, calc(var(--space-m) * var(--density)));
  padding: var(--surface-pad);
}

/* .badge-button — a clickable badge (filter chip). */
.badge-button {
  border: none;
  font: inherit;
  transition: background 0.15s ease, color 0.15s ease;
}
.badge-button:is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))) {
  background: var(--badge-bg-active, var(--accent));
  color: var(--badge-fg-active, var(--on-accent));
}
@media (hover: hover) {
.badge-button:hover {
  background: var(--badge-bg-active, var(--accent));
  color: var(--badge-fg-active, var(--on-accent));
}
}

/* .empty-state — a placeholder for an empty list/collection/result set. */
.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  padding: calc(var(--space-xl) * 1.25 * var(--density)) var(--space-m);
  color: var(--muted);
  text-align: center;
}
.empty-state-icon { font-size: 2rem; opacity: 0.6; }
.empty-state-title { color: var(--text); font-weight: 600; }
.empty-state-hint { font-size: 0.85em; }

/* .tooltip — CSS-only, attribute-driven: <span data-tooltip="…">. No JS,
   consistent with the rest of the library's no-JS-framework stance. */
[data-tooltip] { position: relative; }
[data-tooltip]::after {
  content: attr(data-tooltip);
  position: absolute;
  bottom: calc(100% + 0.4rem);
  left: 50%;
  transform: translateX(-50%) scale(0.95);
  padding: 0.35em 0.6em;
  background: var(--tooltip-bg, var(--surface-2));
  color: var(--tooltip-fg, var(--text));
  border: var(--tooltip-border-width, 1px) solid var(--tooltip-border, var(--border));
  border-radius: var(--tooltip-radius, var(--radius));
  font-size: 0.78em;
  /* Wraps rather than running off a narrow screen. */
  width: max-content;
  max-width: min(18rem, calc(100vw - 2rem));
  white-space: normal;
  /* Hidden means gone (display: none), not just transparent: an invisible
     tooltip near the screen edge still widened the page. The fade-in runs
     through @starting-style and a discrete display transition. */
  display: none;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.1s ease, transform 0.1s ease, display 0.1s allow-discrete;
  z-index: var(--z-tooltip);
}
[data-tooltip]:focus-visible::after {
  display: block;
  opacity: 1;
  transform: translateX(-50%) scale(1);
}
@media (hover: hover) {
[data-tooltip]:hover::after {
  display: block;
  opacity: 1;
  transform: translateX(-50%) scale(1);
}
}
@starting-style {
  [data-tooltip]:hover::after,
  [data-tooltip]:focus-visible::after {
    opacity: 0;
    transform: translateX(-50%) scale(0.95);
  }
}

/* .drawer — a slide-in side panel (settings sheet, filters, channel
   list). Docks to the inline-end edge; `.is-start` docks to the
   inline-start edge instead. Your app toggles `.is-open` (htmx swap,
   `:target`, or a line of JS) — the same contract as `.popover`.
   The closed state is `visibility: hidden` rather than `display: none`
   so the slide transition can run; a `hidden` attribute still removes it
   from the tree entirely for apps that want that. */
.drawer {
  position: fixed;
  inset-block: 0;
  inset-inline-end: 0;
  width: var(--drawer-width, min(24rem, 92vw));
  max-width: 100%;
  overflow-y: auto;
  background: var(--drawer-bg, var(--surface));
  color: var(--on-surface);
  border-inline-start: var(--drawer-border-width, 1px) solid var(--drawer-border, var(--border));
  box-shadow: var(--drawer-shadow, var(--panel-shadow, 0 0 24px rgba(0, 0, 0, 0.35)));
  z-index: var(--z-drawer);
  transform: translateX(105%);
  visibility: hidden;
  transition: transform 0.2s ease, visibility 0.2s;
}
html[dir="rtl"] .drawer { transform: translateX(-105%); }
.drawer.is-start {
  inset-inline-end: auto;
  inset-inline-start: 0;
  border-inline-start: 0;
  border-inline-end: var(--drawer-border-width, 1px) solid var(--drawer-border, var(--border));
  transform: translateX(-105%);
}
html[dir="rtl"] .drawer.is-start { transform: translateX(105%); }
.drawer.is-open { transform: none; visibility: visible; }

/* .popover — same surface as .dropdown, toggled by the app via a
   class (e.g. htmx swap or a small inline script) rather than :hover. */
.popover {
  position: absolute;
  background: var(--popover-bg, var(--dropdown-bg, var(--surface-2)));
  border: var(--popover-border-width, 1px) solid var(--popover-border, var(--dropdown-border, var(--border)));
  border-radius: var(--popover-radius, var(--dropdown-radius, var(--radius)));
  box-shadow: var(--popover-shadow, var(--panel-shadow, 0 2px 12px rgba(0, 0, 0, 0.3)));
  padding: var(--popover-pad, var(--space-s));
  max-width: 20rem;
  z-index: var(--z-dropdown);
}

/* .accordion — built on <details>/<summary> so it needs no JS. */
.accordion-item {
  border-bottom: 1px solid var(--hairline);
}
.accordion-trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.75em 0.25em;
  color: var(--text);
  font-weight: 600;
  list-style: none;
}
.accordion-trigger::-webkit-details-marker { display: none; }
.accordion-trigger::after {
  content: "+";
  color: var(--muted);
  transition: transform 0.15s ease;
}
.accordion-item[open] > .accordion-trigger::after { transform: rotate(45deg); }
.accordion-panel { padding: 0 0.25em 1em; color: var(--accordion-panel-fg, var(--text)); }

/* .list — a bordered stack of rows (queues, menus, activity): the
   thing between a table (columns) and a menu (overlay). Items can be
   plain rows or links/buttons; the active row carries a background and
   a leading marker, the same two-language state tables use. */
.list {
  margin: 0;
  padding: 0;
  list-style: none;
  background: var(--list-bg, var(--surface));
  border: var(--list-border-width, 1px) solid var(--list-border, var(--border));
  border-radius: var(--list-radius, var(--radius));
  overflow: hidden;
}
.list > :not(:first-child) { border-block-start: 1px solid var(--list-rule, var(--hairline)); }
.list-item {
  display: flex;
  align-items: center;
  gap: 0.6em;
  width: 100%;
  padding: var(--list-item-padding, 0.55em 0.9em);
  border: 0;
  background: transparent;
  color: var(--list-item-fg, var(--text));
  font: inherit;
  text-align: start;
}
a.list-item { background: var(--row-hover-bg, rgba(128, 128, 128, 0.12)); }
@media (hover: hover) {
a.list-item:hover,
button.list-item:hover { background: var(--row-hover-bg, rgba(128, 128, 128, 0.12)); }
}
.list-item:is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))) {
  background: var(--row-selected-bg, rgba(128, 128, 128, 0.25));
  color: var(--row-selected-fg, var(--text));
  box-shadow: var(--list-active-marker, var(--row-active-marker, inset 0.25rem 0 0 var(--accent)));
}
/* .list.list-cards — each item is its own card (the theme's card tokens),
   the list itself has no box; items sit --list-gap apart. .is-featured
   takes a thick accent border; .is-success/-warning/-danger/-info a status
   border and tint. */
.list.list-cards { display: grid; gap: var(--list-gap, 0.6rem); background: none; border: 0; border-radius: 0; overflow: visible; }
.list-cards > .list-item {
  background: var(--list-card-bg, var(--card-bg, var(--panel-bg, var(--surface))));
  border: var(--list-card-border-width, var(--card-border-width, 1px)) solid var(--list-card-border, var(--card-border, var(--panel-border, var(--border))));
  border-radius: var(--list-card-radius, var(--card-radius, var(--panel-radius, var(--radius))));
  box-shadow: var(--list-card-shadow, var(--card-shadow, var(--panel-shadow, none)));
}
.list-cards > .list-item.is-featured { border-width: var(--list-featured-border-width, 4px); border-color: var(--list-featured-border, var(--accent)); }
.list-cards > .list-item.is-success { --_list-tone: var(--success); }
.list-cards > .list-item.is-warning { --_list-tone: var(--warning); }
.list-cards > .list-item.is-danger { --_list-tone: var(--danger); }
.list-cards > .list-item.is-info { --_list-tone: var(--info, var(--accent)); }
.list-cards > .list-item:is(.is-success, .is-warning, .is-danger, .is-info) {
  --list-item-meta-fg: var(--list-item-fg, var(--text));
  border-color: var(--_list-tone);
  background: color-mix(in srgb, var(--_list-tone) var(--list-tone-tint, 16%), var(--list-card-bg, var(--card-bg, var(--panel-bg, var(--surface)))));
}
.list-cards > .list-item:is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))) { box-shadow: var(--list-active-marker, var(--row-active-marker, inset 0.25rem 0 0 var(--accent))), var(--list-card-shadow, var(--card-shadow, var(--panel-shadow, 0 0 transparent))); }

/* .carousel — a no-JS slide strip: a scroll-snap track plus plain
   anchor links, so swipe, keyboard, dots and prev/next all work with zero
   script. Dots/arrows are just <a href="#slide-id">; the browser does the
   rest. Smooth scrolling is gated on reduced-motion (both the OS setting
   and html[data-motion]). */
.carousel {
  display: flex;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  border-radius: var(--carousel-radius, var(--radius));
}
.carousel::-webkit-scrollbar { display: none; }
@media (prefers-reduced-motion: no-preference) {
  .carousel { scroll-behavior: smooth; }
}

  :root[data-motion="always"] .carousel { scroll-behavior: smooth; }

html[data-motion="reduced"] .carousel { scroll-behavior: auto; }
.carousel-slide {
  flex: 0 0 100%;
  min-width: 0;
  scroll-snap-align: center;
  scroll-snap-stop: always;
}
.carousel-nav {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5em;
  margin-block-start: 0.6rem;
}
.carousel-nav > a {
  display: inline-block;
  min-width: 0.7em;
  height: 0.7em;
  padding: 0 0.35em;
  border-radius: 999px;
  background: var(--carousel-dot, var(--muted));
  color: transparent;
  font-size: 0.7em;
  line-height: 1;
  overflow: hidden;
  white-space: nowrap;
}
@media (hover: hover) {
.carousel-nav > a:hover { background: var(--carousel-dot-active, var(--accent)); }
}
/* Prev/next keep the dot-row's shape language but read as buttons. */
/* The dot for the slide in view (assets/js/carousel.js marks it). */
.carousel-nav > a[aria-current="true"] { background: var(--carousel-dot-active, var(--accent)); }
.carousel-nav > .carousel-prev,
.carousel-nav > .carousel-next {
  min-width: 1.6em;
  height: 1.6em;
  line-height: 1.6em;
  padding: 0;
  color: var(--carousel-arrow-fg, var(--text));
  background: var(--carousel-arrow-bg, var(--surface-2));
  text-align: center;
  font-size: 0.85em;
}
/* Where CSS carousels ship (::scroll-marker, ::scroll-button(); Chrome
   135+), the browser draws the dots and arrows itself: they scroll only the
   carousel, track the slide in view (:target-current) and need no script,
   so the fragment-link .carousel-nav (and carousel.js) is the fallback. */
@supports selector(::scroll-marker) and selector(::scroll-button(*)) {
  .carousel {
    scroll-marker-group: after;
    anchor-scope: --carousel-markers;
    counter-reset: carousel-slide;
  }
  .carousel + .carousel-nav { display: none; }
  .carousel::scroll-marker-group {
    display: flex;
    justify-content: center;
    gap: 0.5em;
    block-size: var(--tap-min);
    margin-block-start: 0.6rem;
    anchor-name: --carousel-markers;
    padding-inline: calc(var(--tap-min) + 0.5em);
  }
  .carousel > .carousel-slide { counter-increment: carousel-slide; }
  .carousel > .carousel-slide::scroll-marker {
    content: "" / "Slide " counter(carousel-slide);
    display: block;
    box-sizing: border-box;
    inline-size: var(--tap-min);
    block-size: var(--tap-min);
    padding: calc((var(--tap-min) - 0.7em * 0.7) / 2);
    border-radius: 999px;
    background: var(--carousel-dot, var(--muted)) content-box;
  }
  .carousel > .carousel-slide::scroll-marker:target-current { background-color: var(--carousel-dot-active, var(--accent)); }
  .carousel > .carousel-slide::scroll-marker:focus-visible { outline: 2px solid var(--focus, var(--accent)); outline-offset: -0.5em; }
  .carousel::scroll-button(*) {
    position: absolute;
    /* At the two ends of the marker row (its inline padding leaves room). */
    position-anchor: --carousel-markers;
    inset-block-start: anchor(start);
    inset-inline-start: anchor(start);
    z-index: var(--z-raised);
    inline-size: var(--tap-min);
    block-size: var(--tap-min);
    padding: 0;
    border: 0;
    border-radius: 999px;
    color: var(--carousel-arrow-fg, var(--text));
    background: var(--carousel-arrow-bg, var(--surface-2));
    font: inherit;
    font-size: 0.85em;
  }
  .carousel::scroll-button(left) { content: "\2039" / "Previous"; }
  .carousel::scroll-button(right) { content: "\203a" / "Next"; inset-inline: auto anchor(end); }
  .carousel::scroll-button(*):disabled { opacity: 0.4; }
}

/* .scrollspy — a sticky section nav beside long content (docs,
   settings reference). The nav, the smooth scroll and the scroll offset
   are core; the active-link mapping is one `:target` rule per section in
   the app's own CSS (see CONTRACT.md), or an `.is-active` class if the
   app runs a scroll observer. Core cannot map links to sections by
   itself: CSS has no way to correlate an href with an id without
   enumerating them, and enumerating them is app knowledge. */
.scrollspy-nav {
  position: sticky;
  top: 0;
  z-index: var(--z-sticky);
  display: flex;
  flex-wrap: wrap;
  gap: 0.15rem 0.9rem;
  padding-block: 0.5rem;
  background: var(--scrollspy-nav-bg, var(--surface));
  border-block-end: 1px solid var(--scrollspy-nav-rule, var(--hairline));
}
.scrollspy-link {
  padding: 0.25em 0.1em;
  color: var(--scrollspy-link-fg, var(--muted));
  font-size: 0.85em;
  text-decoration: none;
}
@media (hover: hover) {
.scrollspy-link:hover { color: var(--text); }
}
.scrollspy-link:is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))) {
  color: var(--scrollspy-link-active-fg, var(--accent));
  box-shadow: var(--scrollspy-link-active-marker, inset 0 -2px 0 var(--accent));
}
.scrollspy-body > * { scroll-margin-top: var(--scrollspy-offset, 1rem); }
@media (prefers-reduced-motion: no-preference) {
  .scrollspy-body { scroll-behavior: smooth; }
}

  :root[data-motion="always"] .scrollspy-body { scroll-behavior: smooth; }

html[data-motion="reduced"] .scrollspy-body { scroll-behavior: auto; }

/* .nav-collapse — a responsive nav toggler with no JS: a native
   <details>/<summary> (keyboard- and screen-reader-handled by the
   browser, like .accordion). Up to 900px (the mobile and tablet tiers)
   the summary renders as a hamburger and the nav stacks only while open;
   above it the toggle hides and the nav is always laid out, open or not. */
.nav-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.2em;
  height: 2.2em;
  border: var(--input-border-width, 1px) solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  font-size: 1rem;
  list-style: none;
}
.nav-toggle::-webkit-details-marker { display: none; }
.nav-toggle::before { content: "\2630"; }
.nav-collapse[open] > .nav-toggle::before { content: "\d7"; }
.nav-collapse:not([open]) > .nav { display: none; }
@media (max-width: 900px) {
  .nav-collapse[open] > .nav {
    flex-direction: column;
    align-items: stretch;
    gap: 0.25rem;
    margin-block-start: 0.5rem;
  }
}
@media (min-width: 901px) {
  .nav-collapse > .nav-toggle { display: none; }
  /* Outranks the UA's `details:not([open]) > :not(summary)` hide, so the
     nav lays out on desktop whether or not the details is open. */
  .nav-collapse:not([open]) > .nav { display: flex; }
  /* Chromium 131+ hides a closed <details>'s content through
     ::details-content { content-visibility: hidden }, which display
     can't override. Engines without the pseudo-element drop this rule. */
  .nav-collapse::details-content { content-visibility: visible; display: contents; }
}

/* .taskbar — an OS-style task strip: a Start button, window task
   buttons, and a tray well. Pair it with .app-status (or use it
   standalone); desktop-OS themes paint it in their taskbar language
   through --taskbar-* tokens, every other theme leaves the plain
   fallback. Apps can drop their own mark inside .taskbar-start
   (logo svg, orb glyph) — core draws no brand artwork. */
.taskbar {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  background: var(--taskbar-bg, transparent);
}
.taskbar-start {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.45em;
  flex: none;
  width: var(--taskbar-start-size, auto);
  height: var(--taskbar-start-size, auto);
  padding: var(--taskbar-start-padding, 0.3em 0.9em);
  border: var(--taskbar-border-width, 1px) solid var(--taskbar-start-border, transparent);
  border-radius: var(--taskbar-start-radius, var(--radius));
  background: var(--taskbar-start-bg, transparent);
  color: var(--taskbar-start-fg, inherit);
  font-weight: 700;
  font-style: var(--taskbar-start-style, normal);
  text-shadow: var(--taskbar-start-text-shadow, none);
  box-shadow: var(--taskbar-start-shadow, none);
}
.taskbar-start:active { transform: translateY(1px); }
.taskbar-task {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  min-width: 0;
  max-width: var(--taskbar-task-max, 14rem);
  padding: 0.3em 0.8em;
  border: var(--taskbar-border-width, 1px) solid var(--taskbar-task-border, transparent);
  border-radius: var(--taskbar-task-radius, var(--radius));
  background: var(--taskbar-task-bg, transparent);
  color: var(--taskbar-task-fg, inherit);
  box-shadow: var(--taskbar-task-shadow, none);
  overflow: hidden;
  white-space: nowrap;
}
.taskbar-task > :not(svg):not(.icon) {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.taskbar-task:is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))) {
  background: var(--taskbar-task-bg-active, var(--taskbar-task-bg, transparent));
  border-color: var(--taskbar-task-border-active, var(--taskbar-task-border, transparent));
  box-shadow: var(--taskbar-task-shadow-active, var(--taskbar-task-shadow, none));
  outline: var(--taskbar-task-outline-active, none);
  outline-offset: -4px;
}
.taskbar-tray {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  flex: none;
  margin-inline-start: auto;
  padding: 0.3em 0.8em;
  border: var(--taskbar-border-width, 1px) solid var(--taskbar-tray-border, transparent);
  border-radius: var(--taskbar-tray-radius, var(--radius));
  background: var(--taskbar-tray-bg, transparent);
  color: var(--taskbar-tray-fg, inherit);
  font-variant-numeric: tabular-nums;
}

/* .breadcrumbs */
.breadcrumbs {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.4em;
  font-size: 0.85em;
  color: var(--muted);
}
.breadcrumb-item { color: inherit; }
.breadcrumb-item:not(:last-child)::after {
  content: "/";
  margin-inline-start: 0.4em;
  color: var(--breadcrumb-separator, var(--muted));
}
.breadcrumb-item.is-current {
  color: var(--breadcrumb-current-fg, var(--text));
  font-weight: 600;
}

/* .pagination */
.pagination {
  display: flex;
  align-items: center;
  gap: 0.3rem;
}
.pagination-item {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2em;
  height: 2em;
  padding: 0 0.4em;
  border: 1px solid transparent;
  border-radius: var(--pagination-item-radius, var(--radius));
  color: var(--pagination-item-fg, var(--muted));
  font-size: 0.85em;
}
@media (hover: hover) {
.pagination-item:hover {
  background: var(--pagination-item-bg-hover, var(--surface-2));
  color: var(--pagination-item-fg-hover, var(--text));
}
}
.pagination-item:is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))) {
  background: var(--pagination-item-bg-active, var(--accent));
  color: var(--pagination-item-fg-active, var(--on-accent));
  font-weight: 600;
}
.pagination-item.is-disabled { opacity: 0.4; pointer-events: none; }

/* .alert — persistent inline banner. Distinct from the transient
   .toast and the single-word .status. */
.alert {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  padding: 0.75em 1em;
  border: var(--alert-border-width, 1px) solid var(--alert-border, var(--border));
  border-radius: var(--alert-radius, var(--radius));
  background: var(--alert-bg, var(--surface-2));
  color: var(--alert-fg, var(--text));
  font-size: 0.9em;
}
.alert-info { --alert-border: var(--accent); }
.alert-success { --alert-border: var(--success); }
.alert-warning { --alert-border: var(--warning); }
.alert-danger { --alert-border: var(--danger); }

/* .skeleton — loading placeholder shape. */
.skeleton {
  display: block;
  background: var(--skeleton-bg, var(--surface-2));
  border-radius: var(--skeleton-radius, var(--radius));
  background-image: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.08), transparent);
  background-size: 40% 100%;
  background-repeat: no-repeat;
  animation: skeleton-shimmer 1.4s ease-in-out infinite;
}
.skeleton-text { height: 0.9em; margin: 0.25em 0; border-radius: 0.2em; }
.skeleton-block { height: 4rem; }
@keyframes skeleton-shimmer {
  0% { background-position: -40% 0; }
  100% { background-position: 140% 0; }
}

/* .avatar */
.avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--avatar-size, 2.2rem);
  height: var(--avatar-size, 2.2rem);
  flex-shrink: 0;
  border-radius: var(--avatar-radius, 50%);
  background: var(--avatar-bg, var(--surface-2));
  color: var(--avatar-fg, var(--muted));
  font-size: 0.85em;
  font-weight: 600;
  overflow: hidden;
}
.avatar img { width: 100%; height: 100%; object-fit: cover; }
.avatar-sm { --avatar-size: 1.6rem; font-size: 0.7em; }
.avatar-lg { --avatar-size: 3.2rem; font-size: 1.1em; }

/* .stat — a KPI tile. */
.stat { display: flex; flex-direction: column; gap: 0.2em; }
.stat-value {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 1.6rem;
  font-weight: 700;
  color: var(--text);
}
.stat-label {
  font-size: 0.78em;
  color: var(--muted);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.stat-trend { font-size: 0.8em; }
.stat-trend.is-up { --state-fg: var(--success-text, var(--success)); color: var(--state-fg); }
.stat-trend.is-down { --state-fg: var(--danger-text, var(--danger)); color: var(--state-fg); }

/* .divider */
.divider {
  border: none;
  border-top: 1px solid var(--hairline);
  margin: 1rem 0;
}
.divider-v {
  border: none;
  border-inline-start: 1px solid var(--hairline);
  align-self: stretch;
  margin: 0 0.5rem;
}

/* --- Utilities -------------------------------------------------------- */
.mono {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}

.btn-group,
.btn-toolbar {
  display: inline-flex;
  align-items: center;
  gap: var(--btn-group-gap, 0.4rem);
}
.btn-group { flex-wrap: nowrap; }
.btn-toolbar { flex-wrap: wrap; }

/* .toggle-btn — a button-looking checkbox: <label class="toggle-btn">
   <input type="checkbox"><span class="toggle-btn-off">Mute</span>
   <span class="toggle-btn-on">Unmute</span></label>. State is pure CSS. */
.toggle-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5em;
  /* Falls back to the theme's button padding, so a toggle sits at the same
     height as the buttons beside it. */
  padding: var(--toggle-btn-padding, var(--btn-padding, calc(0.5em * var(--density)) calc(1.1em * var(--density))));
  border: var(--toggle-btn-border-width, var(--btn-border-width, 1px)) solid var(--toggle-btn-border, var(--btn-border, var(--border)));
  border-radius: var(--toggle-btn-radius, var(--btn-radius, var(--radius)));
  background: var(--toggle-btn-bg, var(--btn-bg, var(--surface-2)));
  color: var(--toggle-btn-fg, var(--btn-fg, var(--text)));
  font-size: 0.9em;
  user-select: none;
  transition: background 0.15s ease, border-color 0.15s ease;
}
.toggle-btn input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  overflow: hidden;
  clip-path: inset(50%);
  opacity: 0;
}
@media (hover: hover) {
.toggle-btn:hover { border-color: var(--toggle-btn-border-hover, var(--accent)); }
}
.toggle-btn:has(input:checked) {
  background: var(--toggle-btn-bg-on, var(--accent));
  color: var(--toggle-btn-fg-on, var(--on-accent));
  border-color: var(--toggle-btn-border-on, var(--accent));
}
.toggle-btn.is-danger {
  --toggle-btn-bg-on: var(--danger);
  --toggle-btn-fg-on: var(--on-danger);
  --toggle-btn-border-on: var(--danger);
}
.toggle-btn:has(input:focus-visible) { outline: 2px solid var(--focus); outline-offset: 2px; }
.toggle-btn:has(input:disabled) { opacity: 0.45; cursor: var(--cursor-not-allowed, not-allowed); }
/* Both labels share one grid cell and the hidden one keeps its space, so
   the button is always as wide as its longer label: toggling "Rec" to
   "Recording" doesn't change its width or move anything beside it. */
.toggle-btn:has(> .toggle-btn-on) { display: inline-grid; place-items: center; }
.toggle-btn > :is(.toggle-btn-on, .toggle-btn-off) { grid-area: 1 / 1; }
.toggle-btn-on { visibility: hidden; }
.toggle-btn:has(input:checked) .toggle-btn-on { visibility: visible; }
.toggle-btn:has(input:checked) .toggle-btn-off { visibility: hidden; }

/* .schedule — a small time badge: <time class="schedule" datetime="14:30">
   <svg class="icon"><use href="#icon-clock"/></svg><span>14:30</span></time> */
.schedule {
  display: inline-flex;
  align-items: center;
  gap: var(--schedule-gap, 0.3em);
  color: var(--schedule-color, var(--accent-text, var(--accent)));
  font-size: var(--schedule-font-size, 0.8em);
  font-variant-numeric: tabular-nums;
}
.schedule .icon { width: 1em; height: 1em; flex: none; }

@media (prefers-reduced-motion: reduce) {
  :root:not([data-motion="always"]) .btn,:root:not([data-motion="always"]) .drawer,:root:not([data-motion="always"]) .switch-track,:root:not([data-motion="always"]) .switch-thumb,:root:not([data-motion="always"]) .toggle-btn,:root:not([data-motion="always"]) .htmx-swapping,:root:not([data-motion="always"]) .htmx-settling { transition: none; }
  :root:not([data-motion="always"]) .indicator,:root:not([data-motion="always"]) .spinner,:root:not([data-motion="always"]) .skeleton { animation-duration: 2s; }
}

/* --- User display options -----------------------------------------------
 * Three switches an app can offer in its own settings UI, independent of
 * theme choice. All are read off <html>, so an app sets one attribute (or,
 * for density, overrides one variable) and every theme responds — no theme
 * file needs to know these exist. See CONTRACT.md "User display options".
 */

/* Density: apps needing a coarser control than a theme's own --density
 * default set an inline override directly — `<html style="--density:
 * 0.85">` — which wins over any stylesheet rule regardless of specificity,
 * so no attribute or selector is needed here at all. */

/* Motion: an in-app "reduce motion" toggle, for a user who wants animation
 * off without changing their OS-level prefers-reduced-motion setting (or
 * whose browser can't report it, e.g. an embedded webview). Themes opt
 * their own animations out of prefers-reduced-motion already; this forces
 * the same outcome from an explicit attribute instead of a media query. */
html[data-motion="reduced"] * {
  animation-play-state: paused !important;
  transition-duration: 0.001ms !important;
  scroll-behavior: auto !important;
}

/* Contrast: a boost for a user who finds a theme's hairlines/muted text too
 * quiet, without switching theme entirely. Widens the two things contrast
 * actually depends on — visible borders and secondary text — by pulling
 * them toward the theme's own text/border colors rather than introducing a
 * palette of its own, so it stays in-theme instead of overriding it. */
/* The rules for data-contrast live in core/components/experience.css (v5):
   the v4 rules here lost to every theme's root block. */

/* --- Accent swatches (optional per-theme accent override) ---------------
 * A theme MAY offer a curated set of alternate accents as numbered token
 * pairs: --accent-swatch-<n> and --on-accent-swatch-<n> (n = 1-6),
 * each pre-checked by the theme's author against the contrast floor. An
 * app offers whichever swatches a theme actually defines (undefined ones
 * are simply absent — nothing breaks if a theme has 3 instead of 6) and
 * sets data-accent="<n>" on <html> to select one. This replaces "custom
 * theme JSON" style features with something themes can vouch for: every
 * swatch a theme ships is a color ITS author picked and verified, never an
 * arbitrary user-supplied value that could fail contrast unnoticed. */
/* The data-accent mapping lives in core/components/experience.css (v5): the
   v4 rules here lost to every theme's root block. */

/* --- Layout primitives -----------------------------------------------------
 * Components carry no outer margin; these give a page its rhythm without
 * app CSS. Gaps scale with --density.
 *   .stack    vertical flow        .cluster  wrapping row
 *   .grid     responsive columns   -sm / -lg     tighter / looser gap */
.stack {
  display: flex;
  flex-direction: column;
  gap: var(--stack-gap, calc(1rem * var(--density)));
}
.cluster {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--cluster-gap, calc(0.6rem * var(--density)));
}
.grid {
  display: grid;
  grid-template-columns: repeat(var(--grid-repeat, auto-fit), minmax(min(100%, var(--grid-min, 16rem)), 1fr));
  gap: var(--grid-gap, calc(1rem * var(--density)));
  align-items: start;
}
.stack-sm { --stack-gap: calc(0.5rem * var(--density)); }
.stack-lg { --stack-gap: calc(2rem * var(--density)); }
.cluster-sm { --cluster-gap: calc(0.3rem * var(--density)); }
.cluster-lg { --cluster-gap: calc(1.2rem * var(--density)); }
.grid-sm { --grid-min: 11rem; }
.grid-lg { --grid-min: 22rem; }
/* .is-fill keeps empty tracks, so a lone card stays one column wide (galleries);
   the default auto-fit stretches it (dashboards). Gap: .grid-gap-sm / -lg. */
.grid.is-fill { --grid-repeat: auto-fill; }
.grid-gap-sm { --grid-gap: calc(0.5rem * var(--density)); }
.grid-gap-lg { --grid-gap: calc(2rem * var(--density)); }
/* Children of a stack/cluster/grid own no margins of their own. */
:is(.stack, .cluster, .grid) > * { margin-block: 0; }
/* Inline-sized components keep their own width in a stack instead of
   stretching across it. */
.stack > :is(.segmented, .btn, .badge, .anchor, .readout,
  .lamp, .avatar, .spinner, .check, .status) { align-self: flex-start; }

/* .anchor — the positioned parent a popover, context menu or dropdown
 * hangs from. Wrap trigger + surface; the app toggles visibility.
 *   <span class="anchor"><button …>Menu</button><div class="context-menu is-below">…</div></span> */
.anchor { position: relative; display: inline-block; }
.anchor > :is(.popover, .context-menu, .dropdown) {
  position: absolute;
  z-index: var(--z-dropdown);
  inset-inline-start: 0;
}
.anchor > .is-below { top: calc(100% + 0.25rem); }
.anchor > .is-above { bottom: calc(100% + 0.25rem); }
.anchor > .is-end { inset-inline-start: auto; inset-inline-end: 0; }

/* --- Chart palette -------------------------------------------------------
 * For canvas/SVG charts drawn by app JS, which can't see CSS. Read them with
 * getComputedStyle(document.documentElement).getPropertyValue(...) and read
 * again after a theme switch. Defaults derive from base tokens, so every
 * theme has a usable palette; themes override individual series. */
:root {
  --chart-text: var(--muted);
  --chart-grid: var(--hairline);
  --chart-series-1: var(--accent);
  --chart-series-2: var(--success);
  --chart-series-3: var(--warning);
  --chart-series-4: var(--danger);
  --chart-series-5: var(--accent-2);
  --chart-series-6: var(--muted);
}

/* --- .log -------------------------------------------------------------
 * A log/diagnostics console. The app appends lines and owns autoscroll;
 * overflow-anchor keeps a user who has scrolled up where they are. */
.log {
  max-height: var(--log-height, 24rem);
  overflow: auto;
  overflow-anchor: auto;
  padding: var(--space-xs) 0;
  background: var(--log-bg, var(--surface));
  color: var(--log-fg, var(--text));
  border: var(--log-border-width, 1px) solid var(--log-border, var(--border));
  border-radius: var(--log-radius, var(--radius));
  font-family: var(--font-mono);
  font-size: 0.82em;
  line-height: 1.5;
}
.log-line {
  display: flex;
  gap: 0.75em;
  padding: 0 var(--space-s);
  border-inline-start: 3px solid transparent;
  white-space: var(--log-wrap, pre-wrap);
  overflow-wrap: anywhere;
  content-visibility: auto;
  contain-intrinsic-size: auto 1.5em;
}
.log-nowrap .log-line { white-space: pre; overflow: hidden; text-overflow: ellipsis; }
.log-time { flex: none; color: var(--log-time-fg, var(--muted)); }
.log-line[data-level="debug"] { color: var(--log-debug, var(--muted)); }
.log-line[data-level="warn"] {
  color: var(--log-warn, var(--warning-text, var(--warning)));
  border-inline-start-color: var(--warning);
}
.log-line[data-level="error"] {
  color: var(--log-error, var(--danger-text, var(--danger)));
  border-inline-start-color: var(--danger);
}

/* --- Browser chrome ------------------------------------------------------
 * Surfaces the platform paints itself rather than any markup, so a
 * theme that nails every component still leaked an unthemed native blue
 * selection, a default OS scrollbar, or gray browser placeholder text —
 * the tells that break immersion fastest in a retro/console theme. All
 * token-driven with base-token fallbacks, same as every component above:
 * zero theme-file changes required to pick these up.
 */

/* Text selection highlight. */
::selection {
  background: var(--selection-bg, var(--accent));
  color: var(--selection-fg, var(--on-accent));
}

/* Scrollbars: Firefox's two-color shorthand plus the WebKit/Blink pseudo
   elements it has no equivalent for. Track defaults to transparent — most
   themes read better with the page/surface showing through than a second
   opaque fill competing with the thumb. */
:root {
  scrollbar-color: var(--scrollbar-thumb, var(--border)) var(--scrollbar-track, transparent);
  scrollbar-width: var(--scrollbar-width, thin);
}
::-webkit-scrollbar {
  width: var(--scrollbar-size, 0.85rem);
  height: var(--scrollbar-size, 0.85rem);
}
::-webkit-scrollbar-track { background: var(--scrollbar-track, transparent); }
::-webkit-scrollbar-thumb {
  background: var(--scrollbar-thumb, var(--border));
  border-radius: var(--scrollbar-radius, 999px);
  border: var(--scrollbar-thumb-border, 2px solid transparent);
  background-clip: padding-box;
}
@media (hover: hover) {
::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover, var(--accent)); }
}

/* Form placeholder text — browsers default this to their own gray,
   independent of --input-fg. */
.input::placeholder,
.textarea::placeholder { color: var(--input-placeholder, var(--muted)); opacity: 1; }

/* Text-entry caret: cheap to retarget, easy to forget — left at the
   browser default it's whatever native color the OS picked, not the
   theme's accent. */
.input,
.textarea { caret-color: var(--input-caret, var(--focus)); }

/* <kbd> — an inline keyboard-shortcut glyph. Common in retro-OS and
   console themes (tooltips, command references) and left entirely to the
   browser's tiny-monospace-box default otherwise. */
kbd {
  display: inline-block;
  padding: 0.1em 0.45em;
  background: var(--kbd-bg, var(--surface-2));
  color: var(--kbd-fg, var(--text));
  border: var(--kbd-border-width, 1px) solid var(--kbd-border, var(--border));
  border-radius: var(--kbd-radius, calc(var(--radius) * 0.6));
  box-shadow: var(--kbd-shadow, inset 0 -1px 0 var(--kbd-border, var(--border)));
  font-family: var(--font-mono);
  font-size: 0.85em;
  line-height: 1.4;
}

/* <code>/<pre> — inline and block code, distinct from .readout (a
   live machine value) and .mono (a bare font-family utility). */
code {
  padding: 0.15em 0.4em;
  background: var(--code-bg, var(--surface-2));
  color: var(--code-fg, var(--text));
  border-radius: calc(var(--radius) * 0.5);
  font-family: var(--font-mono);
  font-size: 0.9em;
}
pre {
  padding: calc(var(--space-m) * var(--density));
  background: var(--code-block-bg, var(--surface));
  border: var(--code-block-border-width, 1px) solid var(--code-block-border, var(--border));
  border-radius: var(--radius);
  overflow-x: auto;
}
pre code { padding: 0; background: none; border-radius: 0; }

/* --- Palette variants (optional per-theme, e.g. data-variant="grape") ----
 * A theme may ship more than one palette under one identity — the way the
 * references actually shipped (Luna's Blue/Olive/Silver, iMac G3's fruit
 * colors). No core mechanism is needed: a variant is just a theme selector
 * with an extra attribute, `html[data-theme="x"][data-variant="y"]`, which
 * at (0,2,1) outranks the theme's own root block at (0,1,1) and so wins.
 * See CONTRACT.md "Palette variants" and themes/imac-g3 for a worked
 * example (Bondi/Blueberry/Grape/Tangerine).
 */

/* --- Layout/flow utilities ----------------------------------------------
 * Deliberately tiny: the one layer this library doesn't otherwise offer,
 * for the "arrange two children in a row / gap them" sentences every app
 * template needs and would otherwise reach for a separate utility
 * framework (Bootstrap's d-flex, gap-N, row/col, mt/mb, text-muted, etc) to
 * write. Token-driven spacing scale so a theme can widen or tighten the
 * whole app's rhythm from one place — see CONTRACT.md "Layout utilities".
 */
:root {
  --space-3xs: 0.125rem;
  --space-2xs: 0.25rem;
  --space-xs: 0.5rem;
  --space-s: 0.75rem;
  --space-m: 1rem;
  --space-l: 1.5rem;
  --space-xl: 2rem;
  --space-2xl: 3rem;
}
/* v5: the scale steps per tier (PLAN.md §5), so every component that reads
   it tightens on phones and opens up on wide screens from one place. The
   widths match core/layout.css's tiers. */
@media (max-width: 480px) {
  :root {
    --space-s: 0.625rem;
    --space-m: 0.75rem;
    --space-l: 1.125rem;
    --space-xl: 1.5rem;
    --space-2xl: 2rem;
  }
}
@media (min-width: 1801px) {
  :root {
    --space-l: 1.75rem;
    --space-xl: 2.5rem;
    --space-2xl: 3.5rem;
  }
}

.row, .flex { display: flex; gap: var(--space-m); }
.row { flex-direction: row; }
.flex.is-column { flex-direction: column; }

.row.is-items-start, .stack.is-items-start, .flex.is-items-start { align-items: flex-start; }
.row.is-items-center, .stack.is-items-center, .flex.is-items-center { align-items: center; }
.row.is-items-end, .stack.is-items-end, .flex.is-items-end { align-items: flex-end; }
.row.is-items-stretch, .stack.is-items-stretch, .flex.is-items-stretch { align-items: stretch; }

.row.is-justify-between, .stack.is-justify-between, .flex.is-justify-between { justify-content: space-between; }
.row.is-justify-center, .stack.is-justify-center, .flex.is-justify-center { justify-content: center; }
.row.is-justify-end, .stack.is-justify-end, .flex.is-justify-end { justify-content: flex-end; }

.row.is-wrap, .flex.is-wrap { flex-wrap: wrap; }

/* Gap size modifiers — set `gap` itself, not a custom property: a custom
   property would inherit into nested .row/.flex/.stack children. */
.is-gap-none { gap: 0; }
.is-gap-2xs { gap: var(--space-2xs); }
.is-gap-xs { gap: var(--space-xs); }
.is-gap-s { gap: var(--space-s); }
.is-gap-l { gap: var(--space-l); }
.is-gap-xl { gap: var(--space-xl); }

/* Margin-step utilities — the mt-*/mb-* sentences a template reaches for
   most often. Deliberately not a full margin/padding utility grid (that's
   what a real utility framework is for); this is the ~6-name subset that
   shows up constantly in practice. */
.mt-xs { margin-block-start: var(--space-xs); }
.mt-s { margin-block-start: var(--space-s); }
.mt-m { margin-block-start: var(--space-m); }
.mt-l { margin-block-start: var(--space-l); }
.mt-xl { margin-block-start: var(--space-xl); }
.mb-xs { margin-block-end: var(--space-xs); }
.mb-s { margin-block-end: var(--space-s); }
.mb-m { margin-block-end: var(--space-m); }
.mb-l { margin-block-end: var(--space-l); }
.mb-xl { margin-block-end: var(--space-xl); }

/* Muted text — an alias onto the same --muted token .field-hint
   already uses, for body copy that isn't a field hint. */
.text-muted { color: var(--muted); }
.text-danger { color: var(--danger-text, var(--danger)); }
.text-warning { color: var(--warning-text, var(--warning)); }
.text-success { color: var(--success-text, var(--success)); }
.text-info { color: var(--info-text, var(--info, var(--accent-text, var(--accent)))); }
/* In the app bar, transparent buttons and muted text sit on the bar's own
   fill, not --surface, so they take the bar's text colour (--app-bar-fg),
   falling back to the bar nav colour (--nav-item-fg) for themes that only
   set that. check.py holds both to 4.5:1 against the bar. Filled buttons
   keep their own fill and need nothing. */
.app-bar {
  --app-bar-text: var(--app-bar-fg, var(--nav-item-fg, var(--text)));
}
.app-bar .text-muted { color: var(--app-bar-text); }
.app-bar .breadcrumbs { --muted: var(--app-bar-text); --breadcrumb-current-fg: var(--app-bar-text); --breadcrumb-separator: color-mix(in srgb, var(--app-bar-text) 55%, transparent); }
.app-bar .btn-secondary,
.app-bar .btn-tertiary,
.app-bar .btn-ghost {
  --btn-fg: var(--app-bar-text);
  --btn-border: color-mix(in srgb, var(--app-bar-text) 45%, transparent);
  --btn-bg-hover: color-mix(in srgb, var(--app-bar-text) 14%, transparent);
  --btn-border-hover: var(--app-bar-text);
}
.app-bar :is(.btn-tertiary, .btn-ghost) { --btn-border: transparent; }

/* Screen-reader-only content — the standard visually-hidden-but-announced
   clip-rect pattern, not display:none (which screen readers also skip). */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* A themed scrollbar for any overflow container that wants one — opt-in
   via class rather than applied globally, since not every app wants its
   native scrollbar restyled. Reads the same browser-chrome tokens as the
   page scrollbar above, so one tuning point drives both; the size/radius
   fallbacks stay smaller than the page defaults, which is what an inner
   box wants. (Inner boxes can't inherit the page's ::-webkit-scrollbar
   rules — those pseudo-elements never inherit — so they opt in here.) */
.scroll {
  overflow: auto;
  scrollbar-width: var(--scrollbar-width, thin);
  scrollbar-color: var(--scrollbar-thumb, var(--border)) var(--scrollbar-track, transparent);
}
.scroll::-webkit-scrollbar { width: var(--scrollbar-size, 0.6rem); height: var(--scrollbar-size, 0.6rem); }
.scroll::-webkit-scrollbar-track { background: var(--scrollbar-track, transparent); }
.scroll::-webkit-scrollbar-thumb {
  background: var(--scrollbar-thumb, var(--border));
  border-radius: var(--scrollbar-radius, var(--radius));
}
@media (hover: hover) {
.scroll::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover, var(--accent)); }
}

/* Position helpers: pin a bar to the top or bottom of its scroller
   (sticky) or the viewport (fixed). Fixed bars cover content; pad the page
   by their height. */
.sticky-top { position: sticky; inset-block-start: 0; z-index: var(--z-sticky); }
.sticky-bottom { position: sticky; inset-block-end: 0; z-index: var(--z-sticky); }
.fixed-top { position: fixed; inset: 0 0 auto; z-index: var(--z-sticky); }
.fixed-bottom { position: fixed; inset: auto 0 0; z-index: var(--z-sticky); }

/* .figure — an image with its caption; .img-thumbnail — a framed image. */
.figure { display: inline-block; margin: 0 0 1rem; max-inline-size: 100%; }
.figure > :is(img, svg, video, picture) { border-radius: var(--figure-radius, var(--radius)); }
.figure > figcaption, .figure-caption { margin-block-start: 0.4em; font-size: 0.85em; color: var(--figure-caption-fg, var(--muted)); }
.img-thumbnail {
  max-inline-size: 100%;
  block-size: auto;
  padding: var(--thumbnail-padding, 0.25rem);
  background: var(--thumbnail-bg, var(--surface));
  border: var(--thumbnail-border-width, 1px) solid var(--thumbnail-border, var(--border));
  border-radius: var(--thumbnail-radius, var(--radius));
}

/* .vr — a vertical rule between inline items (a .cluster, a toolbar). */
.vr {
  display: inline-block;
  align-self: stretch;
  flex: none;
  inline-size: var(--vr-width, 1px);
  min-block-size: 1em;
  background: var(--vr-color, var(--border));
}

/* .container — a centered max-width column for sheets and readers. */
.container {
  width: min(100% - 2 * var(--container-pad, 1rem), var(--container-max, 64rem));
  margin-inline: auto;
}
.container-narrow { --container-max: var(--container-narrow-max, 32rem); }
.container-wide { --container-max: var(--container-wide-max, 72rem); }
/* .container.is-page adds the page's block padding. */
.container.is-page { padding-block: var(--container-pad-block, calc(1.5rem * var(--density))); }

/* .divide-y — hairlines between stacked children (lists, menus). */
.divide-y > * + * { border-block-start: 1px solid var(--hairline); }

/* .ratio — an aspect-ratio box for video/embeds; children fill it.
   Set `--ratio` directly or use a modifier (default 16/9). */
.ratio {
  position: relative;
  aspect-ratio: var(--ratio, 16 / 9);
  overflow: hidden;
}
.ratio > * {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.ratio-1x1 { --ratio: 1 / 1; }
.ratio-4x3 { --ratio: 4 / 3; }
.ratio-16x9 { --ratio: 16 / 9; }
.ratio-21x9 { --ratio: 21 / 9; }
.ratio-9x16 { --ratio: 9 / 16; max-block-size: var(--ratio-max-block, none); margin-inline: auto; }
/* .ratio.is-canvas — a scaled layout preview: children are not stretched;
   each .ratio-item sits at --x/--y and is --w/--h big, all in percent. */
.ratio.is-canvas > * { position: static; inset: auto; width: auto; height: auto; object-fit: initial; }
.ratio.is-canvas { background: var(--ratio-canvas-bg, var(--surface-2)); border: 1px solid var(--ratio-canvas-border, var(--hairline)); border-radius: var(--radius); }
.ratio.is-canvas > .ratio-item {
  position: absolute;
  inset-inline-start: calc(var(--x, 0) * 1%);
  inset-block-start: calc(var(--y, 0) * 1%);
  inline-size: calc(var(--w, 25) * 1%);
  block-size: calc(var(--h, 25) * 1%);
  box-sizing: border-box;
  overflow: hidden;
  padding: 0.15em 0.3em;
  border: 1px solid var(--ratio-item-border, var(--accent));
  border-radius: var(--radius-sm, 2px);
  background: var(--ratio-item-bg, color-mix(in srgb, var(--accent) 14%, var(--surface)));
  color: var(--ratio-item-fg, var(--text));
  font-size: var(--ratio-item-font-size, 0.65rem);
  line-height: 1.2;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* .thumb — a small framed preview image (a map, a logo, an upload). It is
   contained, not cropped; .is-cover crops to fill. -sm / -lg sizes. */
.thumb {
  display: inline-block;
  max-inline-size: var(--thumb-size, 8rem);
  max-block-size: calc(var(--thumb-size, 8rem) / 2);
  object-fit: contain;
  background: var(--thumb-bg, var(--surface-2));
  border: 1px solid var(--thumb-border, var(--border));
  border-radius: var(--thumb-radius, var(--radius));
  vertical-align: middle;
}
.thumb.is-cover { inline-size: var(--thumb-size, 8rem); block-size: calc(var(--thumb-size, 8rem) / 2); object-fit: cover; }
.thumb-sm { --thumb-size: 4rem; }
.thumb-lg { --thumb-size: 14rem; }

/* .glyph-legend — a wrapping row of glyph + word pairs (shortcut strips,
   editor affordances; .legend is the chart legend). The glyph is
   decorative: a <kbd> or .glyph-legend-mark, aria-hidden; words carry it. */
.glyph-legend { display: flex; flex-wrap: wrap; align-items: center; gap: 0.3em 1.1em; margin: 0; padding: 0; list-style: none; color: var(--glyph-legend-fg, var(--muted)); font-size: 0.85em; }
.glyph-legend > li { display: inline-flex; align-items: center; gap: 0.4em; }
.glyph-legend-mark { color: var(--glyph-legend-mark-fg, var(--accent-text, var(--accent))); font-weight: 700; }

/* Single- and multi-line truncation for cells, titles and log lines. */
.text-truncate {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.clamp-2,
.clamp-3 {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.clamp-2 { -webkit-line-clamp: 2; }
.clamp-3 { -webkit-line-clamp: 3; }

/* .stretched-link — makes a whole card clickable through one of its
   links. The card itself needs `position: relative` (or any positioned
   ancestor above the link) for the stretched area to anchor to. */
.stretched-link::after {
  content: "";
  position: absolute;
  inset: 0;
}

/* --- v4.1 additions: conversation, lists, charts, rows, pages ------------
 * Shapes the demo pages had each hand-rolled (and two had built twice), now
 * themeable through --<comp>-* tokens like every other component. */

/* .thread / .message — a conversation: chat, ticket replies, comments.
 *   <div class="thread">
 *     <div class="thread-divider">Today</div>
 *     <div class="message">
 *       <span class="avatar">AT</span>
 *       <div class="message-bubble">
 *         <div class="message-meta"><strong>Ava</strong> <time>09:41</time></div>
 *         <p>Morning all</p>
 *         <div class="message-reactions"><button class="badge badge-button">👍 3</button></div>
 *       </div>
 *     </div>
 *     <div class="message is-own">…</div>
 *     <div class="typing">Leo is typing…</div>
 *   </div>
 * .is-own mirrors the row (avatar on the end side) and reads the --message-own-*
 * tokens; --message-tail-radius is the corner nearest the avatar. */
.thread {
  display: flex;
  flex-direction: column;
  gap: var(--thread-gap, calc(0.75rem * var(--density)));
}
.message {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
}
.message.is-own { flex-direction: row-reverse; }
.message-bubble {
  min-width: 0;
  max-width: var(--message-max-width, 32rem);
  padding: var(--message-padding, 0.5rem 0.75rem);
  background: var(--message-bg, var(--surface));
  color: var(--message-fg, var(--text));
  border: var(--message-border-width, 1px) solid var(--message-border, var(--border));
  border-radius: var(--message-radius, var(--radius));
  border-start-start-radius: var(--message-tail-radius, var(--message-radius, var(--radius)));
  box-shadow: var(--message-shadow, none);
}
.message.is-own > .message-bubble {
  background: var(--message-own-bg, color-mix(in srgb, var(--accent) 12%, var(--message-bg, var(--surface))));
  color: var(--message-own-fg, var(--message-fg, var(--text)));
  border-color: var(--message-own-border, var(--message-border, var(--border)));
  box-shadow: var(--message-own-shadow, var(--message-shadow, none));
  border-start-start-radius: var(--message-radius, var(--radius));
  border-start-end-radius: var(--message-tail-radius, var(--message-radius, var(--radius)));
}
.message-bubble > * { margin-block: 0; }
.message-bubble > * + * { margin-block-start: 0.35rem; }
.message-meta { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.2em 0.5em; }
.message-meta > :is(time, small, .message-time) { font-size: 0.8em; color: var(--message-meta-fg, var(--muted)); }
.message.is-own .message-meta > :is(time, small, .message-time) { color: var(--message-own-meta-fg, var(--message-meta-fg, var(--muted))); }
.message-reactions { display: flex; flex-wrap: wrap; gap: 0.35rem; }

/* .thread-divider — a centred label between hairlines ("Today", "New"). */
.thread-divider {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  color: var(--thread-divider-fg, var(--muted));
  font-size: 0.8em;
  letter-spacing: var(--label-tracking, normal);
  text-transform: var(--label-transform, none);
}
.thread-divider::before,
.thread-divider::after {
  content: "";
  flex: 1;
  border-block-start: 1px solid var(--thread-divider-rule, var(--hairline));
}

/* .typing — "Leo is typing…" with three pulsing dots drawn in CSS. */
.typing {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.85em;
  color: var(--muted);
}
.typing::before {
  content: "";
  width: 1.6em;
  height: 0.4em;
  flex: none;
  background:
    radial-gradient(circle closest-side, var(--typing-dot, currentColor) 98%, transparent) 0 50% / 0.4em 0.4em no-repeat,
    radial-gradient(circle closest-side, var(--typing-dot, currentColor) 98%, transparent) 50% 50% / 0.4em 0.4em no-repeat,
    radial-gradient(circle closest-side, var(--typing-dot, currentColor) 98%, transparent) 100% 50% / 0.4em 0.4em no-repeat;
}
@media (prefers-reduced-motion: no-preference) {
  .typing::before { animation: typing-pulse 1.2s ease-in-out infinite; }
}

  :root[data-motion="always"] .typing::before { animation: typing-pulse 1.2s ease-in-out infinite; }

html[data-motion="reduced"] .typing::before { animation: none; }
@keyframes typing-pulse { 50% { opacity: 0.35; } }

/* .attachment — a file chip: icon, name, size, optional upload progress.
 *   <div class="attachment"><svg class="icon">…</svg>
 *     <span class="attachment-name">notes.pdf</span><span class="attachment-size">88 KB</span>
 *     <div class="progress">…</div></div> */
.attachment {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.3rem 0.6rem;
  padding: var(--attachment-padding, 0.45rem 0.7rem);
  background: var(--attachment-bg, var(--surface-2));
  color: var(--attachment-fg, var(--text));
  border: 1px solid var(--attachment-border, var(--border));
  border-radius: var(--attachment-radius, var(--radius));
}
.attachment-name { font-family: var(--font-mono); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.attachment-size { font-size: 0.8em; color: var(--muted); margin-inline-start: auto; }
.attachment > .progress { flex-basis: 100%; }

/* Two-line list rows: a .list-item holding .list-item-title stacks a title
 * over a meta line (sender + status, size + date). It keeps the list's
 * hover/selected styling. */
.list-item:has(> .list-item-title) {
  flex-direction: column;
  align-items: stretch;
  gap: 0.2rem;
}
/* .list-item-body stacks a title and meta line while the row's trailing
   buttons stay beside it: <li class="list-item"><div class="list-item-body">
   <span class="list-item-title">…</span><span class="list-item-meta">…</span></div><button …></li> */
.list-item-body { display: flex; flex: 1; flex-direction: column; gap: 0.2rem; min-inline-size: 0; }
.list-item-title { font-weight: var(--list-item-title-weight, 600); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.list-item-meta {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 0.25rem 0.75rem;
  font-size: 0.8em;
  color: var(--list-item-meta-fg, var(--muted));
}
/* On a selected row the meta line follows the row's own (selected) text
   colour; muted grey would vanish into an accent fill. */
.list-item:is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))) .list-item-meta,
.list-item:is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))) .list-item-meta .status { color: var(--list-item-meta-fg-active, inherit); }

/* .bar-chart — a CSS column chart; each child is a bar of height --value (0–1).
 *   <div class="bar-chart" role="img" aria-label="Revenue, 12 weeks, rising">
 *     <span style="--value:0.38"></span>…</div>
 * .is-highlight on a bar uses the second series colour. For anything richer,
 * draw with the Chart palette tokens instead. */
.bar-chart {
  display: flex;
  align-items: flex-end;
  gap: var(--bar-chart-gap, 0.5rem);
  height: var(--bar-chart-height, 8rem);
  padding-block-start: 0.5rem;
  border-block-end: var(--bar-chart-axis-width, 1px) solid var(--bar-chart-axis, var(--chart-grid, var(--hairline)));
}
.bar-chart > * {
  flex: 1;
  min-width: 0;
  height: calc(var(--value, 0) * 100%);
  background: var(--bar-chart-bar, var(--chart-series-1, var(--accent)));
  border-radius: var(--bar-chart-radius, 2px 2px 0 0);
  box-shadow: var(--bar-chart-shadow, none);
}
.bar-chart > .is-highlight { background: var(--bar-chart-bar-alt, var(--chart-series-2, var(--success))); }

/* .value-row — label | control | value, for setpoints, goals and gains.
 *   <div class="value-row"><label class="label" for="g">Gain</label>
 *     <input class="slider" id="g" type="range"><span class="readout readout-sm">62%</span></div> */
.value-row {
  display: grid;
  grid-template-columns: var(--value-row-label, 8rem) minmax(0, 1fr) var(--value-row-value, 4.5rem);
  align-items: center;
  gap: 0.75rem;
}
.value-row > .label { margin: 0; }
.value-row > :last-child { justify-self: end; font-variant-numeric: tabular-nums; }

/* .meter-bank — vertical meters side by side, each over its label (tank
 * levels, bus meters). Each child is a column: the .meter-v grows, the
 * label sits under it. */
.meter-bank {
  display: flex;
  justify-content: space-around;
  align-items: stretch;
  gap: 1rem;
  height: var(--meter-bank-height, 11rem);
}
.meter-bank > * {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.8em;
  color: var(--muted);
}
.meter-bank .meter-v { flex: 1; }

/* .table.is-matrix — a grid of checkboxes or marks (routing, permissions):
 * every column but the first is centred. */
.table.is-matrix :is(th, td) { text-align: center; }
.table.is-matrix :is(th, td):first-child { text-align: start; }

/* .toolbar.is-plain — the toolbar row without its box: filters and bulk
 * actions above a list. */
.toolbar.is-plain { padding: 0; background: none; border: 0; }

/* .hero / .band — marketing page structure. A .band is a full-width page
 * section; .is-tinted paints it. .band-head centres a heading and its lede. */
.hero {
  text-align: center;
  padding: calc(var(--space-2xl) * 4 / 3 * var(--density)) var(--space-m) calc(var(--space-2xl) * var(--density));
  background: var(--hero-bg, transparent);
  color: var(--hero-fg, inherit);
}
.hero :is(h1, h2) + p { max-width: 38rem; margin: 0.75rem auto 1.5rem; color: var(--hero-lede-fg, var(--muted)); }
.band { padding-block: calc(var(--space-2xl) * var(--density)); }
.band.is-tinted { background: var(--band-tint-bg, var(--surface)); color: var(--band-tint-fg, inherit); }
.band-head { text-align: center; max-width: 38rem; margin: 0 auto 2rem; }
.band-head > * { margin-block: 0; }
.band-head > p { margin-block-start: 0.5rem; color: var(--muted); }

/* .price — a plan's big figure with an optional .price-period.
 * .feature-list — a plan's features, ticked; .is-excluded crossed out. */
.price {
  font-size: var(--price-size, 2.2rem);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--price-fg, inherit);
}
.price-period { font-size: 0.4em; font-weight: 400; color: var(--muted); }
.feature-list { list-style: none; margin: 1rem 0; padding: 0; text-align: start; }
.feature-list > li { display: flex; gap: 0.5em; padding: 0.35rem 0; }
.feature-list > li + li { border-block-start: 1px solid var(--hairline); }
.feature-list > li::before { content: var(--feature-mark, "✓"); color: var(--feature-mark-fg, var(--success-text, var(--success))); font-weight: 700; }
.feature-list > li.is-excluded { color: var(--muted); }
.feature-list > li.is-excluded::before { content: var(--feature-mark-off, "✕"); color: var(--muted); }

/* --- Layout helpers ------------------------------------------------------
 * .push — eat the free space before this item (end-align it in a flex row:
 *   a nav's trailing controls, a list item's count).
 * .columns — fixed-ratio columns: style="--columns: 2fr 1fr" (any
 *   grid-template-columns value). Up to 900px (the touch tiers) they become
 *   --columns-narrow (default one column); inside a container (.modal,
 *   .drawer, .cq) narrower than 480px they become one column too.
 * .is-auto on .select/.input — size to content instead of filling the row. */
.push { margin-inline-start: auto; }
.columns {
  display: grid;
  grid-template-columns: var(--columns, 1fr 1fr);
  gap: var(--columns-gap, calc(var(--space-m) * 1.25 * var(--density)));
  align-items: start;
}
.columns > * { min-width: 0; }
@media (max-width: 900px) {
  .columns { grid-template-columns: var(--columns-narrow, minmax(0, 1fr)); }
}
@container (max-width: 480px) {
  .columns { grid-template-columns: minmax(0, 1fr); }
}
:is(.select, .input).is-auto { width: auto; flex: none; }

/* --- Print baseline ------------------------------------------------------
 *
 * No theme opts into this — it's the universal floor every theme gets for
 * free just by loading core. Themes are designed for screens: dark
 * backgrounds, glow, animated chrome. None of that belongs on paper — it
 * either wastes ink, prints as a solid black rectangle on a non-color
 * printer, or (worst case) makes the text illegible because the browser
 * kept a light-on-dark palette but dropped the background.
 *
 * This block only handles what's true for EVERY theme: kill backgrounds,
 * force black text, stop animation, and hide the decorative app-rail and
 * the interactive-only controls in the app-bar/app-status (a button or nav
 * toggle a reader can't click on paper anyway). A theme with its own
 * print-hostile decoration (an animated canvas-like background, a glowing
 * emblem, glass gradients, …) adds a small `@media print` block of its own
 * in its theme.css — see CONTRACT.md's Print section and, for a worked
 * example, themes/matrix/theme.css. */
@media print {
  /* `color: #000 !important` on html/body alone doesn't reach heading/
     accent/status text: inheritance only supplies a value when NO rule
     targets the element directly, and importance doesn't change that — a
     theme's `h1 { color: var(--accent) }` (no !important) still wins
     over an INHERITED value from an ancestor, however important that
     ancestor's own color is. The only way to actually force every element's
     own text black is to target every element directly, which is what this
     universal rule (not just the animation/shadow reset below) is for. */
  * ,
  *::before,
  *::after {
    animation: none !important;
    transition: none !important;
    text-shadow: none !important;
    color: #000 !important;
  }

  html, body {
    /* A dark theme sets `color-scheme: dark` at the root, which makes the
       browser paint its own dark default canvas behind anything left
       transparent — background: none alone still rendered as a black page,
       because that canvas fill is a UA default keyed off color-scheme, not
       an author background. Forcing light here is what actually turns the
       page white. */
    color-scheme: light !important;
    background: none !important;
    color: #000 !important;
  }

  /* Component surfaces almost always paint their own background/color from
     theme tokens; on paper we want plain black text on white, not whatever
     a dark theme's surface token happened to be. */
  .app,
  .app-bar,
  .app-main,
  .app-status,
  .card,
  .panel,
  .surface,
  .table,
  .modal,
  .btn,
  .badge,
  .alert {
    background: none !important;
    color: #000 !important;
    box-shadow: none !important;
    text-shadow: none !important;
    border-color: #999 !important;
  }

  /* The app-rail is decorative-only chrome (see layout.css) with no
     readable content of its own — it never belongs on a printed page.
     Neither does a drawer: its content duplicates a settings surface the
     reader can't interact with on paper. */
  .app-rail,
  .drawer { display: none !important; }

  /* Interactive-only controls inside the app-bar/app-status make no sense
     on paper (nothing to click), but the bar's brand/title text and the
     status footer's readouts may be meaningful, so only the controls are
     hidden, not the whole bar/status. */
  .app-bar button,
  .app-bar .btn,
  .app-bar .btn-icon,
  .app-bar [role="button"],
  .app-bar .segmented,
  .app-bar .tabs,
  .app-status button,
  .app-status .btn,
  .app-status .btn-icon,
  .app-status [role="button"] {
    display: none !important;
  }
}

/* --- v5 touch (PLAN.md §4) ---------------------------------------------
 * --tap-min is the smallest hit area any control may have: 24px on desktop
 * (WCAG 2.5.8 AA), 44px on the mobile and tablet tiers, on any device with
 * a coarse pointer (a touch laptop), or when the user asks for it
 * (data-pointer="touch"). Controls grow to it with min sizes, so a theme's
 * look is kept and only the hit area is enforced. */
:root { --tap-min: 24px; }
@media (max-width: 900px), (any-pointer: coarse) {
  :root { --tap-min: 44px; }
}
html[data-pointer="touch"] { --tap-min: 44px; }

/* Class-level specificity (0,1,0), placed after the components: beats a
   component's own base min sizes, and a component file loaded after core
   (core/components/) can still set its own. */
:is(.btn, .btn-icon, .btn-close, .btn-min, .btn-max, .toggle-btn, .badge-button,
    .nav-item, .tab, .segmented-item, .pagination-item, .dropdown-item,
    .context-menu-item, .accordion-trigger, .scrollspy-link, .taskbar-task,
    .taskbar-start, .nav-toggle, .key, .list-item:where(a, button, label)),
:where(.context-menu, .menubar) li > :is(button, a) {
  min-block-size: var(--tap-min);
  touch-action: manipulation;
}
/* Short labels ("EQ", "L1") too: toggles, tabs and segments. */
:is(.btn-icon, .btn-close, .btn-min, .btn-max, .pagination-item, .nav-toggle, .taskbar-task, .badge-button,
    .toggle-btn, .tab, .segmented-item) {
  min-inline-size: var(--tap-min);
}
:is(.input, .select) { min-block-size: var(--tap-min); }
/* Checkboxes and radios: the label is the target. */
:where(.check, .radio) {
  min-block-size: var(--tap-min);
  touch-action: manipulation;
}
/* Switches and sliders keep their drawn size: an invisible box centred on
   the control makes up the hit area (growing the box itself turned the
   switch thumb into a ball that spilled out of its track). */
.switch { touch-action: manipulation; }
.switch::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: max(100%, var(--tap-min));
  height: max(100%, var(--tap-min));
  transform: translate(-50%, -50%);
}
.slider { touch-action: none; }
@media (max-width: 900px), (any-pointer: coarse) {
  /* The range input's own box is both its track and its target. On touch
     it grows to the target height; padding keeps the drawn track the same
     thin line in the middle (the border can't follow it, so it hides). */
  .slider:not(.is-range > .slider) {
    height: var(--tap-min);
    padding-block: calc((var(--tap-min) - var(--slider-height, 0.35rem)) / 2);
    background-clip: content-box;
    border-color: transparent;
  }
}
/* Inline text links inside running text are exempt (WCAG 2.5.8); links
   that stand alone as navigation are not. */
.breadcrumb-item:not(.is-current) {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-block-size: var(--tap-min);
  min-inline-size: var(--tap-min);
}
/* A native colour well (e.g. a theme-tint picker) is a target too. */
input[type="color"] { min-block-size: var(--tap-min); min-inline-size: var(--tap-min); }

/* iOS Safari zooms the page when a focused field's text is under 16px. */
@media (any-pointer: coarse) {
  :is(.input, .select, .textarea) { font-size: max(16px, 1em); }
}

/* Small visuals, full-size hit areas: the dot or box stays the theme's size
   and transparent padding (clipped out of the background) makes up the
   target. */
/* Carousel dots: a full-size hit area round the small dot, at every tier. */
  /* Sized as border-box on purpose: apps commonly reset box-sizing with an
     unlayered rule, which would beat anything set here. */
  .carousel-nav > a:not(.carousel-prev, .carousel-next) {
    min-width: var(--tap-min);
    height: var(--tap-min);
    padding: calc((var(--tap-min) - 0.7em) / 2);
    background-clip: content-box;
  }
  .carousel-nav > :is(.carousel-prev, .carousel-next) {
    min-width: var(--tap-min);
    height: var(--tap-min);
    line-height: var(--tap-min);
  }

@media (max-width: 900px), (any-pointer: coarse) {
  /* A native checkbox draws itself at whatever size it is given, so its
     hit area can't be bigger than the box. A bare one (a table's select
     column) gets 24px (WCAG AA); for the full touch size, wrap it in a
     label with a hidden name, which core sizes below:
       <label class="check"><input type="checkbox" class="checkbox">
         <span class="visually-hidden">Select row</span></label> */
  td > :is(.checkbox, .radio):only-child,
  th > :is(.checkbox, .radio):only-child {
    width: 24px;
    height: 24px;
  }
  .check:has(> .visually-hidden:last-child) {
    min-inline-size: var(--tap-min);
    justify-content: center;
  }
}

/* --- v5 top layer and nesting (PLAN.md §6) -------------------------------
 * A floating surface opened with the popover attribute lives in the top
 * layer: no overflow can clip it and no stacking context can trap it. The
 * in-flow `position: absolute` above is for the .anchor pattern only; the
 * top-layer form is fixed, and stays hidden until it opens even where core
 * gives the component a display of its own. */
:is(.popover, .context-menu, .dropdown)[popover] {
  position: fixed;
  /* No anchor positioning: centred in the viewport, like a dialog. */
  inset: 0;
  margin: auto;
  width: fit-content;
  height: fit-content;
  max-width: min(22rem, calc(100vw - 2rem));
  max-height: calc(100dvh - 2rem);
  overflow: auto;
  color: var(--text);
}
/* Anchor positioning (not yet Baseline): sit beside the button that opened
   it, flipping at the screen edges. A popovertarget button is the
   popover's implicit anchor. */
@supports (position-area: block-end) {
  :is(.popover, .context-menu, .dropdown)[popover] {
    inset: auto;
    margin: 0.25rem 0;
    position-area: block-end span-inline-end;
    position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
  }
}
/* A context menu shown at the pointer: the app sets --x/--y. */
.context-menu[popover].is-at-pointer {
  inset: auto;
  position-area: none;
  left: var(--x, 0);
  top: var(--y, 0);
  margin: 0;
}
:is(.popover, .context-menu, .dropdown, .toast-region)[popover]:not(:popover-open) { display: none; }

/* Toasts that must show over an open modal live in the top layer too:
   <div class="toast-region" popover="manual">. Without the browser's
   default popover box. */
.toast-region[popover] {
  inset: 1rem 1rem auto auto;
  inset-inline-start: auto;
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
  overflow: visible;
}
/* In the .app shell the region starts below the app bar, so toasts don't
   cover its buttons: anchored to the bar where anchor positioning works,
   else --app-bar-height (themes that fix the bar's height publish it).
   --toast-offset-top adds room for extra sticky chrome. */
.app .app-bar { anchor-name: --app-bar-anchor; }
.app .toast-region,
.app .toast-region[popover] {
  top: calc(var(--app-bar-height, 3.5rem) + env(safe-area-inset-top, 0px) + var(--toast-offset-top, 1rem));
  top: max(var(--toast-offset-top, 1rem), calc(anchor(--app-bar-anchor bottom, var(--app-bar-height, 3.5rem)) + var(--toast-offset-top, 1rem)));
}
/* XL: toasts line up with the content's edge, not the far screen edge. */
@media (min-width: 1801px) {
  .toast-region { inset-inline-end: calc((100vw - var(--app-max, 1800px)) / 2 + 1rem); }
}

/* An anchored surface sizes to its content, not to the trigger it sits
   under (which wrapped a menu one word per line). */
.anchor > :is(.popover, .context-menu, .dropdown) {
  width: max-content;
  max-width: min(20rem, calc(100vw - 2rem));
}

/* The drawer is a surface: it pads its content and publishes the pad for
   a .panel-header inside it. */
.drawer {
  --surface-pad: var(--drawer-pad, var(--space-m));
  padding: var(--surface-pad);
}

/* Sticky headers in a padded scroller (a modal) stick to the scroller's
   edge, covering the padding band rather than letting rows show through
   it. Unpadded scrollers reset it. */
.modal { --sticky-top: calc(-1 * var(--surface-pad)); }
:is(.scroll, .table-wrap) { --sticky-top: 0px; }
.table.is-sticky thead th { top: var(--sticky-top, 0px); }

/* A scroll region with a height cap, as an inline API like --columns:
   <div class="scroll" style="--scroll-max: 12rem">. */
.scroll { max-block-size: var(--scroll-max, none); }

/* A table's own horizontal scroller, so a wide table scrolls inside its
   panel, card or modal instead of being cut off or widening the page. */
.table-wrap {
  overflow-x: auto;
  max-inline-size: 100%;
  scrollbar-width: thin;
}

/* Long unbroken text (URLs, hashes, IDs) wraps instead of spilling into the
   next column. Table cells use break-word so column widths still come from
   their content; badges truncate. */
:is(.panel-header, .modal-header, .card, .panel, .toast, .alert, .popover) { overflow-wrap: anywhere; }
/* break-word (not anywhere) where a box sizes from its content: it still
   breaks a word too long for the line, but doesn't let the box shrink to
   one letter per line (a squeezed title beside header controls, a button
   label in a narrow cell or toast). */
:is(.table td, .table th, .panel-header:has(:is(.input, .select, .btn, .input-group))) { overflow-wrap: break-word; }
.badge { max-inline-size: 100%; }
/* A badge-button keeps its tap-min width (it is a target, not a label). */
.badge:not(.badge-button) { min-inline-size: 0; }
.btn { max-inline-size: 100%; overflow-wrap: break-word; }

/* --- v5 nested menus (PLAN.md §8) ----------------------------------------
 * Nested <ul> lists are the structure: a <li class="has-submenu"> holds its
 * own <ul role="menu">. Items share .context-menu-item's look (above).
 *   Desktop: submenus fly out to the side on hover or keyboard focus.
 *            .is-flip opens them to the other side (near the right edge).
 *   Touch tiers: they expand inline under their item; a tap focuses the
 *            item, which opens it. */
.context-menu li { position: relative; }
:is(.context-menu, .menubar) .has-submenu > :is(button, a)::after {
  content: var(--submenu-glyph, "\203A");
  margin-inline-start: auto;
  padding-inline-start: 1rem;
  opacity: 0.7;
}
:is(.context-menu, .menubar) .has-submenu > ul { display: none; }
@media (hover: hover) {
:is(.context-menu, .menubar) .has-submenu:is(:hover, :focus-within) > ul { display: block; }
}
@media (min-width: 901px) and (hover: hover) {
  .context-menu .has-submenu > ul {
    position: absolute;
    top: calc(-0.35rem - 1px);
    inset-inline-start: calc(100% + 0.35rem);
    min-width: 10rem;
    width: max-content;
    max-width: 20rem;
    padding: 0.35rem;
    background: var(--context-menu-bg, var(--dropdown-bg, var(--surface-2)));
    border: var(--context-menu-border-width, 1px) solid var(--context-menu-border, var(--dropdown-border, var(--border)));
    border-radius: var(--context-menu-radius, var(--dropdown-radius, var(--radius)));
    box-shadow: var(--context-menu-shadow, var(--panel-shadow, 0 2px 12px rgba(0, 0, 0, 0.3)));
  }
  .context-menu.is-flip .has-submenu > ul {
    inset-inline-start: auto;
    inset-inline-end: calc(100% + 0.35rem);
  }
  /* A submenu must stay visible while the pointer crosses the gap to it. */
  .context-menu .has-submenu > ul::before {
    content: "";
    position: absolute;
    inset-block: 0;
    inset-inline-start: -0.5rem;
    inline-size: 0.5rem;
  }
  .context-menu.is-flip .has-submenu > ul::before { inset-inline-start: auto; inset-inline-end: -0.5rem; }
}
/* With anchor positioning (not yet Baseline) each submenu anchors to its
   own item and flips to the other side at the screen edge by itself. */
@supports (position-area: inline-end) and (anchor-scope: all) {
  @media (min-width: 901px) and (hover: hover) {
    .context-menu .has-submenu { anchor-scope: --submenu; anchor-name: --submenu; }
    .context-menu .has-submenu > ul {
      /* fixed: the area it may flip within is the viewport, not its item */
      position: fixed;
      position-anchor: --submenu;
      inset: auto;
      margin-inline: 0.35rem;
      margin-block-start: calc(-0.35rem - 1px);
      position-area: inline-end span-block-end;
      position-try-fallbacks: flip-inline, flip-block, flip-inline flip-block;
    }
    .context-menu .has-submenu > ul::before { display: none; }
  }
}
@media (max-width: 900px), (hover: none) {
  :is(.context-menu, .menubar) .has-submenu > ul {
    margin-inline-start: 0.75rem;
    padding-inline-start: 0.35rem;
    border-inline-start: 1px solid var(--context-menu-border, var(--hairline));
  }
  :is(.context-menu, .menubar) .has-submenu:focus-within > :is(button, a)::after { transform: rotate(90deg); }
}
/* A menu popover holding flyouts must not clip them. */
.context-menu[popover]:has(.has-submenu) { overflow: visible; }

/* --- v5 containers (PLAN.md §2) --------------------------------------------
 * Components respond to the box they sit in through @container rules. A size
 * container also applies layout containment: it becomes the containing block
 * for position:fixed descendants and a stacking context. So core makes only
 * boxes that are already isolated into containers (.modal, .drawer), and
 * .cq opts any other box in: use it on a box whose width comes from outside
 * (not one that shrinks to its content) and that holds no fixed overlays or
 * surfaces that must spill out of it. */
:is(.modal, .drawer, .cq) { container-type: inline-size; }

/* Narrow boxes: a value row puts its label on its own line. */
@container (max-width: 480px) {
  .value-row { grid-template-columns: minmax(0, 1fr) var(--value-row-value, 4.5rem); }
  .value-row > :first-child { grid-column: 1 / -1; }
}
@media (max-width: 480px) {
  .value-row { grid-template-columns: minmax(0, 1fr) var(--value-row-value, 4.5rem); }
  .value-row > :first-child { grid-column: 1 / -1; }
}

/* --- v5 integration fixes ------------------------------------------------ */
/* Nav items centre their label at the touch target height. */
.nav-item { display: inline-flex; align-items: center; }
/* Touch tiers: tabs never push past their box, even when a theme makes
   .tabs inline-flex. */
@media (max-width: 900px) { .tabs { max-inline-size: 100%; } }
/* A panel header holding controls wraps instead of squeezing its title to
   one letter per line. */
.panel-header:has(:is(.input, .select, .btn, .input-group)) {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2xs) var(--space-xs);
}
/* A field row keeps its label when it holds a slider. */
.field-row > .label { min-inline-size: min(8rem, 40%); }
.field-row > .slider { flex: 1 1 8rem; min-inline-size: 0; }
/* Windows and inline modals never get wider than their box. */
.modal { max-inline-size: 100%; }
/* Indeterminate progress: a sweeping segment (static with reduced motion). */
progress.progress:indeterminate {
  background-image: linear-gradient(90deg, transparent, var(--progress-fill, var(--accent)), transparent);
  background-size: 40% 100%;
  background-repeat: no-repeat;
  background-position: 50% 0;
}
progress.progress:indeterminate::-webkit-progress-bar { background: transparent; }
progress.progress:indeterminate::-moz-progress-bar { background: transparent; }
@media (prefers-reduced-motion: no-preference) {
  html:not([data-motion="reduced"]) progress.progress:indeterminate {
    animation: progress-sweep 1.4s ease-in-out infinite;
  }
}

  html[data-motion="always"]:not([data-motion="reduced"]) progress.progress:indeterminate {
    animation: progress-sweep 1.4s ease-in-out infinite;
  }

@keyframes progress-sweep {
  from { background-position: -40% 0; }
  to { background-position: 140% 0; }
}
/* An absolutely positioned child (a visually-hidden label) stays inside its
   scroller instead of escaping it and widening the page. */
:is(.table-wrap, .scroll) { position: relative; }
/* A checkbox or radio wrapped in a label with a hidden name gets the full
   target width at every tier. */
.check:has(> .visually-hidden:last-child) { min-inline-size: var(--tap-min); justify-content: center; }
/* A field-row's label for a switch or checkbox is the target: it gets the
   target height without changing the row's look. */
.field-row:has(.switch, .check, .checkbox) > .label {
  padding-block: max(0px, calc((var(--tap-min) - 1lh) / 2));
}
/* Action links inside toasts and alerts are targets, not inline text. */
:where(.toast, .alert) > a { display: inline-flex; align-items: center; min-block-size: var(--tap-min); }
/* Touch tiers: buttons reach the target width too (a "7d" segment in a
   button group was 34px wide). */
@media (max-width: 900px), (any-pointer: coarse) {
  :where(.btn) { min-inline-size: var(--tap-min); }
}
/* Hover feedback for controls that had none in v4 (found by a hover sweep
   across the Windows themes; every theme was affected). */
@media (hover: hover) {
  .accordion-trigger:hover { background: var(--accordion-trigger-bg-hover, color-mix(in srgb, var(--text) 6%, transparent)); }
  .breadcrumb-item:not(.is-current):hover { color: var(--breadcrumb-fg-hover, var(--link, var(--accent))); text-decoration: underline; }
  :is(a.list-item, button.list-item, .list-item:has(> a:only-child)):hover {
    background: var(--list-item-bg-hover, var(--row-hover-bg, color-mix(in srgb, var(--text) 6%, transparent)));
  }
  .key:hover { filter: var(--key-hover-filter, brightness(1.15)); }
}

/* --- v5 CSS-only selection (CONTRACT.md "CSS-only selection") -------------
 * A hidden radio (or checkbox) inside an item selects it with no JS. The
 * build (scripts/radio_state.py) makes every .is-active / [aria-selected]
 * style, in core and in each theme, also match an item whose own input is
 * checked, so selection looks the same either way. The radio stays in the
 * tab order: arrow keys move through a group, as radios always do.
 *   <label class="list-item"><input type="radio" name="view" checked> Weekly</label> */
:is(.list-item, .nav-item, .tab, .pagination-item, .segmented-item, .dropdown-item, .badge-button) > input:is([type="radio"], [type="checkbox"]) {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  overflow: hidden;
  clip-path: inset(50%);
  opacity: 0;
}
:is(.list-item, .nav-item, .tab, .pagination-item, .dropdown-item):has(> input:is([type="radio"], [type="checkbox"])) { position: relative; }
:is(.list-item, .nav-item, .tab, .pagination-item, .dropdown-item):has(> input:focus-visible) {
  outline: 2px solid var(--focus);
  outline-offset: -2px;
}

/* Tabs with panels, no JS: the checked tab shows the panel in the same
   position (panel n follows tab n; up to 12).
   <div class="tabset">
     <div class="tabs"><label class="tab"><input type="radio" name="t" checked>One</label>…</div>
     <div class="tab-panel">…</div><div class="tab-panel">…</div>
   </div> */
.tabset > .tab-panel { padding-block-start: var(--tab-panel-pad, var(--space-m)); }
/* Hide every panel but the chosen one, so the shown panel keeps its own
   display (a .stack, a grid…). */
.tabset > .tab-panel:not(:is(
  .tabset:has(> .tabs > .tab:nth-child(1) > input:checked) > .tab-panel:nth-child(2),
  .tabset:has(> .tabs > .tab:nth-child(2) > input:checked) > .tab-panel:nth-child(3),
  .tabset:has(> .tabs > .tab:nth-child(3) > input:checked) > .tab-panel:nth-child(4),
  .tabset:has(> .tabs > .tab:nth-child(4) > input:checked) > .tab-panel:nth-child(5),
  .tabset:has(> .tabs > .tab:nth-child(5) > input:checked) > .tab-panel:nth-child(6),
  .tabset:has(> .tabs > .tab:nth-child(6) > input:checked) > .tab-panel:nth-child(7),
  .tabset:has(> .tabs > .tab:nth-child(7) > input:checked) > .tab-panel:nth-child(8),
  .tabset:has(> .tabs > .tab:nth-child(8) > input:checked) > .tab-panel:nth-child(9),
  .tabset:has(> .tabs > .tab:nth-child(9) > input:checked) > .tab-panel:nth-child(10),
  .tabset:has(> .tabs > .tab:nth-child(10) > input:checked) > .tab-panel:nth-child(11),
  .tabset:has(> .tabs > .tab:nth-child(11) > input:checked) > .tab-panel:nth-child(12),
  .tabset:has(> .tabs > .tab:nth-child(12) > input:checked) > .tab-panel:nth-child(13)
)) {
  display: none;
}

/* A drawer opened with the popover attribute: <button popovertarget="d">
   opens it, Esc or a click outside closes it, no JS. */
.drawer[popover] {
  margin: 0;
  inset-inline-start: auto;
  height: 100dvh;
  max-height: none;
  border-block: 0;
  border-inline-end: 0;
}
.drawer[popover]:popover-open { transform: none; visibility: visible; }
.drawer[popover]::backdrop { background: var(--overlay-bg, rgba(0, 0, 0, 0.6)); }

.drawer.is-start[popover] { inset-inline-start: 0; inset-inline-end: auto; border-inline-end: var(--drawer-border-width, 1px) solid var(--drawer-border, var(--border)); border-inline-start: 0; }
/* --- v5 audio controls and visualisers (AudioCN parity) ---------------------
 * Meter RMS and gradient, stereo meter pairs, a clip indicator, bar and
 * electric visualisers, live / smooth / electric traces, a parameter
 * slider, a centre-fill pan slider, device-select states, channel rows,
 * sound pads and a soundboard, scrubber buffering and an area spectrum.
 * Builds on core's .meter, .readout, .key, .knob, .fader, .strip, .mixer,
 * .slider and .lamp, media.css's .scrubber and .waveform, and metering.css's
 * .rta. Values arrive as 0–1 custom properties the app sets (--level,
 * --progress, --value, --buffered); assets/js/controls.js keeps the ones
 * tied to inputs in step. Every look reads a --<component>-* token that
 * falls back to the base palette. See docs/components/audio.md. */

/* --- .meter extras: RMS layer, smooth gradient, stereo pair ----------------
 *   <div class="meter" style="--meter-level:78%; --meter-rms:52%; --meter-peak:90%">
 *     <div class="meter-fill"></div><div class="meter-rms"></div><div class="meter-peak"></div>
 *   </div>
 * .meter-rms is the average level drawn as a solid core inside the peak
 * fill. .is-gradient blends the zones instead of hard bands. .meter-pair
 * stacks a left and right meter (or sits them side by side with .meter-v). */
.meter-rms {
  position: absolute;
  inset: 30% auto 30% 0;
  width: var(--meter-rms, 0%);
  background: var(--meter-rms-fg, color-mix(in srgb, var(--text) 55%, transparent));
  transition: width 0.12s linear;
}
.meter-v .meter-rms {
  inset: auto 30% 0 30%;
  width: auto;
  height: var(--meter-rms, 0%);
  transition: height 0.12s linear;
}
.meter.is-gradient {
  --meter-fill-image: linear-gradient(90deg, var(--meter-low, var(--success)) var(--meter-warn-at, 70%), var(--meter-mid, var(--warning)) var(--meter-peak-at, 90%), var(--meter-high, var(--danger)));
  --meter-fill-image-v: linear-gradient(0deg, var(--meter-low, var(--success)) var(--meter-warn-at, 70%), var(--meter-mid, var(--warning)) var(--meter-peak-at, 90%), var(--meter-high, var(--danger)));
}
.meter-pair { display: grid; gap: var(--meter-pair-gap, 2px); }
.meter-pair:has(> .meter-v) { grid-auto-flow: column; justify-content: start; block-size: 100%; }

/* --- .clip — a clip light that holds, with a count -------------------------
 *   <button class="clip" type="button" aria-pressed="true" aria-label="Clipped 3 times, reset">CLIP <span class="clip-count">3</span></button>
 * aria-pressed="true" (or .is-clipped) lights it; the app holds it after a
 * clip and clears it when the user presses it. */
.clip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.3em;
  min-inline-size: var(--clip-width, 3.2rem);
  min-block-size: 24px;
  padding: 0.15em 0.5em;
  border: 1px solid var(--clip-border, var(--border));
  border-radius: var(--clip-radius, var(--radius));
  background: var(--clip-off, var(--surface-2));
  color: var(--clip-off-fg, var(--muted));
  font: 700 0.7rem/1 var(--font-mono);
  letter-spacing: 0.06em;
}
.clip:is([aria-pressed="true"], .is-clipped) {
  border-color: var(--clip-on, var(--danger));
  background: var(--clip-on, var(--danger));
  color: var(--clip-on-fg, var(--on-danger, #fff));
  box-shadow: 0 0 var(--clip-glow, 0.5rem) color-mix(in srgb, var(--clip-on, var(--danger)) 60%, transparent);
}
.clip-count { font-variant-numeric: tabular-nums; }
.clip-count:empty { display: none; }

/* --- .visualizer — bars driven by frequency bands --------------------------
 *   <div class="visualizer" role="img" aria-label="Microphone level">
 *     <i style="--level:.4"></i><i style="--level:.7"></i>…
 *   </div>
 * Bars rise from the bottom (--level 0–1); .is-mirror grows them from the
 * centre. .is-idle breathes gently while there is no signal, .is-loading
 * runs a wave across them, .is-electric draws them as glowing filaments.
 * Give it a text alternative; the bars are decoration. */
.visualizer {
  display: flex;
  align-items: flex-end;
  gap: var(--visualizer-gap, 3px);
  block-size: var(--visualizer-height, 4rem);
  color: var(--visualizer-fg, var(--accent));
}
.visualizer > * {
  flex: 1 1 0;
  min-inline-size: 1px;
  block-size: max(var(--visualizer-min, 3px), calc(var(--level, 0) * 100%));
  border-radius: var(--visualizer-radius, 2px);
  background: currentColor;
  transition: block-size var(--visualizer-smoothing, 80ms) linear;
}
.visualizer.is-mirror { align-items: center; }
.visualizer:is(.is-idle, .is-loading) > * { --level: 0.12; }
@keyframes visualizer-idle { 50% { opacity: 0.35; } }
@keyframes visualizer-wave { 0%, 100% { block-size: var(--visualizer-min, 3px); } 50% { block-size: 60%; } }
@media (prefers-reduced-motion: no-preference) {
  .visualizer.is-idle > * { animation: visualizer-idle 2.4s ease-in-out infinite; }
  .visualizer.is-loading > * { animation: visualizer-wave 1.1s ease-in-out infinite; }
  .visualizer:is(.is-idle, .is-loading) > :nth-child(4n + 2) { animation-delay: 0.15s; }
  .visualizer:is(.is-idle, .is-loading) > :nth-child(4n + 3) { animation-delay: 0.3s; }
  .visualizer:is(.is-idle, .is-loading) > :nth-child(4n + 4) { animation-delay: 0.45s; }
}

  :root[data-motion="always"] .visualizer.is-idle > * { animation: visualizer-idle 2.4s ease-in-out infinite; }
  :root[data-motion="always"] .visualizer.is-loading > * { animation: visualizer-wave 1.1s ease-in-out infinite; }
  :root[data-motion="always"] .visualizer:is(.is-idle, .is-loading) > :nth-child(4n + 2) { animation-delay: 0.15s; }
  :root[data-motion="always"] .visualizer:is(.is-idle, .is-loading) > :nth-child(4n + 3) { animation-delay: 0.3s; }
  :root[data-motion="always"] .visualizer:is(.is-idle, .is-loading) > :nth-child(4n + 4) { animation-delay: 0.45s; }

/* Electric: a white-hot core in a coloured glow that crackles. */
.visualizer.is-electric > * {
  flex-grow: 0;
  flex-basis: var(--visualizer-filament, 2px);
  margin-inline: auto;
  background: var(--visualizer-core, #fff);
  box-shadow: 0 0 3px currentColor, 0 0 9px currentColor, 0 0 18px color-mix(in srgb, currentColor 60%, transparent);
}
.visualizer.is-electric { justify-content: space-around; }
@keyframes visualizer-crackle { 0%, 100% { opacity: 1; } 47% { opacity: 0.8; } 50% { opacity: 1; } 52% { opacity: 0.6; } }
@media (prefers-reduced-motion: no-preference) {
  .visualizer.is-electric > * { animation: visualizer-crackle 0.9s steps(1) infinite; }
  .visualizer.is-electric > :nth-child(3n) { animation-duration: 1.3s; }
  .visualizer.is-electric > :nth-child(5n) { animation-delay: 0.2s; }
}

  :root[data-motion="always"] .visualizer.is-electric > * { animation: visualizer-crackle 0.9s steps(1) infinite; }
  :root[data-motion="always"] .visualizer.is-electric > :nth-child(3n) { animation-duration: 1.3s; }
  :root[data-motion="always"] .visualizer.is-electric > :nth-child(5n) { animation-delay: 0.2s; }


/* --- .trace — a live, smooth or electric line -------------------------------
 *   <svg class="trace" viewBox="0 0 200 40" preserveAspectRatio="none" role="img" aria-label="Live input">
 *     <line class="trace-axis" x1="0" y1="20" x2="200" y2="20"/>
 *     <path class="trace-fill" d="…closed shape…"/>
 *     <path class="trace-line" d="…"/>
 *   </svg>
 * The app draws the path from its samples (each frame for a live
 * waveform, a smoothed curve through the bands for a smooth one); core
 * only styles it. .is-electric adds the white-hot core and glow. */
.trace {
  display: block;
  inline-size: 100%;
  block-size: var(--trace-height, 4rem);
  overflow: visible;
  color: var(--trace-fg, var(--accent));
}
.trace :is(.trace-line, .trace-fill, .trace-axis) { vector-effect: non-scaling-stroke; }
.trace-line {
  fill: none;
  stroke: currentColor;
  stroke-width: var(--trace-width, 2px);
  stroke-linecap: round;
  stroke-linejoin: round;
}
.trace-fill { fill: color-mix(in srgb, currentColor var(--trace-fill-alpha, 22%), transparent); stroke: none; }
.trace-axis { stroke: var(--trace-axis, var(--border)); stroke-width: 1px; stroke-dasharray: var(--trace-axis-dash, 3 3); }
.trace.is-electric .trace-line {
  stroke: var(--trace-core, #fff);
  stroke-width: var(--trace-width, 1.5px);
  filter: drop-shadow(0 0 2px currentColor) drop-shadow(0 0 6px currentColor) drop-shadow(0 0 14px currentColor);
}
@media (prefers-reduced-motion: no-preference) {
  .trace.is-electric .trace-line { animation: visualizer-crackle 0.7s steps(1) infinite; }
}

  :root[data-motion="always"] .trace.is-electric .trace-line { animation: visualizer-crackle 0.7s steps(1) infinite; }

@media (forced-colors: active) { .trace-line { stroke: CanvasText; filter: none; } }

/* --- .param — a labelled parameter slider ----------------------------------
 *   <div class="param">
 *     <label class="param-label" for="dly">Delay</label>
 *     <span class="param-value"><input class="input" type="number" min="0" max="500" value="120" aria-label="Delay, milliseconds"><span>ms</span></span>
 *     <button class="btn btn-ghost btn-sm param-reset" type="button" aria-label="Reset delay">Reset</button>
 *     <input class="slider" id="dly" type="range" min="0" max="500" value="120" list="dly-marks">
 *     <datalist id="dly-marks"><option value="0" label="0"></option><option value="250" label="250"></option><option value="500" label="500"></option></datalist>
 *   </div>
 * controls.js keeps the slider and number in step and resets both to their
 * defaultValue (the value attribute) from the reset button or a
 * double-click. A log scale is the app's (map the slider's 0–1 itself). */
.param {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 0.35rem 0.5rem;
}
.param-label { color: var(--label-fg, var(--muted)); font-size: 0.85em; }
.param-value { display: inline-flex; align-items: center; gap: 0.25em; color: var(--muted); font-size: 0.85em; }
.param-value > .input {
  inline-size: var(--param-value-width, 5.5ch);
  padding: 0.15em 0.35em;
  text-align: end;
  font-variant-numeric: tabular-nums;
  -moz-appearance: textfield;
}
.param-value > .input::-webkit-inner-spin-button { -webkit-appearance: none; }
.param > .slider,
.param > datalist { grid-column: 1 / -1; }
.param > datalist {
  display: flex;
  justify-content: space-between;
  margin-block-start: -0.15rem;
  color: var(--muted);
  font-size: 0.7em;
}
.param > datalist > option { padding: 0; }

/* --- .slider.is-bipolar — fills from the centre (pan, balance, trim) -------
 *   <input class="slider is-bipolar" type="range" min="-1" max="1" step="0.01" value="0" aria-label="Pan" aria-valuetext="Centre">
 * --value (0–1, from controls.js) fills the track from the middle to the
 * thumb, with a centre tick; controls.js also snaps to the centre near it. */
.slider.is-bipolar {
  --_v: calc(var(--value, 0.5) * 100%);
  background:
    linear-gradient(var(--slider-centre, var(--text)), var(--slider-centre, var(--text))) 50% 50% / 1px 100% no-repeat,
    linear-gradient(90deg, transparent min(50%, var(--_v)), var(--slider-fill, var(--accent)) 0 max(50%, var(--_v)), transparent 0),
    var(--slider-bg, var(--surface-2));
}

/* --- .device-select — a device picker with its state ------------------------
 *   <div class="device-select" data-state="ready">
 *     <label class="label" for="mic">Microphone</label>
 *     <div class="device-select-row"><select class="select" id="mic">…</select>
 *       <div class="meter" role="meter" aria-label="Microphone level" aria-valuenow="-18" aria-valuemin="-60" aria-valuemax="0" style="--meter-level:40%"><div class="meter-fill"></div></div></div>
 *     <p class="device-select-status" role="status">Built-in microphone</p>
 *   </div>
 * data-state: ready, loading (finding devices), prompt (needs permission),
 * denied (blocked), none (no device) or disconnected (the chosen device
 * went away). The status text says what to do; the state adds a glyph and
 * colour, never colour alone. */
.device-select { display: grid; gap: 0.3rem; }
.device-select > .label { margin: 0; }
.device-select-row { display: flex; align-items: center; gap: 0.6rem; }
.device-select-row > .select { flex: 1 1 auto; min-inline-size: 0; }
.device-select-row > .meter { flex: 0 0 var(--device-meter-width, 5rem); }
.device-select-status { display: flex; align-items: center; gap: 0.4em; margin: 0; color: var(--muted); font-size: 0.8em; }
.device-select-status::before { flex: none; inline-size: 1.2em; text-align: center; font-weight: 700; }
.device-select[data-state="ready"] .device-select-status::before { content: "●" / ""; color: var(--success); }
.device-select[data-state="loading"] .device-select-status::before {
  content: "" / "";
  block-size: 0.8em;
  inline-size: 0.8em;
  border: 2px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spinner-rotate 0.7s linear infinite;
}
.device-select[data-state="prompt"] .device-select-status::before { content: "?" / ""; color: var(--accent); }
.device-select[data-state="denied"] .device-select-status { color: var(--danger-text, var(--danger)); }
.device-select[data-state="denied"] .device-select-status::before { content: "✕" / ""; }
.device-select[data-state="none"] .device-select-status::before { content: "–" / ""; }
.device-select[data-state="disconnected"] .select { border-color: var(--warning); }
.device-select[data-state="disconnected"] .device-select-status { color: var(--warning-text, var(--text)); }
.device-select[data-state="disconnected"] .device-select-status::before { content: "!" / ""; color: var(--warning); }
.device-select:is([data-state="loading"], [data-state="denied"], [data-state="none"]) .device-select-row > .meter { opacity: 0.4; }

/* --- .channel — a channel as a row (OBS / call-app style) ------------------
 *   <div class="channel" data-color="2">
 *     <div class="channel-head"><strong>Microphone</strong><span class="status status-ok">Live</span></div>
 *     <div class="channel-body"><div class="meter">…</div><input class="slider" type="range" aria-label="Microphone volume"></div>
 *     <output class="readout channel-value">−6 dB</output>
 *     <div class="channel-keys"><label class="key key-mute"><input type="checkbox">M</label>…</div>
 *     <p class="channel-notice">Your microphone is muted in system settings.</p>
 *   </div>
 * The console form is core's .strip in a .mixer; this is the same channel
 * laid out in one row, stacking on narrow screens. data-color tints the
 * name rule. .is-muted dims the meter. */
.channel {
  display: grid;
  grid-template-columns: minmax(7rem, 11rem) minmax(0, 1fr) auto auto;
  grid-template-areas: "head body value keys" "notice notice notice notice";
  align-items: center;
  gap: 0.35rem 0.9rem;
  padding: var(--channel-padding, 0.6rem 0.8rem);
  background: var(--channel-bg, var(--panel-bg, var(--surface)));
  color: var(--on-surface, var(--text));
  border: 1px solid var(--channel-border, var(--border));
  border-inline-start: 3px solid var(--user-color, var(--channel-accent, var(--border)));
  border-radius: var(--panel-radius, var(--radius));
}
.channel-head { grid-area: head; display: grid; gap: 0.15rem; min-inline-size: 0; }
.channel-head > strong { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.channel-body { grid-area: body; display: grid; gap: 0.35rem; }
.channel-value { grid-area: value; min-inline-size: 6ch; font-size: 0.9em; text-align: end; font-variant-numeric: tabular-nums; }
.channel-keys { grid-area: keys; display: flex; gap: 0.25rem; }
.channel-notice { grid-area: notice; margin: 0; color: var(--warning-text, var(--muted)); font-size: 0.8em; }
.channel-notice:empty { display: none; }
.channel.is-muted .channel-body > .meter { opacity: 0.35; }
/* Stacks when its list is narrow (a card, a popover) or the screen is. */
.channel-list { container-type: inline-size; display: grid; gap: 0.5rem; }
@container (max-width: 34rem) {
  .channel { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "head keys" "body value" "notice notice"; }
}
@media (max-width: 600px) {
  .channel { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "head keys" "body value" "notice notice"; }
}

/* --- .pad and .soundboard — trigger pads -----------------------------------
 *   <div class="soundboard">
 *     <button class="pad" data-mode="one-shot" data-color="1" aria-pressed="true" style="--progress:.4">
 *       <span class="pad-name">Airhorn</span><kbd>Q</kbd></button>
 *     <label class="pad" data-mode="loop"><input type="checkbox"><span class="pad-name">Rain</span><kbd>W</kbd></label>
 *   </div>
 * Modes (data-mode): one-shot and hold are buttons (hold is lit while
 * pressed), toggle and loop are checkboxes in a label, so their state needs
 * no script. Playing is aria-pressed="true", a checked input, :active for
 * hold, or .is-playing. --progress (0–1) fills the bar along the bottom;
 * data-color="1..6" or --pad-color picks the pad's colour. The app plays
 * the sound and binds the hotkey shown in the kbd. */
.soundboard {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--pad-min, 7.5rem), 1fr));
  gap: var(--soundboard-gap, 0.6rem);
}
.pad {
  --_c: var(--user-color, var(--pad-color, var(--accent)));
  position: relative;
  display: grid;
  grid-template-rows: 1fr auto;
  align-content: space-between;
  gap: 0.4rem;
  min-block-size: var(--pad-height, 5.5rem);
  padding: 0.6rem 0.7rem 0.75rem;
  overflow: hidden;
  border: 1px solid color-mix(in srgb, var(--_c) 45%, var(--border));
  border-radius: var(--pad-radius, var(--radius));
  background: var(--pad-bg, var(--surface-2));
  color: var(--pad-fg, var(--text));
  text-align: start;
  font: inherit;
  user-select: none;
  touch-action: manipulation;
}
.pad > input { position: absolute; inset: 0; opacity: 0; margin: 0; }
.pad-name { font-weight: 600; line-height: 1.2; overflow-wrap: anywhere; }
.pad > kbd { justify-self: start; }
.pad[data-mode]::before {
  position: absolute;
  inset-block-start: 0.45rem;
  inset-inline-end: 0.55rem;
  color: var(--muted);
  font-size: 0.75em;
}
.pad[data-mode="toggle"]::before { content: "⏻" / ""; }
.pad[data-mode="hold"]::before { content: "⏵⏵" / ""; letter-spacing: -0.2em; }
.pad[data-mode="loop"]::before { content: "⟲" / ""; }
/* The progress bar along the bottom edge. */
.pad::after {
  content: "";
  position: absolute;
  inset: auto auto 0 0;
  inline-size: calc(var(--progress, 0) * 100%);
  block-size: var(--pad-progress-height, 4px);
  background: var(--_c);
}
.pad:is([aria-pressed="true"], .is-playing, :has(> input:checked)),
.pad[data-mode="hold"]:active {
  border-color: var(--_c);
  background: color-mix(in srgb, var(--_c) var(--pad-lit-alpha, 30%), var(--pad-bg, var(--surface-2)));
  box-shadow: 0 0 0 1px var(--_c), 0 0 var(--pad-glow, 0.9rem) color-mix(in srgb, var(--_c) 45%, transparent);
}
.pad:is(:focus-visible, :has(> input:focus-visible)) { outline: 2px solid var(--focus); outline-offset: 2px; }
.pad:is(:disabled, :has(> input:disabled)) { opacity: 0.5; }
@media (forced-colors: active) {
  .pad:is([aria-pressed="true"], .is-playing, :has(> input:checked)) { border: 3px solid Highlight; }
  .pad::after { background: Highlight; }
}

/* --- .scrubber.has-buffer — how much of the stream has loaded -------------
 *   <div class="scrubber has-buffer" style="--buffered:.62"><time>…</time><input type="range" …><time>…</time></div>
 * The buffered part of the track shows in --scrubber-buffer between the
 * played fill and the plain track. */
.scrubber.has-buffer > input[type="range"] {
  background:
    linear-gradient(90deg, var(--scrubber-buffer, color-mix(in srgb, var(--text) 45%, transparent)) calc(var(--buffered, 0) * 100%), var(--scrubber-track, color-mix(in srgb, var(--text) 25%, transparent)) 0)
    50% 50% / 100% var(--_track) no-repeat;
}
.scrubber.has-buffer > input[type="range"]::-webkit-slider-thumb {
  border-image: linear-gradient(90deg, var(--scrubber-fill, var(--accent)) 50%, transparent 0) 0 1 / calc(50% - var(--_track) / 2) 100vw / 0 100vw;
}
.scrubber.has-buffer > input[type="range"]::-moz-range-thumb {
  border-image: linear-gradient(90deg, var(--scrubber-fill, var(--accent)) 50%, transparent 0) 0 1 / calc(50% - var(--_track) / 2) 100vw / 0 100vw;
}

/* --- .rta.is-area — a filled spectrum without gaps --------------------------
 * The third spectrum style beside bars (default) and .is-line. */
.rta.is-area { --rta-gap: 0px; --rta-fill: 45%; --rta-fill-top: 75%; }
/* --- v5 console: the parts of a digital mixing desk core didn't have ------
 * Aux send blocks, dynamics transfer curves, colour name tags, an effects
 * rack of faceplates with the assignable-controls panel, a graphic EQ and
 * a patchbay. Builds on core's mixer primitives (.mixer, .strip, .knob,
 * .fader, .key, .meter, .scribble) and metering.css's .eq-graph.
 * assets/js/console.js redraws transfer curves and EQ responses from their
 * controls and shows control values in <output>s; everything else is
 * radios and checkboxes. Every look reads a --<component>-* token that
 * falls back to the base palette. See docs/components/console.md. */

/* --- Name tags: a scribble strip filled with the channel colour ----------
 *   <div class="scribble" data-color="3"><span class="scribble-name">Mic3</span></div>
 * With data-color the whole tag takes the per-user colour and dark text, as
 * console channel names do. */
.scribble[data-color] {
  background: var(--user-color);
  color: var(--scribble-tag-fg, #111111);
  border-top-color: transparent;
  text-align: center;
}
.scribble[data-color] .scribble-num { color: inherit; opacity: 0.75; }

/* --- .sends — a channel's aux send levels --------------------------------
 *   <div class="sends" role="group" aria-label="Mic1 aux sends">
 *     <p class="sends-title">Auxes 1-8</p>
 *     <ol><li style="--send:.6"><span>1</span></li>…<li class="is-active" style="--send:.4">…</li></ol>
 *   </div>
 * Each row is a wedge as long as its --send (0–1). .is-active marks the aux
 * being edited; .is-pre a pre-fader send. Put a range input in a row to
 * make it adjustable (controls.js keeps --value; use style="--send: var(--value)"). */
.sends {
  display: grid;
  gap: 1px;
  padding: 0.2rem;
  background: var(--sends-bg, #0c0c12);
  border: 1px solid var(--sends-border, var(--border));
  color: var(--sends-fg, #ffffff);
  font-size: 0.6rem;
  line-height: 1.25;
}
.sends-title { margin: 0 0 0.1rem; text-align: center; font-weight: 700; }
.sends ol { display: grid; gap: 1px; margin: 0; padding: 0; list-style: none; }
.sends li { display: grid; grid-template-columns: 0.9rem 1fr; align-items: center; gap: 0.2rem; }
.sends li::after {
  content: "";
  block-size: 0.45rem;
  inline-size: calc(var(--send, 0) * 100%);
  background: var(--sends-bar, #d8dde8);
  clip-path: polygon(0 100%, 100% 0, 100% 100%);
}
.sends li.is-pre::after { background: var(--sends-bar-pre, var(--accent-2)); }
.sends li:is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))) { color: var(--sends-active, var(--accent)); }
.sends li:is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked)))::after { background: var(--sends-active, var(--accent)); }

/* --- .dyn-graph — a dynamics transfer curve -------------------------------
 *   <svg class="dyn-graph" viewBox="0 0 100 100" role="img" aria-label="Compressor curve">
 *     <path class="dyn-graph-line" d="M0 100 L60 40 L100 25"/>
 *   </svg>
 * Input level across, output up; a 1:1 diagonal under the curve. console.js
 * redraws the path from the controls in the same [data-dyn] group (see the
 * docs). Small as a strip thumbnail, large on a detail page. */
.dyn-graph {
  display: block;
  inline-size: 100%;
  aspect-ratio: 1;
  background:
    linear-gradient(to top right, transparent calc(50% - 0.5px), var(--dyn-graph-unity, rgba(255, 255, 255, 0.25)) 0 calc(50% + 0.5px), transparent 0),
    repeating-linear-gradient(0deg, transparent 0 calc(25% - 1px), var(--dyn-graph-grid, rgba(255, 255, 255, 0.14)) 0 25%),
    repeating-linear-gradient(90deg, transparent 0 calc(25% - 1px), var(--dyn-graph-grid, rgba(255, 255, 255, 0.14)) 0 25%),
    var(--dyn-graph-bg, linear-gradient(180deg, #3a6bd0, #1d3f96));
  border: 1px solid var(--dyn-graph-border, var(--border));
  overflow: visible;
}
.dyn-graph-line {
  fill: none;
  stroke: var(--dyn-graph-line, #ffffff);
  stroke-width: var(--dyn-graph-line-width, 2px);
  vector-effect: non-scaling-stroke;
  stroke-linejoin: round;
}
.dyn-graph-threshold { stroke: var(--dyn-graph-threshold, var(--accent)); stroke-width: 1px; stroke-dasharray: 3 3; vector-effect: non-scaling-stroke; }
[data-dyn]:has(.dyn-bypass:not(:checked)) .dyn-graph-line { stroke: var(--dyn-graph-line-off, rgba(255, 255, 255, 0.45)); }

/* --- .rack — an equipment rack of .rack-units ------------------------------
 *   <div class="rack" aria-label="Effects rack">
 *     <label class="rack-unit" data-finish="teal" style="--u: 2">
 *       <input type="radio" name="fx" value="reverb"><span class="rack-unit-name">Chamber Reverb</span>…
 *     </label>
 *   </div>
 * A flight case with rails; each unit is --u rack units high (1–4) on a
 * finish: data-finish="silver|black|blue|teal|red|purple|green|grey". A unit
 * holding a radio is selectable (outlined when chosen); a .rack-unit.is-empty
 * is a blank panel. */
.rack {
  --_u: var(--rack-u, 2.1rem);
  display: grid;
  gap: 2px;
  padding: 0.6rem 1.4rem;
  background:
    radial-gradient(circle at 0.7rem 0.55rem, #000 0 2px, transparent 2.5px) 0 0 / 100% var(--_u) repeat-y,
    radial-gradient(circle at calc(100% - 0.7rem) 0.55rem, #000 0 2px, transparent 2.5px) 0 0 / 100% var(--_u) repeat-y,
    linear-gradient(90deg, var(--rack-rail, #b08a50) 0 1.4rem, transparent 1.4rem calc(100% - 1.4rem), var(--rack-rail, #b08a50) calc(100% - 1.4rem)),
    var(--rack-bg, radial-gradient(80% 60% at 50% 40%, #1d1d1d, #050505));
  border: var(--rack-frame-width, 0.9rem) solid var(--rack-frame, #121212);
  border-radius: var(--rack-radius, 6px);
  box-shadow: 0 0 0 2px var(--rack-trim, #9a9a9a), 0 8px 24px rgba(0, 0, 0, 0.55);
}
.rack-unit {
  --_finish: var(--rack-finish, #2a2a2a);
  --_finish-fg: #ffffff;
  position: relative;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  min-block-size: calc(var(--_u) * var(--u, 1) + 2px * (var(--u, 1) - 1));
  padding: 0.25rem 1.6rem;
  background:
    radial-gradient(circle at 0.65rem 0.55rem, #000 0 2.5px, #888 3px 4px, transparent 4.5px),
    radial-gradient(circle at calc(100% - 0.65rem) 0.55rem, #000 0 2.5px, #888 3px 4px, transparent 4.5px),
    radial-gradient(circle at 0.65rem calc(100% - 0.55rem), #000 0 2.5px, #888 3px 4px, transparent 4.5px),
    radial-gradient(circle at calc(100% - 0.65rem) calc(100% - 0.55rem), #000 0 2.5px, #888 3px 4px, transparent 4.5px),
    linear-gradient(180deg, rgba(255, 255, 255, 0.18), transparent 35%, rgba(0, 0, 0, 0.25)),
    var(--_finish);
  color: var(--_finish-fg);
  border: 1px solid rgba(0, 0, 0, 0.6);
  border-radius: 2px;
  font-size: 0.68rem;
  overflow: hidden;
}
.rack-unit[data-finish="silver"] { --_finish: linear-gradient(180deg, #e9e9e9, #bdbdbd); --_finish-fg: #111111; }
.rack-unit[data-finish="black"] { --_finish: #161616; }
.rack-unit[data-finish="grey"] { --_finish: linear-gradient(180deg, #4a4a4a, #2b2b2b); }
.rack-unit[data-finish="blue"] { --_finish: linear-gradient(180deg, #34428d, #1d2860); }
.rack-unit[data-finish="teal"] { --_finish: linear-gradient(180deg, #2f6a74, #163c44); }
.rack-unit[data-finish="red"] { --_finish: linear-gradient(180deg, #8e2a1e, #5c140c); }
.rack-unit[data-finish="purple"] { --_finish: linear-gradient(180deg, #6a2f8c, #3b1150); }
.rack-unit[data-finish="green"] { --_finish: linear-gradient(180deg, #2f8a2f, #145214); }
.rack-unit > input[type="radio"] { position: absolute; inset: 0; margin: 0; opacity: 0; }
.rack-unit:has(> input:checked) { outline: 2px solid var(--rack-selected, #ffe33b); outline-offset: -2px; }
.rack-unit:has(> input:focus-visible) { outline: 2px solid var(--focus); outline-offset: -2px; }
.rack-unit.is-empty { --_finish: linear-gradient(180deg, #2b2b2b, #151515); justify-content: center; color: #8a8a8a; }
.rack-unit-name {
  font: 800 1.05rem/1 var(--font);
  letter-spacing: -0.01em;
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.5);
  white-space: nowrap;
}
.rack-unit[data-finish="silver"] .rack-unit-name { text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6); }
.rack-unit-body { display: flex; flex: 1; flex-wrap: wrap; align-items: center; justify-content: space-around; gap: 0.3rem 0.6rem; min-inline-size: 0; }
/* The thin "Change device / Store / Load preset" strip at the top of the rack. */
.rack-head { display: flex; justify-content: center; gap: 0.6rem; padding: 0.4rem; background: linear-gradient(180deg, #f2f2f2, #c9c9c9); border-radius: 2px; }

/* A unit's faceplate seen up close: a bigger .rack-unit inside .faceplate,
   above its .assignable panel. */
.faceplate {
  display: grid;
  gap: 0.6rem;
  padding: 0.5rem;
  background: var(--faceplate-bg, #050505);
  border: 1px solid var(--faceplate-border, #2a2a2a);
  border-radius: 4px;
}
.faceplate > .rack-unit { --_u: 2.6rem; align-items: stretch; padding-block: 0.8rem; font-size: 0.75rem; }
.faceplate-group {
  display: grid;
  gap: 0.35rem;
  justify-items: center;
  padding: 0.4rem 0.5rem;
  border: 1px solid var(--faceplate-group-border, rgba(255, 255, 255, 0.35));
  border-radius: 6px;
}
.faceplate-group:is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))) { border-color: var(--faceplate-group-active, #ffffff); box-shadow: 0 0 0 1px var(--faceplate-group-active, #ffffff); }
.faceplate-title { margin: 0; font-weight: 800; }
.faceplate-knobs { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.5rem 0.8rem; }
.faceplate-display {
  min-inline-size: 7rem;
  padding: 0.4rem 0.6rem;
  background: var(--faceplate-display-bg, #000000);
  color: var(--faceplate-display-fg, #e0e0e0);
  border-radius: 4px;
  font: 800 1.2rem/1.1 var(--font);
  text-align: center;
}

/* --- .assignable — the blue soft-key panel under every unit -------------
 *   <div class="assignable" role="group" aria-label="Assignable controls">
 *     <div class="assignable-col"><span>faders 1</span>
 *       <label class="assignable-key"><input type="checkbox"><span class="visually-hidden">…</span></label>
 *       <label class="knob">…</label><span>dB 20Hz</span></div>…
 *     <p class="assignable-caption">assignable controls</p>
 *   </div>
 * Eight columns: a caption, a square soft key (lit when on), a knob, and
 * its unit and name. */
.assignable {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(5.2rem, 1fr));
  gap: 0.4rem 0.3rem;
  padding: 0.5rem 0.6rem 0.3rem;
  background: var(--assignable-bg, linear-gradient(180deg, #5560ab, #434d98));
  color: var(--assignable-fg, #ffffff);
  border: 1px solid var(--assignable-border, #2a3274);
  border-radius: 3px;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.25);
  font-size: 0.68rem;
  text-align: center;
}
.assignable-col { display: grid; justify-items: center; align-content: start; gap: 0.25rem; }
.assignable-key {
  position: relative;
  display: block;
  inline-size: 2.6rem;
  block-size: 2.4rem;
  background: var(--assignable-key-bg, linear-gradient(180deg, #4a54a0, #3a438b));
  border: 2px solid var(--assignable-key-border, #6a74c0);
  box-shadow: inset 0 0 0 2px #2b3274;
}
.assignable-key > input { position: absolute; inset: 0; margin: 0; opacity: 0; }
.assignable-key:has(> input:checked) { background: var(--assignable-key-on, linear-gradient(180deg, #e9ecff, #b5bce8)); }
.assignable-key:has(> input:focus-visible) { outline: 2px solid var(--focus); outline-offset: 2px; }
.assignable-caption { grid-column: 1 / -1; margin: 0; font-weight: 700; opacity: 0.9; }

/* --- .geq — a graphic EQ faceplate -----------------------------------------
 *   <div class="geq" role="group" aria-label="GEQ A">
 *     <div class="geq-scale" aria-hidden="true"><span>+12</span><span>0</span><span>-12</span></div>
 *     <div class="geq-group is-active"><label class="geq-band"><input type="range" min="-12" max="12" step="0.5" value="0" aria-label="20 Hz"><span>20</span></label>…</div>…
 *   </div>
 * Bands in groups (the console's eight fader groups). Each band is a native
 * vertical range input; a group marked .is-active is the one on the
 * assignable controls. */
.geq {
  display: flex;
  align-items: stretch;
  gap: 2px;
  padding: 0.5rem;
  background: var(--geq-bg, linear-gradient(180deg, #a9a9a9, #8a8a8a));
  border-radius: 3px;
  color: var(--geq-fg, #111111);
  font-size: 0.6rem;
  font-weight: 700;
}
.geq-scale { display: flex; flex-direction: column; justify-content: space-between; padding-block: 0.2rem 1.1rem; text-align: end; }
.geq-group {
  display: flex;
  flex: 1;
  gap: 1px;
  padding: 0.15rem;
  background:
    linear-gradient(transparent calc(50% - 0.6rem), var(--geq-zero, #111111) 0 calc(50% - 0.6rem + 1px), transparent 0),
    var(--geq-group-bg, #9a9a9a);
  border: 1px solid var(--geq-group-border, #1f2a8a);
}
.geq-group:is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))) { --geq-group-bg: var(--geq-group-active, #e6e6e6); }
.geq-band { display: grid; flex: 1; justify-items: center; gap: 0.15rem; min-inline-size: 0; }
.geq-band > input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  writing-mode: vertical-lr;
  direction: rtl;
  /* Physical sizes: in vertical writing mode the logical axes swap. */
  width: 100%;
  min-width: 0.9rem;
  height: var(--geq-height, 7rem);
  margin: 0;
  background: linear-gradient(90deg, transparent calc(50% - 3px), var(--geq-slot, #000000) 0 calc(50% + 3px), transparent 0);
}
.geq-band > input::-webkit-slider-thumb {
  -webkit-appearance: none;
  inline-size: 0.9rem;
  block-size: 0.55rem;
  background: var(--geq-cap, linear-gradient(180deg, #ffffff, #b0b0b0));
  border: 1px solid #000000;
  border-radius: 1px;
}
.geq-band > input::-moz-range-thumb {
  inline-size: 0.9rem;
  block-size: 0.55rem;
  background: var(--geq-cap, linear-gradient(180deg, #ffffff, #b0b0b0));
  border: 1px solid #000000;
  border-radius: 1px;
}
.geq-band > input:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
.geq-band > span { white-space: nowrap; }

/* --- .patchbay — device blocks of patch points ------------------------------
 *   <section class="patch-device" aria-labelledby="dl251a">
 *     <header><span class="patch-id">ID 1</span><span class="patch-name" id="dl251a">DL251A 1</span></header>
 *     <div class="patch-grid"><label class="patch-point"><input type="checkbox" checked><span class="visually-hidden">A1</span></label>…</div>
 *   </section>
 * A checked point is patched (green, or data-kind="aes" yellow); grey is
 * free. .patch-grid takes --patch-cols (default 8). */
.patchbay { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr)); gap: 0.6rem; }
.patch-device {
  display: grid;
  gap: 0.4rem;
  padding: 0.5rem;
  background: var(--patch-device-bg, linear-gradient(180deg, #4f8ad0, #2f62ae));
  border: 1px solid var(--patch-device-border, #12285e);
  border-radius: 8px;
  color: #ffffff;
}
.patch-device > header { display: flex; align-items: center; gap: 0.4rem; font-weight: 800; }
.patch-name { flex: 1; padding: 0.15rem 0.4rem; background: #000000; border-radius: 2px; }
.patch-group-label { grid-column: 1 / -1; margin: 0.2rem 0 0; font-size: 0.75rem; font-weight: 800; text-align: center; }
.patch-grid { display: grid; grid-template-columns: repeat(var(--patch-cols, 8), minmax(1.4rem, 1fr)); gap: 3px; }
.patch-point {
  position: relative;
  aspect-ratio: 1;
  background: var(--patch-free, linear-gradient(180deg, #f0f0f0, #c8c8c8));
  border: 1px solid #333333;
  border-radius: 2px;
}
/* The jack: a small ring in the middle of each point. */
.patch-point::after {
  content: "";
  position: absolute;
  inset: 28%;
  border: 2px solid rgba(0, 0, 0, 0.45);
  border-radius: 50%;
}
.patch-point > input { position: absolute; inset: 0; margin: 0; opacity: 0; }
.patch-point:has(> input:checked) { background: var(--patch-on, linear-gradient(180deg, #8ad35a, #4e9a2a)); }
.patch-point[data-kind="aes"]:has(> input:checked) { background: var(--patch-aes, linear-gradient(180deg, #ffe46b, #d6b21a)); }
.patch-point:has(> input:focus-visible) { outline: 2px solid var(--focus); outline-offset: 1px; }
@media (forced-colors: active) {
  .patch-point:has(> input:checked), .assignable-key:has(> input:checked) { background: Highlight; }
  .rack-unit:has(> input:checked) { outline-color: Highlight; }
}
/* Light faceplates take dark knob captions. */
.rack-unit[data-finish="silver"] { --knob-label-fg: #111111; --scale-fg: #111111; }

/* --- Key tones beyond core's four -------------------------------------------
 *   <label class="key" data-tone="cyan"><input type="checkbox">ø</label>
 * data-tone="cyan|blue|orange|brown|mauve|grey|white" picks a key's lit
 * colour, for consoles that code more functions than on/mute/solo/select. */
.key[data-tone="cyan"] { --key-lit: var(--key-cyan, #19c6ff); --key-lit-fg: var(--key-cyan-fg, #000000); }
.key[data-tone="blue"] { --key-lit: var(--key-blue, #2f7dff); --key-lit-fg: var(--key-blue-fg, #ffffff); }
.key[data-tone="orange"] { --key-lit: var(--key-orange, #ff8a1a); --key-lit-fg: var(--key-orange-fg, #000000); }
.key[data-tone="brown"] { --key-lit: var(--key-brown, #b8862a); --key-lit-fg: var(--key-brown-fg, #000000); }
.key[data-tone="mauve"] { --key-lit: var(--key-mauve, #c27ad6); --key-lit-fg: var(--key-mauve-fg, #000000); }
.key[data-tone="grey"] { --key-lit: var(--key-grey, #a0a0a8); --key-lit-fg: var(--key-grey-fg, #000000); }
.key[data-tone="white"] { --key-lit: var(--key-white, #f4f4f4); --key-lit-fg: var(--key-white-fg, #000000); }

/* --- .knob.is-led — a gain pot inside a ring of LED segments ----------------
 *   <label class="knob is-led" style="--value:.6"><input type="range" …><span class="knob-dial"></span></label>
 * The segments light up to --value: the first two warn in yellow and red
 * (too little gain), the rest green; unlit segments stay dim. */
.knob.is-led .knob-dial { --knob-arc-width: var(--knob-led-width, 0.32rem); }
.knob.is-led .knob-dial::before {
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - var(--knob-arc-width)), #000 calc(100% - var(--knob-arc-width) + 0.5px));
  mask: radial-gradient(farthest-side, transparent calc(100% - var(--knob-arc-width)), #000 calc(100% - var(--knob-arc-width) + 0.5px));
  background:
    repeating-conic-gradient(from 225deg, transparent 0 11deg, #000 11deg 13.5deg),
    conic-gradient(from 225deg,
      var(--knob-led-low, #ff2a1a) 0 calc(min(var(--value, 0.5), 0.06) * 270deg),
      var(--knob-led-warn, #ffe01a) 0 calc(min(var(--value, 0.5), 0.12) * 270deg),
      var(--knob-led-on, #2cff3a) 0 calc(var(--value, 0.5) * 270deg),
      var(--knob-led-off, #0c3a2a) 0 270deg,
      transparent 0);
  box-shadow: 0 0 0 1px #000;
  clip-path: none;
}
.knob.is-led .knob-dial::after { inset: calc(var(--knob-arc-width) + 3px); }

/* --- Pointer styles: .knob[data-pointer] ------------------------------------
 *   <label class="knob" data-pointer="chicken" style="--value:.3">…</label>
 * The cap shapes a large-format console draws its knobs with. Colours come
 * from --knob-color (cap), --knob-pointer (line or blade), --knob-hi
 * (highlight), --knob-rim / --knob-rim-dark (ribs):
 *   ribbed    black dome in a ribbed rim, long pointer line (the default look)
 *   mini      the same, small, for dense panels
 *   lobed     glossy seven-lobed cap (yellow line on the console's EQ pots)
 *   clock     white fluted dome with a coloured hand
 *   flat      an outline ring and line, no cap
 *   sculpted  a silver disc with a raised bar and a coloured dot at its tip
 *   chicken   a chicken-head blade over a round skirt (brass; .is-black: black
 *             with a white tip)
 *   ring      no cap: the value is a lit arc in a dark ring
 *   dots      a flat pointerless disc; the value lights one dot of its scale */
/* Each style's own colours, at zero specificity so a page class still wins
   and a theme's --knob-color for plain knobs doesn't paint over them. */
:where(.knob[data-pointer="lobed"]) { --knob-color: #2a2a2e; --knob-pointer: #f2e21a; --knob-hi: rgba(255, 255, 255, 0.6); }
:where(.knob[data-pointer="clock"]) { --knob-color: #ececf0; --knob-pointer: #4b1a8a; }
:where(.knob[data-pointer="flat"]) { --knob-pointer: #d9cfb2; }
:where(.knob[data-pointer="sculpted"]) { --knob-color: #b4b4b8; --knob-pointer: #3b5bd6; }
:where(.knob[data-pointer="chicken"]) { --knob-color: #a87224; --knob-hi: #f6d08a; --knob-blade: #f2d9a4; --knob-pointer: transparent; }
:where(.knob[data-pointer="chicken"].is-black) { --knob-color: #151515; --knob-hi: #555555; --knob-blade: #2a2a2a; --knob-pointer: #ffffff; }
:where(.knob[data-pointer="dots"]) { --knob-color: #bcbcbc; }
:is(.knob[data-pointer="ribbed"], .knob[data-pointer="mini"]) .knob-dial::after {
  background:
    linear-gradient(var(--knob-pointer, #ffffff), var(--knob-pointer, #ffffff)) 50% 4% / 3px 42% no-repeat,
    radial-gradient(circle at 40% 34%, var(--knob-hi, rgba(255, 255, 255, 0.4)) 0, transparent 34%),
    radial-gradient(circle, transparent 0 50%, rgba(0, 0, 0, 0.45) 58%, transparent 61%),
    radial-gradient(circle, var(--knob-color, #1b1b1f) 0 58%, transparent 60%),
    repeating-conic-gradient(var(--knob-rim, #4a4a50) 0 7deg, var(--knob-rim-dark, #121214) 7deg 15deg);
  border: 1px solid #000000;
}
.knob[data-pointer="mini"] { --knob-size: 1.15rem; --knob-arc-width: 2px; }
.knob[data-pointer="mini"] .knob-dial::after { background-size: 2px 42%, auto, auto, auto, auto; }
.knob[data-pointer="lobed"] .knob-dial::after {
  inset: calc(var(--knob-arc-width, 3px) + 1px);
  background:
    linear-gradient(var(--knob-pointer, #f2e21a), var(--knob-pointer, #f2e21a)) 50% 6% / 3px 40% no-repeat,
    radial-gradient(circle at 36% 28%, var(--knob-hi, rgba(255, 255, 255, 0.6)) 0, transparent 30%),
    radial-gradient(circle at 50% 40%, color-mix(in srgb, var(--knob-color, #2a2a2e) 80%, #fff) 0, var(--knob-color, #2a2a2e) 45%, #050505 85%);
  border: 0;
  border-radius: 0;
  clip-path: polygon(50.0% 0.0%, 55.5% 1.6%, 60.1% 5.6%, 64.0% 10.1%, 67.8% 13.1%, 72.5% 14.2%, 78.4% 14.4%, 84.4% 15.6%, 89.1% 18.8%, 91.2% 24.1%, 91.0% 30.3%, 89.9% 36.0%, 90.0% 40.9%, 92.1% 45.3%, 95.5% 50.0%, 98.4% 55.5%, 98.7% 61.1%, 96.0% 66.1%, 91.0% 69.7%, 85.8% 72.5%, 82.1% 75.6%, 79.9% 79.9%, 78.4% 85.6%, 75.9% 91.2%, 71.7% 95.0%, 66.1% 96.0%, 60.1% 94.4%, 54.7% 92.1%, 50.0% 91.0%, 45.3% 92.1%, 39.9% 94.4%, 33.9% 96.0%, 28.3% 95.0%, 24.1% 91.2%, 21.6% 85.6%, 20.1% 79.9%, 17.9% 75.6%, 14.2% 72.5%, 9.0% 69.7%, 4.0% 66.1%, 1.3% 61.1%, 1.6% 55.5%, 4.5% 50.0%, 7.9% 45.3%, 10.0% 40.9%, 10.1% 36.0%, 9.0% 30.3%, 8.8% 24.1%, 10.9% 18.8%, 15.6% 15.6%, 21.6% 14.4%, 27.5% 14.2%, 32.2% 13.1%, 36.0% 10.1%, 39.9% 5.6%, 44.5% 1.6%);
}
.knob[data-pointer="clock"] .knob-dial::after {
  background:
    linear-gradient(var(--knob-pointer, #4b1a8a), var(--knob-pointer, #4b1a8a)) 50% 2% / 3px 48% no-repeat,
    radial-gradient(circle at 40% 32%, #ffffff 0, transparent 42%),
    radial-gradient(circle, transparent 0 66%, rgba(0, 0, 0, 0.18) 70%, transparent 74%),
    radial-gradient(circle, var(--knob-color, #ececf0) 0 68%, transparent 70%),
    repeating-conic-gradient(#f4f4f6 0 6deg, #a8a8b0 6deg 15deg);
  border: 1px solid #6a6a72;
}
.knob[data-pointer="flat"] .knob-dial::after {
  background: linear-gradient(var(--knob-pointer, #d9cfb2), var(--knob-pointer, #d9cfb2)) 50% 0 / 3px 46% no-repeat;
  border: 3px solid var(--knob-pointer, #d9cfb2);
  box-shadow: none;
}
.knob[data-pointer="sculpted"] .knob-dial::after {
  background:
    radial-gradient(circle at 50% 20%, var(--knob-pointer, #3b5bd6) 0 9%, #10205a 10% 12%, transparent 13%),
    linear-gradient(90deg, transparent 36%, rgba(0, 0, 0, 0.4) 36% 38%, #fbfbfb 38%, #d4d4d4 50%, #a4a4a4 62%, rgba(0, 0, 0, 0.45) 62% 64%, transparent 64%),
    radial-gradient(circle at 38% 30%, #fafafa, var(--knob-color, #b4b4b8) 55%, #7a7a80);
  border: 1px solid #4a4a50;
}
.knob[data-pointer="chicken"] .knob-dial::after {
  inset: 0;
  background:
    linear-gradient(var(--knob-pointer, transparent), var(--knob-pointer, transparent)) 50% 0 / 2px 16% no-repeat,
    conic-gradient(from 171deg at 50% -4%, color-mix(in srgb, var(--knob-blade, #f2d9a4) 65%, #fff) 0 5deg, var(--knob-blade, #f2d9a4) 0 12deg, color-mix(in srgb, var(--knob-blade, #f2d9a4) 60%, #000) 0 18deg, transparent 0),
    radial-gradient(circle at 42% 38%, var(--knob-hi, #f6d08a), var(--knob-color, #a87224) 42%, color-mix(in srgb, var(--knob-color, #a87224) 45%, #000) 66%, transparent 67%);
  border: 0;
  border-radius: 0;
  box-shadow: none;
  -webkit-mask: conic-gradient(from 171deg at 50% -4%, #000 0 18deg, transparent 0), radial-gradient(circle, #000 66%, transparent 67%);
  mask: conic-gradient(from 171deg at 50% -4%, #000 0 18deg, transparent 0), radial-gradient(circle, #000 66%, transparent 67%);
}
.knob[data-pointer="ring"] .knob-dial { --knob-arc-width: var(--knob-ring-width, 26%); }
.knob[data-pointer="ring"] .knob-dial::before {
  background: conic-gradient(from 225deg, var(--knob-ring-on, #22d322) 0 calc(var(--value, 0.5) * 270deg), var(--knob-ring-off, #050505) 0);
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - var(--knob-arc-width)), #000 calc(100% - var(--knob-arc-width) + 0.5px));
  mask: radial-gradient(farthest-side, transparent calc(100% - var(--knob-arc-width)), #000 calc(100% - var(--knob-arc-width) + 0.5px));
  clip-path: none;
}
.knob[data-pointer="ring"] .knob-dial::after { background: none; border: 0; box-shadow: none; }
.knob[data-pointer="dots"] .knob-dial { --knob-arc-width: 4px; }
.knob[data-pointer="dots"] .knob-dial::before {
  background:
    conic-gradient(from calc(219deg + round(var(--value, 0.5) * 10, 1) * 27deg), var(--knob-dot-on, #ff3b2f) 0 12deg, transparent 0),
    repeating-conic-gradient(from 219deg, var(--knob-dot, rgba(255, 255, 255, 0.85)) 0 12deg, transparent 0 27deg);
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - var(--knob-arc-width)), #000 calc(100% - var(--knob-arc-width) + 0.5px)), conic-gradient(from 219deg, #000 0 282deg, transparent 0);
  -webkit-mask-composite: source-in;
  mask: radial-gradient(farthest-side, transparent calc(100% - var(--knob-arc-width)), #000 calc(100% - var(--knob-arc-width) + 0.5px)), conic-gradient(from 219deg, #000 0 282deg, transparent 0);
  mask-composite: intersect;
  clip-path: none;
  filter: drop-shadow(0 0 2px var(--knob-dot-on, #ff3b2f));
}
.knob[data-pointer="dots"] .knob-dial::after {
  inset: calc(var(--knob-arc-width) + 4px);
  background: radial-gradient(circle at 44% 34%, color-mix(in srgb, var(--knob-color, #bcbcbc) 60%, #fff), var(--knob-color, #bcbcbc) 55%, color-mix(in srgb, var(--knob-color, #bcbcbc) 60%, #000));
  border: 1px solid #000000;
}

/* --- Finishes: data-finish="brushed|steel|leather" --------------------------
 * On a .rack-unit, or on any element with .finish. Brushed metal is fine
 * horizontal scratches (three repeating gradients at co-prime periods, so
 * they never line up) over a metallic gradient; --brushed-hi / --brushed-lo
 * set the metal. Leather is a generated grain (an SVG noise lit from the top
 * left, no image file) over --leather. .engraved presses text into it. */
.finish { background: var(--_finish); color: var(--_finish-fg, inherit); }
[data-finish="brushed"], [data-finish="steel"] {
  --_finish:
    repeating-linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0 1px, transparent 1px 2px),
    repeating-linear-gradient(180deg, rgba(0, 0, 0, 0.07) 0 1px, transparent 1px 3px),
    repeating-linear-gradient(180deg, rgba(255, 255, 255, 0.04) 0 1px, transparent 1px 5px),
    repeating-linear-gradient(180deg, rgba(0, 0, 0, 0.06) 0 1px, transparent 1px 7px),
    repeating-linear-gradient(180deg, transparent 0 9px, rgba(255, 255, 255, 0.06) 9px 10px, transparent 10px 13px),
    linear-gradient(90deg, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, 0.1) 22%, rgba(255, 255, 255, 0) 45%, rgba(255, 255, 255, 0.07) 70%, rgba(255, 255, 255, 0) 92%),
    linear-gradient(180deg, var(--brushed-hi, #5e5e62), var(--brushed-lo, #2a2a2d));
}
[data-finish="steel"] { --brushed-hi: #e2e2e4; --brushed-lo: #a2a2a8; --_finish-fg: #111111; --knob-label-fg: #111111; --scale-fg: #111111; }
[data-finish="leather"] {
  --_finish:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='m' x='0' y='0'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.011' numOctaves='3' seed='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 0 0 -.9'/%3E%3C/filter%3E%3Cfilter id='g' x='0' y='0'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.42' numOctaves='3' seed='7' stitchTiles='stitch'/%3E%3CfeDiffuseLighting surfaceScale='2.4' lighting-color='%23fff'%3E%3CfeDistantLight azimuth='225' elevation='48'/%3E%3C/feDiffuseLighting%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1.3 0 0 0 1.05'/%3E%3C/filter%3E%3Crect width='100%' height='100%' filter='url(%23m)' opacity='.4'/%3E%3Crect width='100%' height='100%' filter='url(%23g)' opacity='.6'/%3E%3C/svg%3E") 0 0 / 220px 220px,
    radial-gradient(130% 160% at 30% 15%, color-mix(in srgb, var(--leather, #8c1d16) 88%, #fff), var(--leather, #8c1d16) 45%, color-mix(in srgb, var(--leather, #8c1d16) 50%, #000));
}
.engraved {
  color: var(--engraved-fg, rgba(205, 205, 205, 0.82));
  text-shadow: -1px -1px 1px rgba(0, 0, 0, 0.75), 1px 1px 1px rgba(255, 255, 255, 0.28);
}

/* The area under a dynamics curve, filled (console.js closes the path). */
.dyn-graph-fill { fill: var(--dyn-graph-fill, rgba(140, 190, 255, 0.45)); stroke: none; }
[data-dyn]:has(.dyn-bypass:not(:checked)) .dyn-graph-fill { fill: var(--dyn-graph-fill-off, rgba(140, 190, 255, 0.18)); }

/* --- .led-list — options chosen by a lit LED -------------------------------
 *   <ul class="led-list" role="radiogroup" aria-label="Program">
 *     <li><label><input type="radio" name="prog" checked>Hall</label></li>…
 *   </ul>
 * Each option has a small round LED that lights (red, or green with
 * .is-green) when its radio is checked: program lists, detector modes,
 * display modes. */
.led-list { display: grid; gap: 2px; margin: 0; padding: 0; list-style: none; font-size: 0.62rem; font-weight: 700; }
.led-list label { position: relative; display: flex; align-items: center; gap: 0.35rem; min-block-size: 0.95rem; white-space: nowrap; }
.led-list input { position: absolute; inset: 0; margin: 0; opacity: 0; }
.led-list label::before {
  content: "";
  flex: none;
  inline-size: 0.55rem;
  block-size: 0.55rem;
  border-radius: 50%;
  background: radial-gradient(circle at 40% 35%, var(--led-off-hi, #9a9a9a), var(--led-off, #333333) 70%);
  box-shadow: 0 0 0 1px #000000;
}
.led-list label:has(input:checked)::before {
  background: radial-gradient(circle at 40% 35%, #ffffff 0 15%, var(--led-on, #ff3b2f) 50%, color-mix(in srgb, var(--led-on, #ff3b2f) 50%, #000));
  box-shadow: 0 0 0 1px #000000, 0 0 5px var(--led-on, #ff3b2f);
}
.led-list label:has(input:focus-visible) { outline: 2px solid var(--focus); outline-offset: 1px; }
.led-list.is-green { --led-on: #3df05a; }
@media (forced-colors: active) { .led-list label:has(input:checked)::before { background: Highlight; } }

/* --- Channel tag colours: data-tag on a .scribble ---------------------------
 * The sixteen name-tag colours a large console offers. Each reads a
 * --tag-<name> token, so a theme can retune them. */
.scribble[data-tag] { background: var(--_tag); color: var(--_tag-fg, #111111); border-top-color: transparent; text-align: center; }
.scribble[data-tag="black"] { --_tag: var(--tag-black, #111111); --_tag-fg: #ffffff; }
.scribble[data-tag="dblue"] { --_tag: var(--tag-dblue, #1f3fd0); --_tag-fg: #ffffff; }
.scribble[data-tag="dgreen"] { --_tag: var(--tag-dgreen, #1e7a2a); --_tag-fg: #ffffff; }
.scribble[data-tag="flesh"] { --_tag: var(--tag-flesh, #e8b48a); }
.scribble[data-tag="lblue"] { --_tag: var(--tag-lblue, #4ab4ff); }
.scribble[data-tag="lgreen"] { --_tag: var(--tag-lgreen, #6be06b); }
.scribble[data-tag="lpurple"] { --_tag: var(--tag-lpurple, #a99cf0); }
.scribble[data-tag="lyellow"] { --_tag: var(--tag-lyellow, #f4f0a0); }
.scribble[data-tag="mint"] { --_tag: var(--tag-mint, #5ad6b8); }
.scribble[data-tag="orange"] { --_tag: var(--tag-orange, #ff9a2a); }
.scribble[data-tag="pink"] { --_tag: var(--tag-pink, #ff6ad6); }
.scribble[data-tag="purple"] { --_tag: var(--tag-purple, #8a2ab8); --_tag-fg: #ffffff; }
.scribble[data-tag="red"] { --_tag: var(--tag-red, #e0201a); --_tag-fg: #ffffff; }
.scribble[data-tag="teal"] { --_tag: var(--tag-teal, #108a8a); --_tag-fg: #ffffff; }
.scribble[data-tag="white"] { --_tag: var(--tag-white, #f2f2f2); }
.scribble[data-tag="yellow"] { --_tag: var(--tag-yellow, #f2d22e); }

/* --- Assignable panel: page counter, unassigned columns, navigation -------- */
.assignable-page { position: absolute; inset-block-start: 0.3rem; inset-inline-start: 0.5rem; font-weight: 800; }
.assignable { position: relative; }
.assignable:has(> .assignable-page) { padding-inline-start: 2.2rem; }
.assignable-col.is-off { opacity: 0.35; }
.assignable-col.is-off :is(input, label) { pointer-events: none; }
.assignable-nav {
  display: grid;
  grid-template-columns: repeat(3, 1.6rem);
  grid-template-rows: repeat(4, 0.9rem);
  gap: 0.3rem 0.2rem;
  align-self: center;
  justify-self: center;
}
.assignable-nav > button {
  min-inline-size: 0;
  padding: 0;
  background: var(--assignable-key-bg, linear-gradient(180deg, #4a54a0, #3a438b));
  border: 1px solid var(--assignable-key-border, #6a74c0);
  border-radius: 2px;
}
.assignable-nav > button:active { background: var(--assignable-key-on, #b5bce8); }
.assignable-nav > :nth-child(1) { grid-area: 1 / 1 / 2 / 3; inline-size: 1.6rem; }
.assignable-nav > :nth-child(2) { grid-area: 1 / 2 / 2 / 4; inline-size: 1.6rem; justify-self: end; }
.assignable-nav > :nth-child(3) { grid-area: 2 / 2; }
.assignable-nav > :nth-child(4) { grid-area: 3 / 1; }
.assignable-nav > :nth-child(5) { grid-area: 3 / 3; }
.assignable-nav > :nth-child(6) { grid-area: 4 / 2; }
/* ==========================================================================
 * Desktop shell (v5, PLAN.md §23 "Signature navigation per theme")
 *
 * One markup that each theme turns into its own operating-system shell:
 * a wallpaper with desktop icons, windows that open, minimize, maximize,
 * close and come to the front, and a bar with a Start menu, task buttons
 * and a tray. No JavaScript: every state is a radio or checkbox read with
 * :has(), and the Start menu is a popover holding the nested-list menus.
 *
 *   body.app.desktop
 *     main.app-main.desktop-screen   wallpaper, .desktop-icons, .desktop-window…
 *     footer.app-status.taskbar.desktop-bar
 *       .taskbar-start  +  .start-menu[popover]
 *       nav.desktop-dock > ul.desktop-launchers + ul.taskbar-tasks
 *       .taskbar-tray
 *
 * The arrangement is a token, --desktop-shell, read with container style
 * queries (every element is a style container for its children):
 *   taskbar  (default) Start button, task buttons and tray along the bottom.
 *   dock     a menu bar along the top (the Start menu is its first menu,
 *            the tray its right end) and a Dock at the bottom that
 *            magnifies under the pointer.
 *   home     a touch home screen: an icon grid with page dots, a Dock of
 *            launchers, a status bar on top, and one app at a time filling
 *            the screen. Every theme switches to it on the Mobile tier.
 * Browsers without style queries keep the taskbar arrangement.
 *
 * Window state, per window (inputs are the first children of the
 * .desktop-window, so a label anywhere on the page can drive them and the
 * window can read them with :has()):
 *   .window-app       radio, name shared by every window and .desktop-home:
 *                     the switched-to window (front, active, pressed task;
 *                     the open app in the home arrangement)
 *   .window-open / .window-minimize / .window-close
 *                     one radio group per window
 *   .window-max       checkbox immediately before the .modal (core's
 *                     maximize rule)
 * Task button N and window N pair up by position (up to 12), as .tabset
 * pairs tabs and panels.
 * ======================================================================== */

.desktop {
  --_shell-theme: var(--desktop-shell, taskbar);
  --_shell: var(--desktop-shell, taskbar);
  position: relative;
}
/* Doubled so it outranks the shell's own .app background. */
.desktop.desktop { background: var(--desktop-bg, var(--app-bg, transparent)); }
@media (max-width: 480px) {
  .desktop { --_shell: home; }
}

/* --- The screen ----------------------------------------------------------- */
.desktop-screen {
  position: relative;
  isolation: isolate;
  min-block-size: var(--desktop-min-height, 30rem);
}
.desktop-home,
.desktop-window > input,
.desktop-icon input {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
  width: 1px;
  height: 1px;
  margin: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  opacity: 0;
}

/* --- Desktop icons (also a window's large-icon view) ----------------------- */
.desktop-icons {
  display: grid;
  grid-template-columns: repeat(auto-fill, var(--desktop-icon-cell, 5.75rem));
  grid-auto-rows: min-content;
  gap: var(--desktop-icon-gap, 0.35rem);
  align-content: start;
  padding: var(--desktop-icons-pad, 0.5rem);
}
/* On the wallpaper the icons run down the left edge in columns. */
.desktop-screen > .desktop-icons {
  position: absolute;
  inset: var(--desktop-inset-top, 0px) 0 var(--desktop-inset-bottom, 0px);
  z-index: 0;
  grid-template-columns: none;
  grid-template-rows: repeat(auto-fill, minmax(var(--desktop-icon-row, 5rem), auto));
  grid-auto-flow: column;
  grid-auto-columns: var(--desktop-icon-cell, 5.75rem);
  justify-content: var(--desktop-icons-justify, start);
}
.desktop-icon {
  position: relative;
  min-inline-size: 0;
}
.desktop-icon > label:first-child {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.3rem;
  min-block-size: var(--tap-min);
  padding: 0.25rem 0.15rem;
  text-align: center;
  cursor: var(--cursor-default, default);
  user-select: none;
}
.desktop-icon .icon {
  flex: none;
  width: var(--desktop-icon-size, 2rem);
  height: var(--desktop-icon-size, 2rem);
  padding: var(--desktop-icon-tile-pad, 0);
  border-radius: var(--desktop-icon-tile-radius, 0);
  background: var(--desktop-icon-tile, transparent);
  color: var(--window-icon-glyph, var(--accent));
}
.desktop-screen > .desktop-icons .icon {
  color: var(--desktop-icon-glyph, var(--accent));
  box-shadow: var(--desktop-icon-tile-shadow, none);
  filter: var(--desktop-icon-filter, none);
}
.desktop-icon > label > span {
  max-inline-size: 100%;
  padding: 0.25rem;
  overflow-wrap: break-word;
  hyphens: auto;
  font-size: var(--desktop-icon-font-size, 0.8rem);
  line-height: 1.25;
  border: 1px dotted transparent;
  border-radius: var(--desktop-icon-label-radius, 0);
}
.desktop-screen > .desktop-icons .desktop-icon > label > span {
  color: var(--desktop-icon-fg, var(--text));
  background: var(--desktop-icon-label-bg, transparent);
  text-shadow: var(--desktop-icon-text-shadow, none);
}
.desktop-icon:has(input:checked) > label > span,
.desktop-screen > .desktop-icons .desktop-icon:has(input:checked) > label > span {
  background: var(--desktop-icon-label-bg-selected, var(--accent));
  color: var(--desktop-icon-label-fg-selected, var(--on-accent));
  text-shadow: none;
}
.desktop-screen > .desktop-icons .desktop-icon:has(input:checked) .icon {
  color: var(--desktop-icon-glyph-selected, var(--desktop-icon-glyph, var(--accent)));
  filter: var(--desktop-icon-filter-selected, var(--desktop-icon-filter, none));
}
.desktop-icon:has(input:focus-visible) > label > span { border-color: currentColor; }
/* A selected icon opens with a second click (a double-click without
   script); in the home arrangement one tap opens it. */
:is(.desktop-icon-open, .desktop-icon-launch) {
  position: absolute;
  inset: 0;
  display: none;
  cursor: var(--cursor-default, default);
}
.desktop-icon:has(input:checked) > .desktop-icon-open { display: block; }

/* --- Windows --------------------------------------------------------------- */
.desktop-window {
  position: absolute;
  z-index: var(--z-raised);
  inset-inline-start: min(var(--x, 2rem), 100% - 12rem);
  inset-block-start: var(--y, 2rem);
  inline-size: min(var(--w, 30rem), 100% - min(var(--x, 2rem), 100% - 12rem) - 0.5rem);
  block-size: min(var(--h, 20rem), 100% - var(--y, 2rem) - var(--desktop-inset-top, 0px) - var(--desktop-inset-bottom, 0px) - 0.5rem);
  min-block-size: 10rem;
  /* A theme whose bars float over the screen keeps windows clear of them. */
  margin-block-start: var(--desktop-inset-top, 0px);
  --modal-pad: var(--window-pad, 3px);
  /* The box can outlive its window (minimized, or an app not shown on the
     home screen); only the window itself takes the pointer. */
  pointer-events: none;
}
.desktop-window > .modal { pointer-events: auto; }
.desktop .desktop-window > .modal {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: none;
  height: 100%;
  max-height: none;
  overflow: hidden;
}
.desktop-window > .modal > .modal-header {
  flex: none;
  margin: calc(-1 * var(--surface-pad, 0px)) calc(-1 * var(--surface-pad, 0px)) 0;
  gap: 0.35rem;
  padding: var(--window-header-pad, var(--modal-header-pad, 0.2rem 0.3rem));
}
.desktop-window .modal-header > .icon { flex: none; width: 1.1em; height: 1.1em; }
.window-title {
  flex: 1 1 auto;
  min-inline-size: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.desktop-window .menubar { flex: none; }
.window-body {
  flex: 1 1 auto;
  min-block-size: 0;
  overflow: auto;
  padding: var(--window-body-pad, var(--space-s));
}
.desktop-window .tree :is(summary, li > a, li > button, li > span) { padding-block: 0.25rem; }
.window-body.explorer,
.window-body.notepad { padding: 0; }
.window-status {
  flex: none;
  display: flex;
  gap: 0.2rem;
  padding: 0.15rem;
  font-size: 0.8em;
  color: var(--muted);
  border-block-start: 1px solid var(--hairline);
}
.window-status > * {
  flex: 1 1 0;
  min-inline-size: 0;
  padding: 0.25rem 0.4rem;
  border: 1px solid;
  border-color: var(--window-status-field-border, transparent);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.window-home { display: none; }
.desktop-window .modal-footer { margin-block-start: var(--space-s); }

/* A pane inside a window body (an explorer's tree and file list): the
   theme's field look, as a list view is a field. */
.window-pane {
  background: var(--window-pane-bg, var(--input-bg, var(--surface-2)));
  color: var(--window-pane-fg, var(--input-fg, var(--text)));
  border: var(--input-border-width, 1px) solid;
  border-color: var(--window-pane-border, var(--input-border, var(--border)));
}

/* Minimized and closed windows leave the screen (the inputs stay, so the
   task button and the Start menu can bring them back). */
.desktop-window:has(> .window-minimize:checked, > .window-close:checked) > .modal { display: none; }
/* Front: the switched-to window, then whichever window holds focus. */
.desktop-window:has(> .window-app:checked) { z-index: calc(var(--z-raised) + 2); }
.desktop-window:focus-within { z-index: calc(var(--z-raised) + 3); }
.desktop-window:has(> .window-max:checked) {
  inset: var(--desktop-inset-top, 0px) 0 var(--desktop-inset-bottom, 0px);
  margin: 0;
  inline-size: auto;
  block-size: auto;
}
/* Keyboard focus on a window's state radios rings the window. */
.desktop-window:has(> input:focus-visible) > .modal {
  outline: 2px solid var(--focus, var(--accent));
  outline-offset: 2px;
}
/* Inactive title bars: every window but the active one, which is the one
   holding focus, else the switched-to one, else the topmost (last). */
.desktop:has(.desktop-window:focus-within) .desktop-window:not(:focus-within),
.desktop:not(:has(.desktop-window:focus-within)):has(.window-app:checked) .desktop-window:not(:has(> .window-app:checked)),
.desktop:not(:has(.desktop-window:focus-within, .window-app:checked)) .desktop-window:not(:nth-last-child(1 of .desktop-window)) {
  --_inactive: 1;
}
.desktop:has(.desktop-window:focus-within) .desktop-window:not(:focus-within) > .modal > .modal-header,
.desktop:not(:has(.desktop-window:focus-within)):has(.window-app:checked) .desktop-window:not(:has(> .window-app:checked)) > .modal > .modal-header,
.desktop:not(:has(.desktop-window:focus-within, .window-app:checked)) .desktop-window:not(:nth-last-child(1 of .desktop-window)) > .modal > .modal-header {
  background: var(--window-header-bg-inactive, var(--modal-header-bg, transparent));
  color: var(--window-header-fg-inactive, var(--modal-header-fg, inherit));
  filter: var(--window-header-filter-inactive, saturate(0.45) opacity(0.85));
}

/* A small monitor showing the wallpaper (Display Properties). */
.desktop-monitor {
  inline-size: 9rem;
  padding: 0.55rem 0.55rem 1.1rem;
  border-radius: var(--desktop-monitor-radius, 0.4rem);
  background: var(--desktop-monitor-bg, var(--surface-2));
  border: 1px solid var(--border);
  box-shadow: var(--panel-shadow, none);
}
.desktop-monitor-screen {
  aspect-ratio: 4 / 3;
  background: var(--desktop-bg, var(--bg));
  background-size: cover;
  border: 1px solid var(--border);
}

/* --- The bar --------------------------------------------------------------- */
.desktop .desktop-bar {
  position: relative;
  z-index: var(--z-sticky);
  gap: var(--taskbar-gap, 0.3rem);
  min-block-size: var(--taskbar-height, 2.5rem);
  padding: var(--taskbar-padding, 0.2rem 0.3rem);
  background: var(--taskbar-bg, var(--app-status-bg, var(--surface)));
  color: var(--taskbar-fg, var(--app-status-fg, var(--text)));
  font-size: var(--taskbar-font-size, 0.85rem);
}
.desktop-bar .taskbar-start { align-self: stretch; anchor-name: --desktop-start; }
.taskbar-start .icon { width: 1.1em; height: 1.1em; }
.desktop:has(.start-menu:popover-open) .taskbar-start {
  background: var(--taskbar-start-bg-open, var(--taskbar-start-bg, transparent));
  border-color: var(--taskbar-start-border-open, var(--taskbar-start-border, transparent));
  box-shadow: var(--taskbar-start-shadow-open, var(--taskbar-start-shadow, none));
  color: var(--taskbar-start-fg-open, var(--taskbar-start-fg, inherit));
  outline: var(--taskbar-start-outline-open, none);
  outline-offset: -4px;
}
.desktop-dock {
  display: flex;
  align-items: stretch;
  gap: var(--taskbar-gap, 0.3rem);
  flex: 1 1 auto;
  min-inline-size: 0;
  align-self: stretch;
}
.desktop-launchers,
.taskbar-tasks {
  display: flex;
  align-items: stretch;
  gap: var(--taskbar-gap, 0.3rem);
  min-inline-size: 0;
  margin: 0;
  padding: 0;
  list-style: none;
}
/* Quick Launch: small icon buttons beside Start. */
.desktop-launchers {
  display: var(--desktop-launchers-display, flex);
  flex: none;
  padding-inline-end: var(--taskbar-gap, 0.3rem);
  border-inline-end: 1px solid var(--desktop-launchers-rule, var(--hairline));
}
.desktop-launchers > .is-trash { display: none; }
.dock-item {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: var(--tap-min);
  block-size: 100%;
  color: inherit;
  text-decoration: none;
  border-radius: var(--radius);
}
.desktop-bar .dock-item .icon { width: 1.15rem; height: 1.15rem; }
@media (hover: hover) {
  .desktop-launchers .dock-item:hover { background: var(--taskbar-task-bg-active, var(--row-hover-bg, rgba(128, 128, 128, 0.2))); }
}
.taskbar-tasks { flex: 1 1 auto; }
.taskbar-tasks > li {
  position: relative;
  display: flex;
  flex: 0 1 var(--taskbar-task-max, 10.5rem);
  min-inline-size: 2.5rem;
}
.desktop-bar .taskbar-task {
  flex: 1 1 auto;
  max-width: none;
  cursor: var(--cursor-default, default);
}
.taskbar-task .icon { flex: none; width: 1.1em; height: 1.1em; }
.taskbar-restore {
  position: absolute;
  inset: 0;
  display: none;
  cursor: var(--cursor-default, default);
}
.desktop-bar .taskbar-tray { align-self: stretch; }
.taskbar-tray .icon { width: 1.05em; height: 1.05em; }
.taskbar-tray .is-battery { display: var(--desktop-battery-display, none); }
.desktop-pages { display: none; }

/* --- The Start menu ---------------------------------------------------------
 * One popover holding a header, the programs list, the places list and a
 * footer list. Single column by default; --start-menu-layout: columns puts
 * programs and places side by side under the header (and shows the items
 * marked .is-columns-only); classic turns the header into a vertical banner
 * down the left edge (--start-menu-banner is its text). */
.start-menu[popover] {
  position: fixed;
  inset: auto auto calc(var(--taskbar-height, 2.5rem) + 0.1rem) 0.2rem;
  margin: 0;
  padding: 0;
  overflow: visible;
  width: max-content;
  max-width: min(30rem, calc(100vw - 0.5rem));
  max-height: none;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-areas: "banner header" "banner programs" "banner places" "banner footer";
  border: var(--start-menu-border-width, var(--context-menu-border-width, 1px)) solid;
  border-color: var(--start-menu-border, var(--context-menu-border, var(--dropdown-border, var(--border))));
  border-radius: var(--start-menu-radius, var(--context-menu-radius, var(--dropdown-radius, var(--radius))));
  background: var(--start-menu-bg, var(--context-menu-bg, var(--dropdown-bg, var(--surface-2))));
  box-shadow: var(--start-menu-shadow, var(--context-menu-shadow, var(--panel-shadow, 0 2px 12px rgba(0, 0, 0, 0.3))));
  color: var(--context-menu-item-fg, var(--text));
  font-size: var(--start-menu-font-size, 0.9rem);
}
.start-menu[popover]:not(:popover-open) { display: none; }
/* A bar's own child alignment (a theme centring status items) must not
   move the menu inside its anchored area. */
.desktop .desktop-bar > .start-menu[popover] { align-self: normal; justify-self: normal; }
@supports (position-area: block-start) {
  .start-menu[popover] {
    inset: auto;
    margin: 0;
    position-anchor: --desktop-start;
    position-area: block-start span-inline-end;
    position-try-fallbacks: flip-block;
  }
}
.start-menu-header {
  grid-area: header;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.5rem 0.7rem;
  background: var(--start-menu-header-bg, transparent);
  color: var(--start-menu-header-fg, inherit);
  font-weight: 700;
  border-block-end: 1px solid var(--start-menu-rule, var(--hairline));
  border-radius: var(--start-menu-header-radius, 0);
}
.start-menu-header .avatar { flex: none; }
.desktop .start-menu > ul.context-menu {
  position: static;
  min-width: 0;
  margin: 0;
  padding: 0.3rem;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  background: transparent;
  list-style: none;
}
.start-menu-programs { grid-area: programs; }
.start-menu-places { grid-area: places; }
.start-menu-footer {
  grid-area: footer;
  border-block-start: 1px solid var(--start-menu-rule, var(--hairline)) !important;
}
.start-menu .is-columns-only { display: none; }
/* Items: icon, text, then the submenu marker at the end. Labels drive the
   window radios, so they get the menu item look too. */
.desktop .start-menu li > :is(a, button, label) {
  justify-content: flex-start;
  gap: 0.6em;
  text-decoration: none;
}
.start-menu li > label,
.desktop-window .menubar li li > label {
  display: flex;
  align-items: center;
  gap: 0.6em;
  width: 100%;
  min-block-size: var(--tap-min);
  padding: 0.45em 0.7em;
  border-radius: var(--context-menu-radius, var(--radius));
  color: var(--context-menu-item-fg, var(--text));
  cursor: var(--cursor-default, default);
}
@media (hover: hover) {
  .start-menu li > label:hover,
  .desktop-window .menubar li li > label:hover {
    background: var(--context-menu-item-bg-hover, var(--dropdown-item-bg-hover, var(--accent)));
    color: var(--context-menu-item-fg-hover, var(--dropdown-item-fg-hover, var(--on-accent)));
  }
}
.start-menu li > :is(a, button, label) > .icon {
  flex: none;
  width: var(--start-menu-icon-size, 1.25rem);
  height: var(--start-menu-icon-size, 1.25rem);
}
.start-menu li li > :is(a, button, label) > .icon {
  width: var(--start-menu-sub-icon-size, 1rem);
  height: var(--start-menu-sub-icon-size, 1rem);
}
.start-menu li > :is(a, button, label) > span { display: flex; flex-direction: column; text-align: start; }
.start-menu li > :is(a, button, label) > span > b { font-weight: 700; }
.start-menu li > :is(a, button, label) small { font-size: 0.8em; opacity: 0.75; }
.start-menu .has-submenu > :is(a, button)::after { margin-inline-start: auto; }
.start-menu li > :is(a, button, label) > span { white-space: nowrap; }
/* Flyouts cross the neighbouring column (its items are positioned too). */
.start-menu .has-submenu > ul { z-index: calc(var(--z-raised) + 1); }

/* Classic: the header is a banner down the left edge. */
@container style(--start-menu-layout: classic) {
  .start-menu-header {
    grid-area: banner;
    display: block;
    padding: 0.4rem 0.15rem;
    writing-mode: vertical-rl;
    transform: rotate(180deg);
    border: 0;
    background: var(--start-menu-banner-bg, var(--accent));
    color: var(--start-menu-banner-fg, var(--on-accent));
    font-size: 1.35rem;
    line-height: 1.2;
    white-space: nowrap;
  }
  .start-menu-header > * { display: none; }
  .start-menu-header::before { content: var(--start-menu-banner, ""); }
  .start-menu .context-menu { padding-block: 0.1rem; }
  .start-menu-places { border-block-start: 0; }
}
/* Columns: programs on the left, places on the right, header and footer
   across both. */
@container style(--start-menu-layout: columns) {
  .start-menu[popover] {
    grid-template-columns: minmax(12rem, auto) minmax(11rem, auto);
    grid-template-areas: "header header" "programs places" "footer footer";
    max-width: min(34rem, calc(100vw - 0.5rem));
  }
  .start-menu .is-columns-only { display: revert; }
  .start-menu span.is-columns-only { display: inline; }
  .start-menu .is-classic-only { display: none; }
  .desktop .start-menu > ul.start-menu-programs { background: var(--start-menu-programs-bg, transparent); }
  .desktop .start-menu > ul.start-menu-places {
    background: var(--start-menu-places-bg, transparent);
    border-inline-start: 1px solid var(--start-menu-rule, var(--hairline));
  }
  .desktop .start-menu > ul.start-menu-footer {
    display: flex;
    justify-content: flex-end;
    gap: 0.4rem;
    background: var(--start-menu-footer-bg, transparent);
    border-radius: var(--start-menu-footer-radius, 0);
  }
  .desktop .start-menu-footer > li > :is(a, button) {
    color: var(--start-menu-footer-fg, inherit);
    width: auto;
  }
}

/* --- Task buttons follow their windows (positional, up to 12) --------------
 * Pressed: the active window's task. Hidden: closed windows' tasks.
 * Restore: a minimized window's task restores it. Generated in pairs so
 * window N always maps to task N.
 * ponytail: CSS can't match a task to its window by name, so pairing is by
 * position with a fixed count; raise N here if a desktop needs more. */
.desktop:has(.desktop-window:nth-child(1 of .desktop-window):focus-within > .window-open:checked) .taskbar-tasks > li:nth-child(1) > .taskbar-task,
.desktop:not(:has(.desktop-window:focus-within)):has(.desktop-window:nth-child(1 of .desktop-window) > .window-app:checked ~ .window-open:checked) .taskbar-tasks > li:nth-child(1) > .taskbar-task,
.desktop:has(.desktop-window:nth-child(2 of .desktop-window):focus-within > .window-open:checked) .taskbar-tasks > li:nth-child(2) > .taskbar-task,
.desktop:not(:has(.desktop-window:focus-within)):has(.desktop-window:nth-child(2 of .desktop-window) > .window-app:checked ~ .window-open:checked) .taskbar-tasks > li:nth-child(2) > .taskbar-task,
.desktop:has(.desktop-window:nth-child(3 of .desktop-window):focus-within > .window-open:checked) .taskbar-tasks > li:nth-child(3) > .taskbar-task,
.desktop:not(:has(.desktop-window:focus-within)):has(.desktop-window:nth-child(3 of .desktop-window) > .window-app:checked ~ .window-open:checked) .taskbar-tasks > li:nth-child(3) > .taskbar-task,
.desktop:has(.desktop-window:nth-child(4 of .desktop-window):focus-within > .window-open:checked) .taskbar-tasks > li:nth-child(4) > .taskbar-task,
.desktop:not(:has(.desktop-window:focus-within)):has(.desktop-window:nth-child(4 of .desktop-window) > .window-app:checked ~ .window-open:checked) .taskbar-tasks > li:nth-child(4) > .taskbar-task,
.desktop:has(.desktop-window:nth-child(5 of .desktop-window):focus-within > .window-open:checked) .taskbar-tasks > li:nth-child(5) > .taskbar-task,
.desktop:not(:has(.desktop-window:focus-within)):has(.desktop-window:nth-child(5 of .desktop-window) > .window-app:checked ~ .window-open:checked) .taskbar-tasks > li:nth-child(5) > .taskbar-task,
.desktop:has(.desktop-window:nth-child(6 of .desktop-window):focus-within > .window-open:checked) .taskbar-tasks > li:nth-child(6) > .taskbar-task,
.desktop:not(:has(.desktop-window:focus-within)):has(.desktop-window:nth-child(6 of .desktop-window) > .window-app:checked ~ .window-open:checked) .taskbar-tasks > li:nth-child(6) > .taskbar-task,
.desktop:has(.desktop-window:nth-child(7 of .desktop-window):focus-within > .window-open:checked) .taskbar-tasks > li:nth-child(7) > .taskbar-task,
.desktop:not(:has(.desktop-window:focus-within)):has(.desktop-window:nth-child(7 of .desktop-window) > .window-app:checked ~ .window-open:checked) .taskbar-tasks > li:nth-child(7) > .taskbar-task,
.desktop:has(.desktop-window:nth-child(8 of .desktop-window):focus-within > .window-open:checked) .taskbar-tasks > li:nth-child(8) > .taskbar-task,
.desktop:not(:has(.desktop-window:focus-within)):has(.desktop-window:nth-child(8 of .desktop-window) > .window-app:checked ~ .window-open:checked) .taskbar-tasks > li:nth-child(8) > .taskbar-task,
.desktop:has(.desktop-window:nth-child(9 of .desktop-window):focus-within > .window-open:checked) .taskbar-tasks > li:nth-child(9) > .taskbar-task,
.desktop:not(:has(.desktop-window:focus-within)):has(.desktop-window:nth-child(9 of .desktop-window) > .window-app:checked ~ .window-open:checked) .taskbar-tasks > li:nth-child(9) > .taskbar-task,
.desktop:has(.desktop-window:nth-child(10 of .desktop-window):focus-within > .window-open:checked) .taskbar-tasks > li:nth-child(10) > .taskbar-task,
.desktop:not(:has(.desktop-window:focus-within)):has(.desktop-window:nth-child(10 of .desktop-window) > .window-app:checked ~ .window-open:checked) .taskbar-tasks > li:nth-child(10) > .taskbar-task,
.desktop:has(.desktop-window:nth-child(11 of .desktop-window):focus-within > .window-open:checked) .taskbar-tasks > li:nth-child(11) > .taskbar-task,
.desktop:not(:has(.desktop-window:focus-within)):has(.desktop-window:nth-child(11 of .desktop-window) > .window-app:checked ~ .window-open:checked) .taskbar-tasks > li:nth-child(11) > .taskbar-task,
.desktop:has(.desktop-window:nth-child(12 of .desktop-window):focus-within > .window-open:checked) .taskbar-tasks > li:nth-child(12) > .taskbar-task,
.desktop:not(:has(.desktop-window:focus-within)):has(.desktop-window:nth-child(12 of .desktop-window) > .window-app:checked ~ .window-open:checked) .taskbar-tasks > li:nth-child(12) > .taskbar-task,
.desktop:not(:has(.desktop-window:focus-within, .window-app:checked)):has(.desktop-window:nth-last-child(1 of .desktop-window) > .window-open:checked) .taskbar-tasks > li:last-child > .taskbar-task {
  background: var(--taskbar-task-bg-active, var(--taskbar-task-bg, transparent));
  border-color: var(--taskbar-task-border-active, var(--taskbar-task-border, transparent));
  box-shadow: var(--taskbar-task-shadow-active, var(--taskbar-task-shadow, none));
  outline: var(--taskbar-task-outline-active, none);
  outline-offset: -4px;
  font-weight: var(--taskbar-task-weight-active, inherit);
  --_task-active: 1;
}
.desktop:has(.desktop-window:nth-child(1 of .desktop-window) > .window-close:checked) .taskbar-tasks > li:nth-child(1),
.desktop:has(.desktop-window:nth-child(2 of .desktop-window) > .window-close:checked) .taskbar-tasks > li:nth-child(2),
.desktop:has(.desktop-window:nth-child(3 of .desktop-window) > .window-close:checked) .taskbar-tasks > li:nth-child(3),
.desktop:has(.desktop-window:nth-child(4 of .desktop-window) > .window-close:checked) .taskbar-tasks > li:nth-child(4),
.desktop:has(.desktop-window:nth-child(5 of .desktop-window) > .window-close:checked) .taskbar-tasks > li:nth-child(5),
.desktop:has(.desktop-window:nth-child(6 of .desktop-window) > .window-close:checked) .taskbar-tasks > li:nth-child(6),
.desktop:has(.desktop-window:nth-child(7 of .desktop-window) > .window-close:checked) .taskbar-tasks > li:nth-child(7),
.desktop:has(.desktop-window:nth-child(8 of .desktop-window) > .window-close:checked) .taskbar-tasks > li:nth-child(8),
.desktop:has(.desktop-window:nth-child(9 of .desktop-window) > .window-close:checked) .taskbar-tasks > li:nth-child(9),
.desktop:has(.desktop-window:nth-child(10 of .desktop-window) > .window-close:checked) .taskbar-tasks > li:nth-child(10),
.desktop:has(.desktop-window:nth-child(11 of .desktop-window) > .window-close:checked) .taskbar-tasks > li:nth-child(11),
.desktop:has(.desktop-window:nth-child(12 of .desktop-window) > .window-close:checked) .taskbar-tasks > li:nth-child(12) { display: none; }
.desktop:has(.desktop-window:nth-child(1 of .desktop-window) > .window-minimize:checked) .taskbar-tasks > li:nth-child(1) > .taskbar-restore,
.desktop:has(.desktop-window:nth-child(2 of .desktop-window) > .window-minimize:checked) .taskbar-tasks > li:nth-child(2) > .taskbar-restore,
.desktop:has(.desktop-window:nth-child(3 of .desktop-window) > .window-minimize:checked) .taskbar-tasks > li:nth-child(3) > .taskbar-restore,
.desktop:has(.desktop-window:nth-child(4 of .desktop-window) > .window-minimize:checked) .taskbar-tasks > li:nth-child(4) > .taskbar-restore,
.desktop:has(.desktop-window:nth-child(5 of .desktop-window) > .window-minimize:checked) .taskbar-tasks > li:nth-child(5) > .taskbar-restore,
.desktop:has(.desktop-window:nth-child(6 of .desktop-window) > .window-minimize:checked) .taskbar-tasks > li:nth-child(6) > .taskbar-restore,
.desktop:has(.desktop-window:nth-child(7 of .desktop-window) > .window-minimize:checked) .taskbar-tasks > li:nth-child(7) > .taskbar-restore,
.desktop:has(.desktop-window:nth-child(8 of .desktop-window) > .window-minimize:checked) .taskbar-tasks > li:nth-child(8) > .taskbar-restore,
.desktop:has(.desktop-window:nth-child(9 of .desktop-window) > .window-minimize:checked) .taskbar-tasks > li:nth-child(9) > .taskbar-restore,
.desktop:has(.desktop-window:nth-child(10 of .desktop-window) > .window-minimize:checked) .taskbar-tasks > li:nth-child(10) > .taskbar-restore,
.desktop:has(.desktop-window:nth-child(11 of .desktop-window) > .window-minimize:checked) .taskbar-tasks > li:nth-child(11) > .taskbar-restore,
.desktop:has(.desktop-window:nth-child(12 of .desktop-window) > .window-minimize:checked) .taskbar-tasks > li:nth-child(12) > .taskbar-restore { display: block; }

/* --- The dock arrangement --------------------------------------------------- */
@container style(--_shell: dock) {
  .desktop .desktop-bar { position: static; }
  /* The bar becomes a transparent strip; the Dock sits centred in it. */
  .desktop-bar {
    justify-content: center;
    min-block-size: 0;
    padding-block: 0 var(--dock-gap-below, 0.35rem);
    border: 0;
  }
  .desktop .desktop-bar { background: transparent; box-shadow: none; }
  /* Start and the tray move up into the menu bar (the app bar). */
  .desktop-bar > .taskbar-start,
  .desktop-bar > .taskbar-tray {
    position: absolute;
    inset-block-start: var(--app-padding, 0);
    block-size: var(--desktop-menubar-height, 1.6rem);
    min-block-size: 0;
    min-inline-size: var(--tap-min);
    padding-block: 0;
    background: transparent;
    border: 0;
    box-shadow: none;
    color: var(--app-bar-fg, var(--text));
    z-index: calc(var(--z-sticky) + 2);
  }
  .desktop-bar > .taskbar-start {
    inset-inline-start: var(--app-padding, 0);
    margin-inline-start: 0.4rem;
    width: auto;
    padding-inline: 0.5rem;
    border-radius: 0;
    text-shadow: none;
    font-style: normal;
  }
  .desktop-bar > .taskbar-start > .taskbar-start-label {
    position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%);
  }
  .desktop-bar > .taskbar-tray { inset-inline-end: var(--app-padding, 0); margin-inline-end: 0.6rem; }
  .start-menu[popover] { inset: var(--desktop-menubar-height, 1.6rem) auto auto 0.4rem; }
  @supports (position-area: block-end) {
    .start-menu[popover] { inset: auto; position-area: block-end span-inline-end; }
  }
  .desktop-dock {
    flex: none;
    align-items: flex-end;
    gap: 0.15rem;
    padding: 0.25rem 0.45rem 0.15rem;
    border: 1px solid var(--dock-border, rgba(255, 255, 255, 0.45));
    border-radius: var(--dock-radius, 0.6rem);
    background: var(--dock-bg, color-mix(in srgb, var(--surface) 55%, transparent));
    box-shadow: var(--dock-shadow, 0 2px 12px rgba(0, 0, 0, 0.25));
    backdrop-filter: var(--dock-blur, blur(8px));
  }
  .desktop-launchers,
  .taskbar-tasks { display: contents; }
  .desktop-launchers > li,
  .taskbar-tasks > li {
    position: relative;
    display: flex;
    flex: none;
    min-inline-size: 0;
    --_s: 1;
    --taskbar-task-bg-active: transparent;
    --taskbar-task-border-active: transparent;
    --taskbar-task-shadow-active: none;
    --taskbar-task-outline-active: none;
  }
  .desktop-launchers > .is-trash {
    display: flex;
    order: 2;
    margin-inline-start: 0.35rem;
    padding-inline-start: 0.5rem;
    border-inline-start: 1px solid var(--dock-separator, rgba(255, 255, 255, 0.5));
  }
  .taskbar-tasks > li { order: 1; }
  .desktop-bar :is(.desktop-launchers, .taskbar-tasks) > li > :is(.dock-item, .taskbar-task) {
    position: relative;
    flex-direction: column;
    justify-content: flex-end;
    gap: 0;
    width: auto;
    height: auto;
    min-inline-size: 0;
    padding: 0 0 0.45rem;
    border: 0;
    border-radius: 0;
    background: transparent;
    box-shadow: none;
    outline: 0;
    color: var(--text);
  }
  .desktop-bar :is(.desktop-launchers, .taskbar-tasks) > li .icon {
    width: calc(var(--dock-icon-size, 2.9rem) * var(--_s));
    height: calc(var(--dock-icon-size, 2.9rem) * var(--_s));
    padding: calc(var(--dock-icon-size, 2.9rem) * 0.16 * var(--_s));
    border-radius: var(--dock-tile-radius, 22%);
    background: var(--dock-tile, var(--dock-tile-gloss, linear-gradient(transparent, transparent)), linear-gradient(180deg, color-mix(in srgb, var(--_tile, var(--accent)) 70%, #ffffff), var(--_tile, var(--accent))));
    color: var(--dock-glyph, var(--accent));
    box-shadow: var(--dock-tile-shadow, 0 1px 3px rgba(0, 0, 0, 0.35));
  }
  /* The name floats above the icon while it is pointed at. */
  :is(.desktop-launchers, .taskbar-tasks) > li > :is(.dock-item, .taskbar-task) > span {
    position: absolute;
    inset-block-end: calc(100% + 0.3rem);
    inset-inline-start: 50%;
    translate: -50% 0;
    width: auto;
    height: auto;
    margin: 0;
    overflow: visible;
    clip: auto;
    clip-path: none;
    padding: 0.1rem 0.6rem;
    border-radius: 1rem;
    background: var(--dock-label-bg, rgba(0, 0, 0, 0.7));
    color: var(--dock-label-fg, #ffffff);
    font-size: 0.8rem;
    white-space: nowrap;
    opacity: 0;
    pointer-events: none;
  }
  :is(.desktop-launchers, .taskbar-tasks) > li:is(:hover, :focus-within) > :is(.dock-item, .taskbar-task) > span { opacity: 1; }
  /* Running apps carry an indicator under the icon. */
  .taskbar-tasks > li > .taskbar-task::after {
    content: "";
    position: absolute;
    inset-block-end: 0.05rem;
    inset-inline-start: 50%;
    translate: -50% 0;
    width: var(--dock-indicator-size, 0.35rem);
    height: var(--dock-indicator-size, 0.35rem);
    border-radius: var(--dock-indicator-radius, 50%);
    background: var(--dock-indicator, currentColor);
    clip-path: var(--dock-indicator-shape, none);
    box-shadow: var(--dock-indicator-glow, none);
  }
  /* Room in the menu bar for the mark and the tray. */
  .desktop > .app-bar {
    min-block-size: var(--desktop-menubar-height, 1.6rem);
    padding-inline-start: calc(var(--tap-min) + 1rem);
    padding-inline-end: var(--desktop-tray-room, 8.5rem);
  }
  :is(.desktop-launchers, .taskbar-tasks) > li:nth-child(5n + 1) { --_tile: var(--chart-series-1, var(--accent)); }
  :is(.desktop-launchers, .taskbar-tasks) > li:nth-child(5n + 2) { --_tile: var(--chart-series-2, var(--accent)); }
  :is(.desktop-launchers, .taskbar-tasks) > li:nth-child(5n + 3) { --_tile: var(--chart-series-3, var(--accent)); }
  :is(.desktop-launchers, .taskbar-tasks) > li:nth-child(5n + 4) { --_tile: var(--chart-series-4, var(--accent)); }
  :is(.desktop-launchers, .taskbar-tasks) > li:nth-child(5n + 5) { --_tile: var(--chart-series-5, var(--accent)); }
  .desktop-launchers > li.is-trash { --_tile: var(--muted); }
  .desktop-window .menubar { display: var(--dock-window-menubar-display, none); }
  .desktop-screen > .desktop-icons { --desktop-icons-justify: end; }
  .desktop-screen > .desktop-icons .desktop-icon.is-trash { display: none; }
  /* Magnification: the pointed icon and, less, its neighbours. */
  @media (hover: hover) {
    :is(.desktop-launchers, .taskbar-tasks) > li:hover { --_s: var(--dock-magnify, 1.55); }
    .desktop-dock li:has(+ li:hover),
    .desktop-dock li:hover + li { --_s: calc(1 + (var(--dock-magnify, 1.55) - 1) * 0.5); }
    .desktop-launchers > li:last-child:not(.is-trash):has(~ *:hover),
    .desktop-dock:has(.taskbar-tasks > li:first-child:hover) .desktop-launchers > li:nth-last-child(2) { --_s: calc(1 + (var(--dock-magnify, 1.55) - 1) * 0.5); }
  }
  @media (hover: hover) and (prefers-reduced-motion: no-preference) {
    :is(.desktop-launchers, .taskbar-tasks) > li .icon { transition: width 0.15s ease-out, height 0.15s ease-out, padding 0.15s ease-out; }
  }
@media (hover: hover) {
    :root[data-motion="always"] :is(.desktop-launchers, .taskbar-tasks) > li .icon { transition: width 0.15s ease-out, height 0.15s ease-out, padding 0.15s ease-out; }
  }
}
html[data-motion="reduced"] .desktop-dock .icon { transition: none !important; }

/* --- The home arrangement ---------------------------------------------------- */
@container style(--_shell: home) {
  /* Icons: a centred grid of app tiles. */
  .desktop-screen > .desktop-icons {
    position: relative;
    inset: auto;
    grid-auto-flow: row;
    grid-template-rows: none;
    grid-template-columns: repeat(auto-fill, minmax(var(--home-cell-min, 4.5rem), 1fr));
    grid-auto-columns: auto;
    gap: 1.1rem 0.4rem;
    max-inline-size: var(--home-grid-max, 36rem);
    margin-inline: auto;
    padding: var(--desktop-statusbar-height, 1.6rem) 0.6rem 0.5rem;
    justify-content: stretch;
  }
  .desktop-screen > .desktop-icons .desktop-icon.is-trash { display: none; }
  .desktop-screen > .desktop-icons .desktop-icon > label:first-child { gap: 0.35rem; }
  .desktop-screen > .desktop-icons .icon {
    width: var(--home-icon-size, 3.6rem);
    height: var(--home-icon-size, 3.6rem);
    padding: calc(var(--home-icon-size, 3.6rem) * 0.2);
    border-radius: var(--home-tile-radius, 22%);
    background: var(--home-tile, var(--home-tile-gloss, var(--dock-tile-gloss, linear-gradient(transparent, transparent))), linear-gradient(180deg, color-mix(in srgb, var(--_tile, var(--accent)) 75%, #ffffff), var(--_tile, var(--accent))));
    color: var(--home-glyph, #ffffff);
    box-shadow: var(--home-tile-shadow, 0 1px 3px rgba(0, 0, 0, 0.3));
  }
  .desktop-screen > .desktop-icons .desktop-icon:nth-child(6n + 1) { --_tile: var(--chart-series-1, var(--accent)); }
  .desktop-screen > .desktop-icons .desktop-icon:nth-child(6n + 2) { --_tile: var(--chart-series-2, var(--accent)); }
  .desktop-screen > .desktop-icons .desktop-icon:nth-child(6n + 3) { --_tile: var(--chart-series-3, var(--accent)); }
  .desktop-screen > .desktop-icons .desktop-icon:nth-child(6n + 4) { --_tile: var(--chart-series-4, var(--accent)); }
  .desktop-screen > .desktop-icons .desktop-icon:nth-child(6n + 5) { --_tile: var(--chart-series-5, var(--accent)); }
  .desktop-screen > .desktop-icons .desktop-icon:nth-child(6n + 6) { --_tile: var(--chart-series-6, var(--accent)); }
  .desktop-screen > .desktop-icons .desktop-icon > label > span,
  .desktop-screen > .desktop-icons .desktop-icon:has(input:checked) > label > span {
    background: transparent;
    color: var(--home-label-fg, var(--desktop-icon-fg, var(--text)));
    text-shadow: var(--home-label-shadow, var(--desktop-icon-text-shadow, none));
    font-size: var(--home-label-size, 0.75rem);
  }
  .desktop-screen > .desktop-icons .desktop-icon:has(input:checked) .icon { filter: none; }
  .desktop-icon > .desktop-icon-open,
  .desktop-icon:has(input:checked) > .desktop-icon-open { display: none; }
  .desktop-icon > .desktop-icon-launch { display: block; }
  .desktop-pages {
    display: flex;
    justify-content: center;
    gap: 0.5rem;
    padding-block: 0.6rem;
  }
  .desktop-pages > span {
    width: 0.45rem;
    height: 0.45rem;
    border-radius: 50%;
    background: var(--home-dot, color-mix(in srgb, var(--desktop-icon-fg, var(--text)) 35%, transparent));
  }
  .desktop-pages > :is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))) { background: var(--home-dot-active, var(--desktop-icon-fg, var(--text))); }

  /* One app at a time, filling the screen; the home indicator goes back. */
  .desktop-window,
  .desktop-window:has(> .window-max:checked) {
    inset: var(--desktop-inset-top, 0px) 0 var(--desktop-inset-bottom, 0px);
    margin: 0;
    inline-size: auto;
    block-size: auto;
    min-block-size: 0;
    z-index: calc(var(--z-raised) + 3);
  }
  .desktop .desktop-window > .modal { display: none; }
  .desktop .desktop-window:has(> .window-app:checked) > .modal {
    display: flex;
    max-inline-size: var(--home-sheet-max, none);
    margin-inline: auto;
    border-radius: var(--home-sheet-radius, 0);
  }
  .desktop-window > .modal > .modal-header {
    background: var(--modal-header-bg, transparent);
    color: var(--modal-header-fg, inherit);
    filter: none;
  }
  .desktop-window .modal-header > :is(.btn-min, .btn-max, .btn-close) { display: none; }
  .desktop-window .window-title { text-align: center; }
  .window-home {
    display: block;
    flex: none;
    align-self: center;
    width: 8rem;
    height: var(--tap-min);
    background: linear-gradient(currentColor, currentColor) 50% 60% / 100% 0.3rem no-repeat;
    border-radius: 1rem;
    color: var(--home-indicator, var(--text));
    opacity: 0.6;
  }

  /* The bar: a Dock of launchers; the tray becomes the status bar on top. */
  .desktop-bar { justify-content: center; }
  /* Not a containing block, so the tray can rise to the top of the page. */
  .desktop.desktop > .desktop-bar.desktop-bar {
    position: static;
    transform: none;
    filter: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
  .start-menu[popover] { max-height: calc(100dvh - var(--taskbar-height, 2.5rem) - 1rem); overflow: auto; }
  .desktop-bar > .taskbar-tray {
    position: absolute;
    inset-block-start: var(--app-padding, 0);
    inset-inline: var(--app-padding, 0);
    block-size: var(--desktop-statusbar-height, 1.6rem);
    min-block-size: 0;
    margin: 0;
    padding: 0 1rem;
    justify-content: flex-end;
    border: 0;
    border-radius: 0;
    background: var(--home-statusbar-bg, transparent);
    box-shadow: none;
    color: var(--home-statusbar-fg, var(--app-bar-fg, var(--nav-brand-fg, var(--text))));
    font-weight: 600;
    --desktop-battery-display: inline;
  }
  .desktop-bar > .taskbar-tray > time { order: -1; margin-inline-end: auto; }
  .desktop > .app-bar { padding-block-start: calc(var(--desktop-statusbar-height, 1.6rem) + 0.2rem); }
  .taskbar-tasks { display: none; }
  .desktop-dock { flex: 0 1 auto; justify-content: center; }
  .desktop-launchers {
    display: flex;
    gap: 0.9rem;
    padding: 0.5rem 0.9rem;
    border: 0;
    border-radius: var(--dock-radius, 1.4rem);
    background: var(--home-dock-bg, var(--dock-bg, transparent));
  }
  .desktop-launchers > li:nth-child(n + 5) { display: none; }
  .desktop-launchers .dock-item { min-inline-size: 0; }
  .desktop-launchers .dock-item .icon {
    width: var(--home-dock-icon-size, 3.1rem);
    height: var(--home-dock-icon-size, 3.1rem);
    padding: calc(var(--home-dock-icon-size, 3.1rem) * 0.2);
    border-radius: var(--home-tile-radius, 22%);
    background: var(--home-tile, var(--home-tile-gloss, var(--dock-tile-gloss, linear-gradient(transparent, transparent))), linear-gradient(180deg, color-mix(in srgb, var(--_tile, var(--accent)) 75%, #ffffff), var(--_tile, var(--accent))));
    color: var(--home-dock-glyph, var(--home-glyph, #ffffff));
    box-shadow: var(--home-tile-shadow, 0 1px 3px rgba(0, 0, 0, 0.3));
  }
  .desktop-launchers > li:nth-child(4n + 1) { --_tile: var(--chart-series-2, var(--accent)); }
  .desktop-launchers > li:nth-child(4n + 2) { --_tile: var(--chart-series-1, var(--accent)); }
  .desktop-launchers > li:nth-child(4n + 3) { --_tile: var(--chart-series-4, var(--accent)); }
  .desktop-launchers > li:nth-child(4n + 4) { --_tile: var(--chart-series-3, var(--accent)); }
}
/* A home-screen theme has no Start button; a taskbar theme keeps it on a
   phone, so its Start menu is still a tap away. */
@container style(--_shell-theme: home) {
  .desktop-bar > .taskbar-start { display: none; }
}
@container style(--_shell-theme: dock) and style(--_shell: home) {
  .desktop-bar > .taskbar-start { display: none; }
}
/* --- v5 experience layer (PLAN.md §10–§14) ---------------------------------
 * User display settings on <html>, forced colours, motion tokens and the
 * theme-switch transition, the .prefs panel with its accent swatches, the
 * .splash screen, and the themed .empty-state states. Reference script:
 * assets/js/prefs.js. Docs: docs/components/experience.md.
 *
 * User settings override the theme, so the attribute rules use !important
 * on custom properties: a theme's root block (html[data-theme], 0,1,1) and
 * its palette variants (0,2,1) load after core and would otherwise win.
 * Every attribute is optional and falls back to the OS media query. */

/* --- §11 Motion tokens ---------------------------------------------------
 * A theme describes how it moves with these. --motion-enter/--motion-exit
 * are animation shorthands, so a theme can name its own keyframes and
 * timing (steps() for a "decode", a spring for glass). Every animation
 * below sits inside prefers-reduced-motion: no-preference and stops under
 * data-motion="reduced" or "none". */
:root {
  --motion-duration-s: 120ms;
  --motion-duration-m: 200ms;
  --motion-duration-l: 320ms;
  --motion-ease: cubic-bezier(0.2, 0, 0, 1);
  --motion-enter: motion-fade-in var(--motion-duration-m) var(--motion-ease) both;
  --motion-exit: motion-fade-out var(--motion-duration-s) var(--motion-ease) both;
}
@keyframes motion-fade-in { from { opacity: 0; transform: scale(0.98); } }
@keyframes motion-fade-out { to { opacity: 0; } }

/* Theme switching (theme-loader.js runs it through startViewTransition):
   a cross-fade at the theme's own pace. A theme supplies its entrance with
   ::view-transition-new(root). Without the API, or under reduced motion,
   the switch is instant. */
::view-transition-group(root) {
  animation-duration: var(--motion-duration-l);
  animation-timing-function: var(--motion-ease);
}

/* Themed exits: a theme names how it leaves with --motion-leave (one of
   the keywords below). The old theme's stylesheet is gone by the time the
   transition plays, so theme-loader.js copies the keyword to
   data-leaving on <html> before switching, and the keyframes live here.
   The old snapshot plays on top of the new theme, which shows through
   (with its own ::view-transition-new entrance, if it has one). */
:root[data-leaving]::view-transition-old(root) {
  z-index: var(--z-raised);
  mix-blend-mode: normal;
  animation: var(--leave-name) var(--leave-duration, 700ms) var(--leave-ease, ease-in) both;
}
:root[data-leaving]::view-transition-new(root) { mix-blend-mode: normal; }
/* A screen that powers off or crashes goes dark before the new one comes up. */
:root:is([data-leaving="power-off"], [data-leaving="bsod"])::view-transition-image-pair(root) { background: #000; }
:where(:root:is([data-leaving="power-off"], [data-leaving="bsod"]))::view-transition-new(root) {
  animation: leave-reveal var(--leave-duration, 700ms) linear both;
}
:root[data-leaving="power-off"] { --leave-name: leave-power-off; }
:root[data-leaving="bsod"] { --leave-name: leave-bsod; --leave-duration: 900ms; --leave-ease: linear; }
:root[data-leaving="rain"] { --leave-name: leave-rain; --leave-duration: 900ms; }
:root[data-leaving="warp"] { --leave-name: leave-warp; --leave-duration: 550ms; }
:root[data-leaving="shatter"] { --leave-name: leave-shatter; --leave-duration: 800ms; }
:root[data-leaving="drop"] { --leave-name: leave-drop; }
:root[data-leaving="iris"] { --leave-name: leave-iris; --leave-ease: ease-in-out; }
:root[data-leaving="genie"] { --leave-name: leave-genie; --leave-duration: 600ms; }
@property --leave-crack { syntax: "<angle>"; inherits: false; initial-value: 0deg; }
@keyframes leave-reveal { 0%, 70% { opacity: 0; } }
/* A CRT: the picture collapses to a bright line, then a dot. */
@keyframes leave-power-off {
  45% { transform: scale(1, 0.006); filter: brightness(3); }
  75% { transform: scale(0.006, 0.006); filter: brightness(5); opacity: 1; }
  100% { transform: scale(0, 0); opacity: 0; }
}
/* The screen turns crash blue, then is wiped away top down in chunks. */
@keyframes leave-bsod {
  15%, 40% {
    filter: brightness(0.45) sepia(1) hue-rotate(190deg) saturate(9); clip-path: inset(0);
    animation-timing-function: steps(8, end);
  }
  70%, 100% { filter: brightness(0.45) sepia(1) hue-rotate(190deg) saturate(9); clip-path: inset(100% 0 0); }
}
/* Columns of green fall away. */
@keyframes leave-rain {
  0% { mask-position: 0 0, 0 0; }
  20% { filter: sepia(1) hue-rotate(70deg) saturate(6); }
  100% {
    transform: translateY(35%); opacity: 0;
    filter: sepia(1) hue-rotate(70deg) saturate(6) brightness(1.5);
    mask-position: 0 0, 0 100%;
  }
}
:root[data-leaving="rain"]::view-transition-old(root) {
  mask-image: repeating-linear-gradient(90deg, #000 0 7px, #0006 7px 9px, transparent 9px 11px),
    linear-gradient(#000 40%, transparent 70%);
  mask-size: 100% 100%, 100% 300%;
  mask-repeat: no-repeat;
  mask-composite: intersect;
}
/* Stretches into streaks and is gone. */
@keyframes leave-warp {
  25% { transform: scale(0.96); }
  100% { transform: scale(5, 0.4); filter: blur(10px) brightness(2.5); opacity: 0; }
}
/* Cracks open into wedges that drift apart. */
@keyframes leave-shatter {
  15% { --leave-crack: 2deg; transform: none; }
  100% { --leave-crack: 30deg; transform: scale(1.3) rotate(3deg); opacity: 0; }
}
:root[data-leaving="shatter"]::view-transition-old(root) {
  mask-image: repeating-conic-gradient(from 11deg at 46% 54%, #000 0 calc(30deg - var(--leave-crack)), transparent 0 30deg);
}
/* Tips over and falls off the bottom. */
@keyframes leave-drop {
  25% { transform: rotate(4deg); }
  100% { transform: translateY(110%) rotate(18deg); }
}
:root[data-leaving="drop"]::view-transition-old(root) { transform-origin: 0 100%; }
/* A closing circle, as at the end of a film reel. */
@keyframes leave-iris {
  from { clip-path: circle(75% at 50% 50%); }
  to { clip-path: circle(0 at 50% 50%); }
}
/* Pinches down into the bottom of the screen. */
@keyframes leave-genie {
  from { clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%); }
  60% { clip-path: polygon(10% 30%, 90% 30%, 56% 100%, 44% 100%); opacity: 1; }
  to { clip-path: polygon(46% 100%, 54% 100%, 51% 100%, 49% 100%); opacity: 0; }
}

/* --- §10 Text size ------------------------------------------------------
 * Every component is in rem, so the layout follows the root size. A theme
 * whose root size isn't the browser default declares it as
 * --root-font-size (e.g. 17px) so the steps scale from it. */
:root[data-text-size="s"] { font-size: calc(var(--root-font-size, 100%) * 0.875) !important; }
:root[data-text-size="l"] { font-size: calc(var(--root-font-size, 100%) * 1.125) !important; }
:root[data-text-size="xl"] { font-size: calc(var(--root-font-size, 100%) * 1.25) !important; }

/* --- §10 Density --------------------------------------------------------
 * Multiplies the theme's own --density. The theme's value is read on <html>
 * and the product is set on <body>, because a custom property can't refer
 * to itself. */
:root[data-density] { --density-theme: var(--density); }
:root[data-density="compact"] body { --density: calc(var(--density-theme, 1) * 0.8) !important; }
:root[data-density="comfortable"] body { --density: calc(var(--density-theme, 1) * 1.25) !important; }

/* --- §10 Contrast -------------------------------------------------------
 * "high" (also the OS prefers-contrast: more, opted out with "standard")
 * pulls hairlines and muted text up to the theme's border and text colours.
 * core.css has the same rule without !important, which a theme's root block
 * outranks; these make it stick. "more" goes further: every border in the
 * text colour, links underlined, no text glow and no translucency. */
:root:is([data-contrast="high"], [data-contrast="more"]) {
  --hairline: var(--border) !important;
  --muted: var(--text) !important;
}
@media (prefers-contrast: more) {
  :root:not([data-contrast="standard"], [data-contrast="default"]) {
    --hairline: var(--border) !important;
    --muted: var(--text) !important;
  }
}
:root[data-contrast="more"] {
  --border: var(--text) !important;
  --hairline: var(--text) !important;
}
:root[data-contrast="more"] :focus-visible { outline-width: 3px; outline-offset: 3px; }
:root[data-contrast="more"] *,
:root[data-contrast="more"] *::before,
:root[data-contrast="more"] *::after { text-shadow: none !important; }

/* --- §10 Underlined links -----------------------------------------------
 * Running-text links, for themes that tell links apart by colour alone.
 * Links that are controls (buttons, nav, tabs, menu items) keep their
 * shape, which is their affordance. */
:root:is([data-underline-links], [data-contrast="more"])
  a[href]:not(.btn, .nav-item, .nav-brand, .tab, .pagination-item, .dropdown-item,
    .scrollspy-link, .list-item, .card, .stretched-link, [role="menuitem"], [role="tab"]) {
  text-decoration-line: underline !important;
  text-decoration-thickness: max(1px, 0.08em);
  text-underline-offset: 0.18em;
}

/* --- §10 Transparency ---------------------------------------------------
 * Glass is backdrop-filter plus a translucent fill. Core removes the blur
 * everywhere; the fill is the theme's own token, so a translucent theme
 * makes it opaque in its own block (docs/components/experience.md):
 *   html[data-theme="x"][data-transparency="reduced"] { --x-glass: var(--surface); }
 * plus the same inside @media (prefers-reduced-transparency: reduce) on
 * html[data-theme="x"]:not([data-transparency="default"]). */
:root:is([data-transparency="reduced"], [data-contrast="more"]) {
  --overlay-blur: none !important;
}
:root:is([data-transparency="reduced"], [data-contrast="more"]) *,
:root:is([data-transparency="reduced"], [data-contrast="more"]) *::before,
:root:is([data-transparency="reduced"], [data-contrast="more"]) *::after,
:root:is([data-transparency="reduced"], [data-contrast="more"]) *::backdrop {
  -webkit-backdrop-filter: none !important;
  backdrop-filter: none !important;
}
@media (prefers-reduced-transparency: reduce) {
  :root:not([data-transparency="default"]) { --overlay-blur: none !important; }
  :root:not([data-transparency="default"]) *,
  :root:not([data-transparency="default"]) *::before,
  :root:not([data-transparency="default"]) *::after,
  :root:not([data-transparency="default"]) *::backdrop {
    -webkit-backdrop-filter: none !important;
    backdrop-filter: none !important;
  }
}

/* --- §10 Motion ---------------------------------------------------------
 * "reduced" (core.css) pauses animations and zeroes transitions on
 * elements; this also pauses pseudo-elements and the gutter art layer.
 * "none" removes every animation and transition, ambient theme motion
 * (gutter art, glows, scanlines) and view transitions included.
 * "full" is the same as unset: CSS gated on the OS setting still honours
 * it, but theme-loader.js plays the theme-switch transition. */
:root[data-motion="reduced"]::before,
:root[data-motion="reduced"] *::before,
:root[data-motion="reduced"] *::after {
  animation-play-state: paused !important;
}
:root[data-motion="none"]::before,
:root[data-motion="none"]::after,
:root[data-motion="none"] *,
:root[data-motion="none"] *::before,
:root[data-motion="none"] *::after,
:root[data-motion="none"]::view-transition-group(*),
:root[data-motion="none"]::view-transition-old(*),
:root[data-motion="none"]::view-transition-new(*) {
  animation: none !important;
  transition: none !important;
  scroll-behavior: auto !important;
}

/* --- §12 Accent swatches ------------------------------------------------
 * data-accent="<n>" picks the theme's --accent-swatch-<n>. core.css has the
 * same mapping without !important, which every theme's root block
 * outranks; these make the user's pick stick. Only set a number the
 * theme declares (prefs.js checks): an undeclared one leaves --accent
 * invalid, and a swatch must come as a pair (--on-accent-swatch-<n>).
 * Derived colours follow wherever a theme writes them from
 * --accent (color-mix(in oklab, var(--accent) 80%, black)), not as hex. */
:root[data-accent="1"] { --accent: var(--accent-swatch-1) !important; --on-accent: var(--on-accent-swatch-1) !important; }
:root[data-accent="2"] { --accent: var(--accent-swatch-2) !important; --on-accent: var(--on-accent-swatch-2) !important; }
:root[data-accent="3"] { --accent: var(--accent-swatch-3) !important; --on-accent: var(--on-accent-swatch-3) !important; }
:root[data-accent="4"] { --accent: var(--accent-swatch-4) !important; --on-accent: var(--on-accent-swatch-4) !important; }
:root[data-accent="5"] { --accent: var(--accent-swatch-5) !important; --on-accent: var(--on-accent-swatch-5) !important; }
:root[data-accent="6"] { --accent: var(--accent-swatch-6) !important; --on-accent: var(--on-accent-swatch-6) !important; }

/* --- .prefs: the settings panel (§10) -----------------------------------
 * A plain form: one .prefs-item per setting (a fieldset of segmented
 * radios, or a label with a switch). assets/js/prefs.js applies each
 * change as it happens and stores it; an app can store it server-side
 * and render the attributes on <html> instead. */
.prefs {
  display: flex;
  flex-direction: column;
  min-inline-size: 0;
  color: var(--prefs-fg, var(--text));
}
.prefs-item {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-xs) var(--space-m);
  min-inline-size: 0;
  min-block-size: var(--tap-min);
  margin: 0;
  padding: var(--prefs-item-pad, var(--space-s) 0);
  border: 0;
  border-block-end: 1px solid var(--prefs-divider, var(--hairline));
  touch-action: manipulation;
}
.prefs-item:last-of-type { border-block-end: 0; }
/* A floated legend takes part in the fieldset's flex layout like any child. */
.prefs-item > legend { float: inline-start; padding: 0; }
.prefs-label {
  flex: 1 1 12rem;
  min-inline-size: 0;
  color: var(--prefs-label-fg, var(--text));
  font-weight: var(--prefs-label-weight, 500);
  overflow-wrap: anywhere;
}
.prefs-hint {
  display: block;
  margin-block-start: var(--space-3xs);
  color: var(--prefs-hint-fg, var(--muted));
  font-size: 0.82em;
  font-weight: 400;
}
.prefs-item > .segmented { flex-wrap: wrap; max-inline-size: 100%; }
.prefs-item > .switch { flex: none; min-block-size: 0; } /* the row is the target */
.prefs-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--space-xs);
  padding-block-start: var(--space-m);
}

/* Segmented radios: the label is the segment and the radio hides inside
   it (still focusable, arrow keys move between options). */
.prefs .segmented-item {
  position: relative;
  min-inline-size: var(--tap-min);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.prefs .segmented-item > input {
  position: absolute;
  inset: 0;
  margin: 0;
  opacity: 0;
}
.prefs .segmented-item:has(> input:checked) {
  background: var(--segmented-item-bg-active, var(--accent));
  color: var(--segmented-item-fg-active, var(--on-accent));
  font-weight: var(--prefs-checked-weight, 600);
}
.prefs .segmented-item:has(> input:focus-visible) {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}
/* Text-size samples: the letter shows the step it picks. */
.prefs-sample { font-weight: 600; line-height: 1; }
.prefs-sample.is-s { font-size: 0.8em; }
.prefs-sample.is-l { font-size: 1.15em; }
.prefs-sample.is-xl { font-size: 1.35em; }

/* --- Accent swatches (§12) ----------------------------------------------
 * One radio per swatch the theme declares (--accent-swatch-1..6); prefs.js
 * builds them from the theme, so only approved colours are offered. The
 * picked swatch shows a ring and a check mark, never colour alone. */
.prefs-accents {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2xs);
}
.prefs-swatch {
  position: relative;
  display: inline-grid;
  place-items: center;
  min-inline-size: var(--tap-min);
  min-block-size: var(--tap-min);
  touch-action: manipulation;
}
.prefs-swatch > input {
  position: absolute;
  inset: 0;
  margin: 0;
  opacity: 0;
}
.prefs-swatch::before {
  content: "";
  grid-area: 1 / 1;
  inline-size: var(--prefs-swatch-size, 1.6rem);
  block-size: var(--prefs-swatch-size, 1.6rem);
  border-radius: var(--prefs-swatch-radius, 50%);
  background: var(--swatch, var(--accent));
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 30%, transparent);
}
.prefs-swatch::after {
  content: "\2713";
  grid-area: 1 / 1;
  color: var(--on-swatch, var(--on-accent));
  font-weight: 700;
  line-height: 1;
  visibility: hidden;
}
.prefs-swatch:has(> input:checked)::before {
  outline: 2px solid var(--prefs-swatch-ring, var(--text));
  outline-offset: 2px;
}
.prefs-swatch:has(> input:checked)::after { visibility: visible; }
.prefs-swatch:has(> input:focus-visible)::before {
  outline: 3px solid var(--focus);
  outline-offset: 3px;
}
/* "Theme default": a labelled pill, not a colour. */
.prefs-swatch.is-default { padding-inline: var(--space-xs); }
.prefs-swatch.is-default::before {
  inline-size: 100%;
  border-radius: var(--prefs-swatch-radius, 999px);
  background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--border);
}
.prefs-swatch.is-default::after { content: none; }
.prefs-swatch.is-default > span { grid-area: 1 / 1; position: relative; font-size: 0.85em; padding-inline: var(--space-xs); }
.prefs-swatch.is-default:has(> input:checked) > span { font-weight: 600; }

/* --- .splash: boot and loading screen (§13) -----------------------------
 * Real-load mode (default): render it, remove it (or set hidden) the
 * moment the app is ready. Timed intro: data-intro="2s"; prefs.js hides it
 * after that time or on any key, click or tap, once per session. Under
 * reduced motion it is a static frame. .is-contained covers its nearest
 * positioned ancestor instead of the viewport. */
.splash {
  position: fixed;
  inset: 0;
  z-index: calc(var(--z-window) + 1);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--splash-gap, var(--space-m));
  padding: var(--space-xl) var(--space-m);
  overflow: hidden;
  background: var(--splash-bg, var(--surface));
  color: var(--splash-fg, var(--text));
  font-family: var(--splash-font, inherit);
  text-align: center;
}
.splash.is-contained { position: absolute; z-index: var(--z-raised); }
html[data-splash-seen] .splash[data-intro] { display: none; }
/* .is-stage: black on every theme (a waiting screen in a dark room), with
   light ink checked against black; a theme may set --splash-stage-bg/-fg/
   -muted/-accent. .is-standby: an operator blackout, dimmer still, still. */
.splash:is(.is-stage, .is-standby) {
  --splash-bg: var(--splash-stage-bg, #000000);
  --splash-fg: var(--splash-stage-fg, #f2f2f2);
  --splash-title-fg: var(--splash-stage-fg, #f2f2f2);
  --splash-message-fg: var(--splash-stage-muted, #a0a0ac);
  --text: var(--splash-stage-fg, #f2f2f2);
  --muted: var(--splash-stage-muted, #a0a0ac);
  --accent-text: var(--splash-stage-accent, #8cc8ff);
  --readout-fg: var(--splash-stage-fg, #f2f2f2);
  color-scheme: dark;
}
.splash.is-standby { --splash-fg: var(--splash-standby-fg, #8a8a94); --splash-title-fg: var(--splash-standby-fg, #8a8a94); --splash-art-motion: none; }
.splash.is-standby > .splash-art { opacity: var(--splash-standby-art-opacity, 0.3); }
/* A logo as the mark: <img class="splash-art"> or .splash-art.is-image with
   --splash-art: url(…). No ring mask, no radius, no spin. */
:is(img.splash-art, .splash-art.is-image) { --splash-art-motion: none; aspect-ratio: auto; border-radius: 0; mask: none; }
img.splash-art { block-size: auto; background: none; object-fit: contain; }
.splash-art.is-image { aspect-ratio: 1; background: var(--splash-art, none) center / contain no-repeat; }

/* .overlay — lays content (an .empty-state.is-offline, a message) over the
   viewport behind a scrim; toggle it with [hidden], like .splash.
   .is-contained covers the nearest positioned ancestor instead.
   .empty-state.is-overlay is the same without a wrapper. */
:is(.overlay, .empty-state.is-overlay) {
  position: fixed;
  z-index: var(--overlay-z, calc(var(--z-window) + 1));
  inset: 0;
  display: grid;
  place-content: center;
  place-items: center;
  padding: var(--space-l);
  background: var(--overlay-scrim, color-mix(in srgb, var(--surface) 92%, transparent));
  color: var(--text);
}
:is(.overlay, .empty-state.is-overlay).is-contained { position: absolute; z-index: var(--z-raised); }
:is(.overlay, .empty-state.is-overlay)[hidden] { display: none; }
/* The theme's mark: any background layers (gradients, SVG data URIs). */
.splash-art {
  inline-size: var(--splash-art-size, 5rem);
  aspect-ratio: 1;
  flex: none;
  background: var(--splash-art,
    conic-gradient(var(--accent) 0 70%, color-mix(in srgb, var(--accent) 20%, transparent) 0));
  border-radius: var(--splash-art-radius, 50%);
  /* The default mark is a ring with a gap: a spinner when motion is on. */
  mask: var(--splash-art-mask, radial-gradient(circle, transparent 0 52%, #000 53% 69%, transparent 70%));
}
.splash-title {
  margin: 0;
  color: var(--splash-title-fg, inherit);
  font-size: var(--splash-title-size, 1.5rem);
  font-weight: var(--splash-title-weight, 600);
  letter-spacing: var(--splash-title-tracking, 0.02em);
  text-transform: var(--splash-title-transform, none);
  overflow-wrap: anywhere;
}
.splash-message {
  margin: 0;
  min-block-size: 1.5em;
  color: var(--splash-message-fg, var(--muted));
  font-family: var(--splash-message-font, inherit);
  font-size: 0.9em;
  overflow-wrap: anywhere;
}
.splash > progress {
  flex: none;
  inline-size: min(var(--splash-progress-width, 16rem), 100%);
  block-size: var(--progress-height, 0.5rem);
}
.splash-skip { margin: 0; color: var(--muted); font-size: 0.78em; }
/* Indeterminate <progress> (no value): a segment sweeps along the track;
   static (a centred segment) without motion. */
.splash > progress:indeterminate {
  block-size: var(--progress-height, 0.5rem);
  border: var(--progress-border-width, 1px) solid var(--progress-border, var(--border));
  border-radius: var(--progress-radius, var(--radius));
  background: linear-gradient(90deg, transparent 35%, var(--progress-fill, var(--accent)) 35% 65%, transparent 65%)
    50% 0 / 200% 100% no-repeat, var(--progress-bg, var(--surface-2));
}
.splash > progress:indeterminate::-webkit-progress-bar { background: transparent; }
.splash > progress:indeterminate::-moz-progress-bar { background: transparent; }

/* --- Themed states (§14) -----------------------------------------------
 * Modifiers on core's .empty-state. An <svg class="icon empty-state-icon">
 * becomes the illustration: the state's icon on a tinted disc (or the
 * theme's --empty-state-art). An empty .empty-state-title shows the
 * theme's suggested heading; the app's own text always wins. */
.empty-state > svg.empty-state-icon {
  /* Sized as border-box on purpose: apps commonly reset box-sizing with an
     unlayered rule. The disc is the icon plus its padding. */
  box-sizing: border-box;
  inline-size: calc(var(--empty-state-icon-size, 2.5rem) + 2 * var(--empty-state-art-pad, var(--space-m)));
  block-size: calc(var(--empty-state-icon-size, 2.5rem) + 2 * var(--empty-state-art-pad, var(--space-m)));
  padding: var(--empty-state-art-pad, var(--space-m));
  margin-block-end: var(--space-xs);
  border-radius: var(--empty-state-art-radius, 50%);
  background: var(--_empty-art, var(--empty-state-art,
    color-mix(in srgb, var(--empty-state-tone, var(--muted)) 16%, transparent)));
  color: var(--empty-state-tone, var(--muted));
  opacity: 1;
}
.empty-state.is-error {
  --empty-state-tone: var(--empty-state-error-fg, var(--danger-text, var(--danger)));
  --_empty-art: var(--empty-state-error-art);
}
.empty-state.is-offline {
  --empty-state-tone: var(--empty-state-offline-fg, var(--warning));
  --_empty-art: var(--empty-state-offline-art);
}
.empty-state.is-404 {
  --empty-state-tone: var(--empty-state-404-fg, var(--accent));
  --_empty-art: var(--empty-state-404-art);
}
.empty-state.is-locked {
  --empty-state-tone: var(--empty-state-locked-fg, var(--text));
  --_empty-art: var(--empty-state-locked-art);
}
.empty-state.is-loading {
  --empty-state-tone: var(--empty-state-loading-fg, var(--accent));
  --_empty-art: var(--empty-state-loading-art);
}
.empty-state > :is(.empty-state-title, .empty-state-hint) { margin: 0; max-inline-size: 36rem; overflow-wrap: anywhere; }
.empty-state > .cluster { justify-content: center; margin-block-start: var(--space-xs); }
.empty-state.is-error > .empty-state-title:empty::before { content: var(--empty-state-error-title, "Something went wrong"); }
.empty-state.is-offline > .empty-state-title:empty::before { content: var(--empty-state-offline-title, "You\2019re offline"); }
.empty-state.is-404 > .empty-state-title:empty::before { content: var(--empty-state-404-title, "Page not found"); }
.empty-state.is-locked > .empty-state-title:empty::before { content: var(--empty-state-locked-title, "You don\2019t have access"); }
.empty-state.is-loading > .empty-state-title:empty::before { content: var(--empty-state-loading-title, "Loading\2026"); }

/* --- Motion for the components above ----------------------------------- */
@media (prefers-reduced-motion: no-preference) {
  :root:not([data-motion="reduced"], [data-motion="none"]) .splash {
    transition: opacity var(--motion-duration-l) var(--motion-ease),
      display var(--motion-duration-l) allow-discrete;
  }
  :root:not([data-motion="reduced"], [data-motion="none"]) .splash[hidden] { opacity: 0; }
  :root:not([data-motion="reduced"], [data-motion="none"]) :is(.overlay, .empty-state.is-overlay) {
    transition: opacity var(--motion-duration-m, 0.25s) var(--motion-ease), display var(--motion-duration-m, 0.25s) allow-discrete;
  }
  :root:not([data-motion="reduced"], [data-motion="none"]) :is(.overlay, .empty-state.is-overlay)[hidden] { opacity: 0; }
  :root:not([data-motion="reduced"], [data-motion="none"]) .splash[data-intro] > * {
    animation: var(--motion-enter);
    animation-delay: calc(sibling-index() * 150ms);
  }
  :root:not([data-motion="reduced"], [data-motion="none"]) .splash-art {
    animation: var(--splash-art-motion, splash-turn 2.4s linear infinite);
  }
  :root:not([data-motion="reduced"], [data-motion="none"]) .splash > progress:indeterminate {
    animation: splash-sweep 1.4s ease-in-out infinite;
  }
  :root:not([data-motion="reduced"], [data-motion="none"]) .empty-state.is-loading > svg.empty-state-icon {
    animation: var(--empty-state-loading-motion, empty-state-flip 2s var(--motion-ease) infinite);
  }
  :root:not([data-motion="reduced"], [data-motion="none"]) .prefs .segmented-item,
  :root:not([data-motion="reduced"], [data-motion="none"]) .prefs-swatch::before {
    transition: background var(--motion-duration-s) var(--motion-ease), color var(--motion-duration-s) var(--motion-ease);
  }
}

  :root[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) .splash {
    transition: opacity var(--motion-duration-l) var(--motion-ease),
      display var(--motion-duration-l) allow-discrete;
  }
  :root[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) .splash[hidden] { opacity: 0; }
  :root[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) :is(.overlay, .empty-state.is-overlay) {
    transition: opacity var(--motion-duration-m, 0.25s) var(--motion-ease), display var(--motion-duration-m, 0.25s) allow-discrete;
  }
  :root[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) :is(.overlay, .empty-state.is-overlay)[hidden] { opacity: 0; }
  :root[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) .splash[data-intro] > * {
    animation: var(--motion-enter);
    animation-delay: calc(sibling-index() * 150ms);
  }
  :root[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) .splash-art {
    animation: var(--splash-art-motion, splash-turn 2.4s linear infinite);
  }
  :root[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) .splash > progress:indeterminate {
    animation: splash-sweep 1.4s ease-in-out infinite;
  }
  :root[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) .empty-state.is-loading > svg.empty-state-icon {
    animation: var(--empty-state-loading-motion, empty-state-flip 2s var(--motion-ease) infinite);
  }
  :root[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) .prefs .segmented-item,:root[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) .prefs-swatch::before {
    transition: background var(--motion-duration-s) var(--motion-ease), color var(--motion-duration-s) var(--motion-ease);
  }

@keyframes splash-turn { to { transform: rotate(1turn); } }
@keyframes splash-sweep { from { background-position: 100% 0, 0 0; } to { background-position: 0 0, 0 0; } }
@keyframes empty-state-flip { 0%, 40% { transform: rotate(0); } 50%, 90% { transform: rotate(180deg); } 100% { transform: rotate(360deg); } }

/* --- §10 Forced colours (Windows High Contrast) --------------------------
 * The OS replaces every colour with system colours and drops box-shadows
 * and background images. Shapes themes draw with shadows or fills get a
 * real outline, selected states use Highlight, and fills that carry a
 * value (meters, progress, switches) opt out to paint in system colours. */
@media (forced-colors: active) {
  :focus-visible { outline: 2px solid Highlight !important; outline-offset: 2px; }
  :is(.panel, .card, .modal, .drawer, .dropdown, .popover, .context-menu, .toast, .alert,
      .segmented, .tabs, .field-row, .readout, .knob, .avatar, .badge, .skeleton,
      .app-bar, .app-status, .tooltip, .splash-art):not(:focus-visible) {
    outline: 1px solid CanvasText;
    outline-offset: -1px;
  }
  :is(.btn, .btn-icon, .btn-close, .toggle-btn, .badge-button, .segmented-item, .tab,
      .nav-item, .pagination-item, .key):not(:focus-visible) {
    outline: 1px solid ButtonText;
    outline-offset: -1px;
  }
  :is(.btn, .toggle-btn, .badge-button, .key):disabled { color: GrayText; outline-color: GrayText; }
  :is(:is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))), .toggle-btn-on, .key-on, :is([aria-selected="true"], :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))), [aria-pressed="true"],
      [aria-current]:not([aria-current="false"])):is(.btn, .tab, .nav-item, .segmented-item,
      .pagination-item, .toggle-btn, .badge-button, .dropdown-item, .list-item, .key, .scrollspy-link),
  .prefs .segmented-item:has(> input:checked) {
    forced-color-adjust: none;
    background: Highlight;
    color: HighlightText;
  }
  :is(.meter-fill, .progress-bar, .switch-thumb, .meter-peak) { forced-color-adjust: none; background: CanvasText; }
  :is(.switch input:checked ~ .switch-track .switch-thumb, .switch[aria-checked="true"] .switch-thumb) { background: Highlight; }
  .switch-track, .meter, .progress { border: 1px solid CanvasText; }
  progress.progress, meter.meter { forced-color-adjust: none; color: Highlight; }
  progress.progress::-webkit-progress-value { background: Highlight; }
  .slider { forced-color-adjust: none; accent-color: Highlight; }
  .prefs-swatch::before { forced-color-adjust: none; border: 1px solid CanvasText; }
  .prefs-swatch:has(> input:checked)::before { outline-color: Highlight; }
  .splash-art { background: none; mask: none; }
  .empty-state > svg.empty-state-icon { background: Canvas; color: CanvasText; outline: 1px solid CanvasText; outline-offset: -1px; }
  .splash > progress:indeterminate { background: Canvas; }
}
/* --- v5 forms group (PLAN.md §9 #17–#35) ---------------------------------
 * Buttons and icons: .btn-clear, .btn-icon.is-clear, icon + label buttons,
 * .btn-split, .fab, .toggle-group, .has-badge.
 * Forms and inputs: .input-icon / .input-clear (inside .input-group),
 * search fields, native date/time inputs, .calendar, date ranges,
 * .combobox + .listbox, .tag-input + .tag, .stepper, .slider.is-range,
 * .rating, .swatches, .file-list.
 * Built on core's .btn, .input, .input-group, .toggle-btn, .slider, .list,
 * .progress and [popover] rules; every look reads a --<component>-* token
 * that falls back to the base palette. See docs/components/forms.md. */

/* --- .btn-clear / .btn-icon.is-clear -------------------------------------
 * No fill and no border at rest (not even a theme's button shadow); a tint
 * of the accent on hover, keyboard focus and press. .btn-ghost differs: it
 * keeps the theme's button chrome and hovers to an opaque --surface-2. */
:is(.btn-clear, .btn-icon.is-clear) {
  --btn-bg: transparent;
  --btn-border: transparent;
  --btn-fg: var(--btn-clear-fg, var(--text));
  --btn-shadow: none;
  --btn-shadow-hover: none;
  --btn-border-hover: transparent;
  --btn-bg-hover: var(--btn-clear-bg-hover, color-mix(in srgb, var(--accent) 16%, transparent));
}
:is(.btn-clear, .btn-icon.is-clear):focus-visible { background: var(--btn-clear-bg-hover, color-mix(in srgb, var(--accent) 16%, transparent)); }
:is(.btn-clear, .btn-icon.is-clear):is(:active, [aria-pressed="true"], [aria-expanded="true"]) {
  background: var(--btn-clear-bg-active, color-mix(in srgb, var(--accent) 28%, transparent));
}

/* --- Icon + label buttons -------------------------------------------------
 * The icon scales with the button's own font size (so .btn-sm shrinks it
 * too), never grows the line box, and sits optically flush with the
 * padding edge it is next to. Icon after the label: put the <svg> last. */
:is(.btn, .toggle-btn) > .icon {
  --icon-size: var(--btn-glyph-size, 1.15em);
  margin-block: -0.15em;
}
.btn:not(.btn-icon) > .icon:first-child:not(:last-child) { margin-inline-start: -0.2em; }
.btn:not(.btn-icon) > .icon:last-child:not(:first-child) { margin-inline-end: -0.2em; }
.btn:has(> .icon) { gap: var(--btn-gap, 0.45em); }

/* --- .btn-split — a main action joined to a caret that opens a menu -------
 *   <div class="btn-split">
 *     <button class="btn btn-primary">Save</button>
 *     <button class="btn btn-primary btn-split-toggle" popovertarget="m"
 *       aria-haspopup="menu" aria-label="More save options">…caret…</button>
 *     <ul class="context-menu" role="menu" popover id="m">…</ul>
 *   </div> */
.btn-split { display: inline-flex; align-items: stretch; max-inline-size: 100%; }
.btn-split > .btn:not(.btn-split-toggle) {
  border-start-end-radius: 0;
  border-end-end-radius: 0;
  min-inline-size: 0;
}
.btn-split > .btn-split-toggle {
  --btn-padding: 0 var(--btn-split-toggle-pad, 0.55em);
  margin-inline-start: calc(-1 * var(--btn-border-width, 1px));
  border-start-start-radius: 0;
  border-end-start-radius: 0;
  border-inline-start-color: var(--btn-split-divider, color-mix(in srgb, currentColor 35%, transparent));
  min-inline-size: var(--tap-min);
}
.btn-split > .btn:is(:hover, :focus-visible) { z-index: var(--z-raised); }
.btn-split-toggle > .icon { margin: 0; }
.btn-split:has(> :popover-open) > .btn-split-toggle > .icon,
.btn-split-toggle[aria-expanded="true"] > .icon { rotate: 180deg; }

/* --- .fab — floating action button ---------------------------------------
 * Fixed to the end corner, clear of the safe area (home indicator, notch).
 * Icon-only: <button class="btn btn-primary btn-icon fab" aria-label="New">.
 * .is-extended (no .btn-icon) adds a label, .is-start moves it to the start corner,
 * .is-contained pins it inside a positioned parent (a map, a panel). Apps
 * with a bottom bar lift it with --fab-offset-block. Layered on .btn (the
 * doubled class outranks core's (0,1,1) tap-min rule). */
.btn.fab {
  position: fixed;
  inset-block-end: calc(var(--fab-offset-block, var(--space-l)) + env(safe-area-inset-bottom, 0px));
  inset-inline-end: calc(var(--fab-offset-inline, var(--space-l)) + max(env(safe-area-inset-left, 0px), env(safe-area-inset-right, 0px)));
  z-index: var(--z-sticky);
  --btn-padding: 0;
  --btn-radius: var(--fab-radius, 999px);
  --btn-shadow: var(--fab-shadow, 0 0.25rem 0.9rem rgb(0 0 0 / 0.35));
  --btn-shadow-hover: var(--fab-shadow, 0 0.25rem 0.9rem rgb(0 0 0 / 0.35));
  min-inline-size: max(var(--tap-min), var(--fab-size, 3.5rem));
  min-block-size: max(var(--tap-min), var(--fab-size, 3.5rem));
  font-size: 1rem;
}
.fab > .icon { --icon-size: var(--fab-icon-size, 1.5em); width: var(--icon-size); height: var(--icon-size); min-width: var(--icon-size); margin: 0; }
.fab.is-extended { --btn-padding: 0 1.4em 0 1.1em; }
.fab.is-extended:dir(rtl) { --btn-padding: 0 1.1em 0 1.4em; }
.fab.is-start {
  inset-inline-end: auto;
  inset-inline-start: calc(var(--fab-offset-inline, var(--space-l)) + max(env(safe-area-inset-left, 0px), env(safe-area-inset-right, 0px)));
}
.fab.is-contained {
  position: absolute;
  inset-block-end: var(--fab-offset-block, var(--space-m));
  inset-inline-end: var(--fab-offset-inline, var(--space-m));
}
.fab.is-contained.is-start { inset-inline-end: auto; inset-inline-start: var(--fab-offset-inline, var(--space-m)); }

/* --- .toggle-group — joined .toggle-btn labels -----------------------------
 * Checkboxes inside = multi-select (bold + italic); radios sharing a name =
 * single-select (align left/centre/right). State is the inputs' own. */
.toggle-group { display: inline-flex; max-inline-size: 100%; }
.toggle-group > .toggle-btn { min-inline-size: var(--tap-min); }
.toggle-group > .toggle-btn:not(:first-child) {
  margin-inline-start: calc(-1 * var(--toggle-btn-border-width, 1px));
  border-start-start-radius: 0;
  border-end-start-radius: 0;
}
.toggle-group > .toggle-btn:not(:last-child) {
  border-start-end-radius: 0;
  border-end-end-radius: 0;
}
.toggle-group > .toggle-btn:is(:hover, :has(input:checked), :has(input:focus-visible)) { z-index: var(--z-raised); }
.toggle-group > .toggle-btn:has(input:focus-visible) { z-index: calc(var(--z-raised) + 1); }
/* .is-block: full width, equal items, multi-line content aligned to the
   start (a .toggle-btn-title over a .toggle-btn-hint). .is-stacked: joined
   top to bottom instead of side by side. */
.toggle-group:is(.is-block, .is-stacked) { display: flex; inline-size: 100%; }
.toggle-group.is-block > .toggle-btn { flex: 1 1 0; min-inline-size: 0; }
.toggle-group:is(.is-block, .is-stacked) > .toggle-btn { flex-direction: column; align-items: flex-start; justify-content: flex-start; gap: 0.15em; text-align: start; }
.toggle-group.is-stacked { flex-direction: column; }
.toggle-group.is-stacked > .toggle-btn { margin: 0; border-radius: var(--toggle-btn-radius, var(--btn-radius, var(--radius))); }
.toggle-group.is-stacked > .toggle-btn:not(:first-child) { margin-block-start: calc(-1 * var(--toggle-btn-border-width, 1px)); border-start-start-radius: 0; border-start-end-radius: 0; }
.toggle-group.is-stacked > .toggle-btn:not(:last-child) { border-end-start-radius: 0; border-end-end-radius: 0; }
.toggle-btn-title { font-weight: 600; }
.toggle-btn-hint { font-size: 0.85em; font-weight: 400; }

/* --- .has-badge — a dot or count on any button, tab or nav item ------------
 * <button class="btn btn-icon has-badge" data-count="3" aria-label="Alerts, 3 new">
 * No data-count (or an empty one) draws a dot. The number is decoration:
 * put it in the accessible name too. Items that sit in a scroller (tabs,
 * nav, menus, lists) carry it inline after their label instead. */
.has-badge:not(.fab) { position: relative; }
.has-badge::after {
  content: attr(data-count);
  content: attr(data-count) / "";
  position: absolute;
  inset-block-start: var(--has-badge-y, -0.3rem);
  inset-inline-end: var(--has-badge-x, -0.3rem);
  box-sizing: border-box;
  min-inline-size: 1.6em;
  block-size: 1.6em;
  padding-inline: 0.4em;
  border-radius: 999px;
  background: var(--has-badge-bg, var(--danger));
  color: var(--has-badge-fg, var(--on-danger));
  box-shadow: 0 0 0 var(--has-badge-ring-width, 2px) var(--has-badge-ring, var(--surface));
  font-family: var(--font);
  font-size: var(--has-badge-font-size, 0.68rem);
  font-weight: 700;
  line-height: 1.6em;
  letter-spacing: 0;
  text-align: center;
  text-transform: none;
  white-space: nowrap;
  pointer-events: none;
}
.has-badge:is(:not([data-count]), [data-count=""])::after {
  min-inline-size: 0;
  inline-size: var(--has-badge-dot, 0.6rem);
  block-size: var(--has-badge-dot, 0.6rem);
  padding: 0;
  inset-block-start: var(--has-badge-y, -0.1rem);
  inset-inline-end: var(--has-badge-x, -0.1rem);
}
/* On an icon button the badge hangs off the glyph's corner, so it stays put
   when a touch tier grows the hit area around the glyph. */
.btn-icon.has-badge::after {
  inset-block-start: calc(50% - var(--btn-icon-size, 2.2em) / 2 + var(--has-badge-y, -0.3rem));
  inset-inline-end: calc(50% - var(--btn-icon-size, 2.2em) / 2 + var(--has-badge-x, -0.3rem));
}
.btn-icon.has-badge:is(:not([data-count]), [data-count=""])::after {
  inset-block-start: calc(50% - var(--btn-icon-size, 2.2em) / 2 + var(--has-badge-y, -0.1rem));
  inset-inline-end: calc(50% - var(--btn-icon-size, 2.2em) / 2 + var(--has-badge-x, -0.1rem));
}
:is(.tab, .nav-item, .segmented-item, .list-item, .dropdown-item, .context-menu li > *).has-badge::after {
  position: static;
  display: inline-block;
  margin-inline-start: 0.5em;
  vertical-align: 0.1em;
}

/* --- .input-icon / .input-clear inside .input-group ------------------------
 *   <div class="input-group">
 *     <span class="input-icon"><svg class="icon">…</svg></span>
 *     <input class="input" placeholder="Filter">
 *     <button class="input-clear" type="button" aria-label="Clear"></button>
 *   </div>
 * The icon and the clear button overlay the field's own padding, so the
 * field keeps one border and its native focus ring. The clear button hides
 * itself while the field is empty (that needs a placeholder, even " "). */
:is(.input-icon, .input-clear) {
  position: relative;
  z-index: calc(var(--z-raised) + 1);
  display: grid;
  place-items: center;
  flex: none;
  font-size: 0.9em;
  color: var(--input-icon-fg, var(--muted));
}
@media (any-pointer: coarse) {
  :is(.input-icon, .input-clear) { font-size: max(16px, 1em); }
}
.input-group > .input-icon {
  inline-size: var(--input-icon-w, 2.5em);
  margin-inline-end: calc(-1 * var(--input-icon-w, 2.5em));
  pointer-events: none;
}
.input-group > .input-icon:first-child + :is(.input, .select) {
  margin-inline-start: 0;
  padding-inline-start: var(--input-icon-w, 2.5em);
  border-start-start-radius: var(--input-radius, var(--radius));
  border-end-start-radius: var(--input-radius, var(--radius));
}
.input-clear {
  inline-size: var(--input-clear-w, max(var(--tap-min), 2.5em));
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
  font-family: inherit;
  line-height: 1;
}
.input-group > .input-clear {
  margin-inline-start: calc(-1 * var(--input-clear-w, max(var(--tap-min), 2.5em)));
}
.input-group > :is(.input, .select):has(+ .input-clear) { padding-inline-end: var(--input-clear-w, max(var(--tap-min), 2.5em)); }
.input-clear:not(:has(.icon))::before { content: var(--input-clear-glyph, "\00d7"); font-size: 1.35em; }
.input-clear .icon { --icon-size: 1em; }
.input-clear:focus-visible { color: var(--text); outline-offset: -4px; }
@media (hover: hover) {
  .input-clear:hover { color: var(--text); }
}
.input:placeholder-shown + .input-clear { visibility: hidden; }
.input-clear:disabled { visibility: hidden; }
/* The field keeps its end radii when only overlays (clear, shortcut) follow. */
.input-group > :is(.input, .select):not(:has(~ :not(.input-clear, kbd))) {
  border-start-end-radius: var(--input-radius, var(--radius));
  border-end-end-radius: var(--input-radius, var(--radius));
}

/* Search: type="search" plus .input-icon, .input-clear and an optional
 * shortcut hint (<kbd> as the group's last child), which hides once the
 * field has focus or a value. The native cancel button is themed, and
 * hidden when an .input-clear stands in for it. */
.input[type="search"] { -webkit-appearance: none; appearance: none; }
.input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; }
.input[type="search"]::-webkit-search-cancel-button {
  -webkit-appearance: none;
  inline-size: 0.9em;
  block-size: 0.9em;
  background: var(--input-icon-fg, var(--muted));
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.5' stroke-linecap='round'%3E%3Cpath d='M6 6l12 12M18 6 6 18'/%3E%3C/svg%3E") center / contain no-repeat;
}
.input[type="search"]:has(+ .input-clear)::-webkit-search-cancel-button { display: none; }
.input-group:has(> kbd) { position: relative; }
.input-group > kbd {
  position: absolute;
  z-index: calc(var(--z-raised) + 1);
  inset-inline-end: var(--space-xs);
  inset-block-start: 50%;
  translate: 0 -50%;
  pointer-events: none;
}
.input-group:has(> kbd) > :is(.input, .select) { padding-inline-end: var(--input-kbd-w, 4.5em); }
.input-group > :is(.input:focus, .input:not(:placeholder-shown)) ~ kbd { display: none; }

/* --- Native date, time and list inputs ------------------------------------
 * The popup calendar follows the theme's color-scheme (every theme sets
 * one). The indicator is redrawn in --input-icon-fg from a mask a theme
 * can swap (--input-date-icon, --input-time-icon, --input-list-icon); the
 * focused segment (day, month, hour…) takes the accent. */
.input:is([type="date"], [type="datetime-local"], [type="month"], [type="week"], [type="time"], [list])::-webkit-calendar-picker-indicator {
  inline-size: 1.1em;
  block-size: 1.1em;
  margin-inline-start: 0.4em;
  padding: 0.15em;
  border-radius: var(--radius);
  background: var(--input-icon-fg, var(--muted));
  -webkit-mask: var(--input-date-icon, url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2'/%3E%3Cpath d='M16 2v4M8 2v4M3 10h18'/%3E%3C/svg%3E")) center / contain no-repeat;
  mask: var(--input-date-icon, url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2'/%3E%3Cpath d='M16 2v4M8 2v4M3 10h18'/%3E%3C/svg%3E")) center / contain no-repeat;
}
.input[type="time"]::-webkit-calendar-picker-indicator {
  -webkit-mask-image: var(--input-time-icon, url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M12 7v5l3 3'/%3E%3C/svg%3E"));
  mask-image: var(--input-time-icon, url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M12 7v5l3 3'/%3E%3C/svg%3E"));
}
.input[list]::-webkit-calendar-picker-indicator {
  -webkit-mask-image: var(--input-list-icon, url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E"));
  mask-image: var(--input-list-icon, url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E"));
}
@media (hover: hover) {
  .input::-webkit-calendar-picker-indicator:hover { background: var(--text); }
}
.input:focus-visible::-webkit-calendar-picker-indicator { background: var(--focus); }
.input::-webkit-datetime-edit-text { color: var(--input-icon-fg, var(--muted)); }
.input::-webkit-datetime-edit-day-field:focus,
.input::-webkit-datetime-edit-month-field:focus,
.input::-webkit-datetime-edit-year-field:focus,
.input::-webkit-datetime-edit-week-field:focus,
.input::-webkit-datetime-edit-hour-field:focus,
.input::-webkit-datetime-edit-minute-field:focus,
.input::-webkit-datetime-edit-second-field:focus,
.input::-webkit-datetime-edit-ampm-field:focus {
  background: var(--input-segment-bg, var(--accent));
  color: var(--input-segment-fg, var(--on-accent));
  border-radius: calc(var(--radius) * 0.5);
}
/* Native date inputs have a wide intrinsic size; in a group they may
   shrink to fit (a date range on a phone). */
.input-group > .input:is([type="date"], [type="time"], [type="datetime-local"], [type="month"]) { min-inline-size: 0; }

/* --- .calendar — a month grid for app-rendered pickers -------------------
 * A <table> of day buttons. Day state on the button: aria-current="date"
 * (today), aria-pressed="true" (selected), disabled, .is-outside (another
 * month). Range state on the <td>: .is-range-start, .is-in-range,
 * .is-range-end (the band runs through the cells). */
.calendar {
  display: inline-block;
  inline-size: fit-content;
  max-inline-size: 100%;
  padding: var(--calendar-pad, var(--space-s));
  background: var(--calendar-bg, var(--surface));
  color: var(--calendar-fg, var(--text));
  border: var(--calendar-border-width, 1px) solid var(--calendar-border, var(--border));
  border-radius: var(--calendar-radius, var(--panel-radius, var(--radius)));
  font-size: 0.9em;
  --calendar-day: max(var(--tap-min), var(--calendar-day-size, 2.25em));
}
.popover > .calendar { padding: 0; border: 0; background: none; }
.calendar-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-xs);
  margin-block-end: var(--space-xs);
}
.calendar-title { margin: 0; font-size: 1em; font-weight: 600; text-align: center; flex: 1; }
.calendar-grid { inline-size: auto; border-collapse: collapse; border-spacing: 0; table-layout: fixed; }
.calendar-grid th {
  inline-size: var(--calendar-day);
  padding-block: 0 var(--space-2xs);
  color: var(--calendar-weekday-fg, var(--muted));
  font-size: 0.8em;
  font-weight: 600;
  text-align: center;
  text-transform: var(--label-transform, none);
}
.calendar-grid td { padding: 1px 0; text-align: center; }
.calendar-day {
  display: grid;
  place-items: center;
  inline-size: var(--calendar-day);
  block-size: var(--calendar-day);
  margin: 0;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--calendar-day-radius, var(--radius));
  background: transparent;
  color: inherit;
  font: inherit;
  font-variant-numeric: tabular-nums;
  touch-action: manipulation;
}
@media (hover: hover) {
  .calendar-day:hover:not(:disabled, [aria-pressed="true"]) { background: var(--calendar-day-bg-hover, var(--surface-2)); }
}
.calendar-day.is-outside { color: var(--calendar-outside-fg, var(--muted)); }
.calendar-day[aria-current="date"] {
  border-color: var(--calendar-today-ring, var(--accent));
  font-weight: 700;
}
.calendar-day:disabled {
  color: var(--muted);
  opacity: 0.55;
  text-decoration: line-through;
  cursor: var(--cursor-not-allowed, not-allowed);
}
.calendar-day:is([aria-pressed="true"], :is([aria-selected="true"], :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked)))) {
  background: var(--calendar-selected-bg, var(--accent));
  color: var(--calendar-selected-fg, var(--on-accent));
  font-weight: 700;
}
.calendar-grid td.is-in-range { background: var(--calendar-range-bg, color-mix(in srgb, var(--accent) 22%, transparent)); }
.calendar-grid td.is-range-start { background: linear-gradient(to right, transparent 50%, var(--calendar-range-bg, color-mix(in srgb, var(--accent) 22%, transparent)) 50%); }
.calendar-grid td.is-range-end { background: linear-gradient(to left, transparent 50%, var(--calendar-range-bg, color-mix(in srgb, var(--accent) 22%, transparent)) 50%); }
.calendar-grid td.is-range-start:dir(rtl) { background: linear-gradient(to left, transparent 50%, var(--calendar-range-bg, color-mix(in srgb, var(--accent) 22%, transparent)) 50%); }
.calendar-grid td.is-range-end:dir(rtl) { background: linear-gradient(to right, transparent 50%, var(--calendar-range-bg, color-mix(in srgb, var(--accent) 22%, transparent)) 50%); }
.calendar-grid td.is-range-start.is-range-end { background: none; }
.calendar-grid td.is-in-range > .calendar-day:not(:disabled) { border-radius: 0; }
.calendar-footer {
  display: flex;
  justify-content: space-between;
  gap: var(--space-xs);
  margin-block-start: var(--space-xs);
}

/* --- .combobox + .listbox ---------------------------------------------------
 *   <div class="combobox">
 *     <input class="input" role="combobox" aria-expanded="true"
 *       aria-controls="lb" aria-autocomplete="list" aria-activedescendant="o2">
 *     <ul class="listbox" role="listbox" id="lb" popover="manual">
 *       <li role="option" id="o1">Berlin</li>
 *       <li role="option" id="o2" class="is-active" aria-selected="true">Bern</li>
 *     </ul>
 *   </div>
 * With popover the list lives in the top layer, anchored under the field
 * and as wide as it (centred where anchor positioning is unsupported).
 * Without popover it drops below the field in flow, like .anchor. */
.combobox { position: relative; anchor-scope: --combobox; }
.combobox > .input { anchor-name: --combobox; }
.listbox {
  margin: 0;
  padding: var(--listbox-pad, 0.3rem);
  list-style: none;
  background: var(--listbox-bg, var(--dropdown-bg, var(--surface-2)));
  color: var(--listbox-fg, var(--text));
  border: var(--listbox-border-width, 1px) solid var(--listbox-border, var(--dropdown-border, var(--border)));
  border-radius: var(--listbox-radius, var(--dropdown-radius, var(--radius)));
  box-shadow: var(--listbox-shadow, var(--panel-shadow, 0 2px 12px rgb(0 0 0 / 0.3)));
  max-block-size: var(--listbox-max, 16rem);
  overflow-y: auto;
  font-size: 0.9em;
}
.combobox > .listbox:not([popover]) {
  position: absolute;
  z-index: var(--z-dropdown);
  inset-block-start: calc(100% + 0.25rem);
  inset-inline: 0;
}
.listbox[popover] {
  position: fixed;
  inset: 0;
  margin: auto;
  inline-size: min(22rem, calc(100vw - 2rem));
  block-size: fit-content;
}
.listbox[popover]:not(:popover-open) { display: none; }
@supports (position-anchor: --a) {
  .combobox > .listbox[popover] {
    position-anchor: --combobox;
    inset: auto;
    margin: 0.25rem 0;
    position-area: block-end span-inline-end;
    position-try-fallbacks: flip-block;
    inline-size: anchor-size(inline);
  }
}
.listbox [role="option"] {
  position: relative;
  display: block;
  align-content: center;
  min-block-size: var(--tap-min);
  padding: 0.35em 2em 0.35em 0.7em;
  border-radius: var(--listbox-radius, var(--dropdown-radius, var(--radius)));
  overflow-wrap: anywhere;
}
.listbox [role="option"]:dir(rtl) { padding-inline: 2em 0.7em; }
.listbox [role="option"]::after {
  content: var(--listbox-check-glyph, "\2713");
  position: absolute;
  inset-inline-end: 0.7em;
  inset-block-start: 50%;
  translate: 0 -50%;
  visibility: hidden;
}
.listbox [role="option"]:is([aria-selected="true"], :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))) { font-weight: 600; }
.listbox [role="option"]:is([aria-selected="true"], :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked)))::after { visibility: visible; }
.listbox [role="option"]:is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))) {
  background: var(--listbox-option-bg-active, var(--dropdown-item-bg-hover, var(--accent)));
  color: var(--listbox-option-fg-active, var(--dropdown-item-fg-hover, var(--on-accent)));
}
@media (hover: hover) {
  .listbox [role="option"]:not([aria-disabled="true"]):hover { background: var(--listbox-option-bg-hover, var(--row-hover-bg, rgb(128 128 128 / 0.15))); }
  .listbox [role="option"]:is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))):hover { background: var(--listbox-option-bg-active, var(--dropdown-item-bg-hover, var(--accent))); }
}
.listbox [role="option"][aria-disabled="true"] { color: var(--muted); cursor: var(--cursor-not-allowed, not-allowed); }
.listbox mark { background: none; color: inherit; font-weight: 700; text-decoration: underline; }
.listbox-empty { padding: 0.6em 0.7em; color: var(--muted); font-style: italic; }

/* --- .tag-input + .tag — removable chips inside a field ---------------------
 *   <div class="tag-input">
 *     <span class="tag">Design<button class="tag-remove" aria-label="Remove Design"></button></span>
 *     <input class="tag-input-field" aria-label="Add a tag" placeholder="Add…">
 *   </div> */
.tag-input {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2xs);
  min-block-size: var(--tap-min);
  padding: var(--space-2xs) var(--space-xs);
  background: var(--input-bg, var(--surface));
  color: var(--input-fg, var(--text));
  border: var(--input-border-width, 1px) solid var(--input-border, var(--border));
  border-radius: var(--input-radius, var(--radius));
  box-shadow: var(--input-shadow, none);
  font-size: 0.9em;
  cursor: var(--cursor-text, text);
}
@media (any-pointer: coarse) {
  .tag-input { font-size: max(16px, 1em); }
}
.tag-input:focus-within {
  border-color: var(--focus);
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  box-shadow: var(--focus-ring, var(--input-shadow, none));
}
.tag-input:has(.tag-input-field:is([aria-invalid="true"], .is-invalid)) { border-color: var(--danger); outline-color: var(--danger); }
.tag-input:has(.tag-input-field:disabled) { border-style: dashed; cursor: var(--cursor-not-allowed, not-allowed); }
.tag-input-field {
  flex: 1 1 6ch;
  min-inline-size: 6ch;
  /* The field's hit area is the full row height: it overlaps the
     container's padding instead of shrinking inside it. */
  min-block-size: var(--tap-min);
  margin-block: calc(-1 * var(--space-2xs) - var(--input-border-width, 1px));
  padding: 0 var(--space-3xs);
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
}
.tag-input-field:focus { outline: none; }
.tag {
  display: inline-flex;
  align-items: center;
  gap: 0.3em;
  max-inline-size: 100%;
  padding: 0.15em 0.55em;
  background: var(--tag-bg, var(--surface-2));
  color: var(--tag-fg, var(--text));
  border: var(--tag-border-width, 1px) solid var(--tag-border, var(--border));
  border-radius: var(--tag-radius, var(--radius));
  font-size: 0.85em;
  line-height: 1.4;
  overflow-wrap: anywhere;
  cursor: var(--cursor-default, default);
}
.tag:has(> .tag-remove) { padding-inline-end: 0.2em; }
.tag-remove {
  position: relative;
  flex: none;
  display: grid;
  place-items: center;
  /* A real 24px box (WCAG 2.5.8 counts the box, not ::before), pulled back
     by negative margins so the tag keeps its 1.25em footprint; the hover
     fill stays 1.25em via content-box clipping. */
  --_tag-x: max(1.25em, 24px);
  inline-size: var(--_tag-x);
  block-size: var(--_tag-x);
  margin: calc((1.25em - var(--_tag-x)) / 2);
  padding: calc((var(--_tag-x) - 1.25em) / 2);
  border: 0;
  border-radius: var(--tag-radius, var(--radius));
  background: transparent;
  background-clip: content-box;
  color: var(--tag-remove-fg, var(--muted));
  font: inherit;
  line-height: 1;
  touch-action: manipulation;
}
.tag-remove::before {
  content: "";
  position: absolute;
  inset: calc((var(--_tag-x) - var(--tap-min)) / 2);
}
.tag-remove:not(:has(.icon))::after { content: var(--input-clear-glyph, "\00d7"); font-size: 1.2em; }
.tag-remove:focus-visible { color: var(--text); outline-offset: 0; }
@media (hover: hover) {
  .tag-remove:hover { background: var(--tag-remove-bg-hover, color-mix(in srgb, var(--danger) 20%, transparent)); color: var(--text); }
}
.tag.is-accent { --tag-bg: color-mix(in srgb, var(--accent) 20%, var(--surface)); --tag-border: var(--accent); }

/* --- .stepper — − / value / + ---------------------------------------------
 *   <div class="input-group stepper" role="group" aria-label="Guests">
 *     <button class="btn btn-icon" aria-label="Fewer guests">−</button>
 *     <input class="input" type="number" value="2" aria-label="Guests">
 *     <button class="btn btn-icon" aria-label="More guests">+</button>
 *   </div>
 * Built on .input-group's joins; the buttons are square tap targets. */
.stepper { display: inline-flex; inline-size: auto; max-inline-size: 100%; vertical-align: middle; }
.stepper > .btn {
  --btn-padding: 0;
  --btn-radius: var(--stepper-radius, var(--input-radius, var(--radius)));
  min-inline-size: max(var(--tap-min), var(--stepper-btn-size, 2.4em));
  min-block-size: max(var(--tap-min), var(--stepper-btn-size, 2.4em));
  font-size: 1em;
}
.stepper > .input {
  flex: 0 1 auto;
  inline-size: var(--stepper-input-w, 4.5em);
  text-align: center;
  font-variant-numeric: tabular-nums;
  -moz-appearance: textfield;
  appearance: textfield;
}
.stepper > .input::-webkit-inner-spin-button,
.stepper > .input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
.stepper > .btn:is(:hover, :focus-visible) { z-index: var(--z-raised); }

/* --- .slider.is-range — two thumbs on one track -------------------------------
 *   <div class="slider is-range" style="--from: 20; --to: 75">
 *     <input class="slider" type="range" value="20" aria-label="Minimum price">
 *     <input class="slider" type="range" value="75" aria-label="Maximum price">
 *   </div>
 * --from/--to are the two values as percentages of the track (0–100); the
 * app updates them on input, as it does --value elsewhere. */
.slider.is-range {
  position: relative;
  display: grid;
  align-items: center;
  block-size: auto;
  min-block-size: var(--tap-min);
  padding: 0;
  background: none;
  border: 0;
  border-radius: 0;
}
.slider.is-range::before,
.slider.is-range::after {
  content: "";
  grid-area: 1 / 1;
  block-size: var(--slider-height, 0.35rem);
  border-radius: var(--slider-radius, var(--radius));
}
.slider.is-range::before {
  background: var(--slider-bg, var(--surface-2));
  border: var(--slider-border-width, 1px) solid var(--slider-border, var(--border));
}
.slider.is-range::after {
  margin-inline-start: calc(var(--from, 0) * 1%);
  margin-inline-end: calc(100% - var(--to, 100) * 1%);
  background: var(--slider-range-fill, var(--accent));
}
.slider.is-range > .slider {
  grid-area: 1 / 1;
  position: relative;
  z-index: var(--z-raised);
  margin: 0;
  block-size: var(--tap-min); /* the inputs are the hit area; the track is ::before/::after */
  background: transparent;
  border-color: transparent;
  pointer-events: none;
}
.slider.is-range > .slider::-webkit-slider-thumb { pointer-events: auto; }
.slider.is-range > .slider::-moz-range-thumb { pointer-events: auto; }
.slider.is-range > .slider:focus-visible { z-index: calc(var(--z-raised) + 1); outline-offset: 4px; }
.slider.is-range:has(> :disabled) { opacity: 0.5; }

/* --- .rating — radio stars (or a theme glyph) -------------------------------
 *   <fieldset class="rating"><legend class="visually-hidden">Rating</legend>
 *     <label><input type="radio" name="r" value="1"><span class="visually-hidden">1 star</span></label> …
 *   </fieldset>
 * Filled and empty glyphs differ in shape, not only colour. */
.rating {
  display: inline-flex;
  margin: 0;
  padding: 0;
  border: 0;
  min-inline-size: 0;
  font-size: var(--rating-size, 1.6em);
  line-height: 1;
}
.rating > label {
  position: relative;
  display: grid;
  place-items: center;
  min-inline-size: max(var(--tap-min), 1.2em);
  min-block-size: max(var(--tap-min), 1.2em);
  color: var(--rating-empty-fg, var(--muted));
  touch-action: manipulation;
}
.rating > label > input {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  margin: 0;
  opacity: 0;
  clip-path: inset(50%);
}
.rating > label::before { content: var(--rating-glyph-empty, "\2606"); }
.rating:has(input:checked) > label,
.rating > label:has(input:checked) { color: var(--rating-fg, var(--warning)); }
.rating:has(input:checked) > label::before { content: var(--rating-glyph, "\2605"); }
.rating > label:has(input:checked) ~ label { color: var(--rating-empty-fg, var(--muted)); }
.rating > label:has(input:checked) ~ label::before { content: var(--rating-glyph-empty, "\2606"); }
@media (hover: hover) {
  .rating:not(:disabled):hover > label { color: var(--rating-fg, var(--warning)); }
  .rating:not(:disabled):hover > label::before { content: var(--rating-glyph, "\2605"); }
  .rating:not(:disabled) > label:hover ~ label { color: var(--rating-empty-fg, var(--muted)); }
  .rating:not(:disabled) > label:hover ~ label::before { content: var(--rating-glyph-empty, "\2606"); }
}
.rating > label:has(input:focus-visible) { outline: 2px solid var(--focus); outline-offset: -2px; border-radius: var(--radius); }
.rating:disabled { opacity: 0.5; }
.rating:disabled > label { cursor: var(--cursor-not-allowed, not-allowed); }

/* --- .swatches — radio colour picker ------------------------------------------
 *   <fieldset class="swatches"><legend class="label">Accent</legend>
 *     <label class="swatch" style="--swatch: #2f7cf6"><input type="radio" name="c" value="blue"><span class="visually-hidden">Blue</span></label> …
 *   </fieldset>
 * --swatch is the colour (a documented value API). The chosen one gets a
 * ring and a check mark. */
.swatches {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--swatches-gap, var(--space-xs));
  margin: 0;
  padding: 0;
  border: 0;
  min-inline-size: 0;
}
/* .color-dot — a small colour indicator for dense rows. As a <button> its
   visible size stays small but its hit area grows to --tap-min. Name it by
   the colour ("Alert 1 colour: amber"). */
.color-dot {
  display: inline-block;
  flex: none;
  inline-size: var(--color-dot-size, 0.85em);
  block-size: var(--color-dot-size, 0.85em);
  border-radius: 50%;
  background: var(--swatch, var(--accent));
  box-shadow: inset 0 0 0 1px var(--swatch-border, rgb(128 128 128 / 0.5));
  vertical-align: -0.1em;
}
button.color-dot { position: relative; margin: 0; padding: 0; border: 0; }
button.color-dot::before { content: ""; position: absolute; inset-block-start: 50%; inset-inline-start: 50%; inline-size: var(--tap-min); block-size: var(--tap-min); translate: -50% -50%; }
button.color-dot:dir(rtl)::before { translate: 50% -50%; }
button.color-dot:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.swatch {
  position: relative;
  display: grid;
  place-items: center;
  min-inline-size: max(var(--tap-min), var(--swatch-size, 1.75rem));
  min-block-size: max(var(--tap-min), var(--swatch-size, 1.75rem));
  touch-action: manipulation;
}
.swatch > input {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  margin: 0;
  opacity: 0;
  clip-path: inset(50%);
}
.swatch::before {
  content: "";
  grid-area: 1 / 1;
  inline-size: var(--swatch-size, 1.75rem);
  block-size: var(--swatch-size, 1.75rem);
  border-radius: var(--swatch-radius, 50%);
  background: var(--swatch, var(--accent));
  box-shadow: inset 0 0 0 1px var(--swatch-border, rgb(128 128 128 / 0.5));
}
.swatch::after {
  grid-area: 1 / 1;
  color: var(--swatch-check-fg, #fff);
  font-size: calc(var(--swatch-size, 1.75rem) * 0.55);
  font-weight: 700;
  line-height: 1;
  text-shadow: 0 0 2px rgb(0 0 0 / 0.9), 0 0 1px rgb(0 0 0 / 0.9);
}
.swatch:has(input:checked)::before {
  box-shadow: inset 0 0 0 1px var(--swatch-border, rgb(128 128 128 / 0.5)),
    0 0 0 2px var(--swatch-gap, var(--surface)), 0 0 0 4px var(--swatch-ring, var(--text));
}
.swatch:has(input:checked)::after { content: var(--swatch-check-glyph, "\2713"); }
.swatch:has(input:focus-visible)::before { outline: 2px solid var(--focus); outline-offset: 5px; }
.swatch:has(input:disabled) { opacity: 0.4; cursor: var(--cursor-not-allowed, not-allowed); }

/* --- .file-list — an upload queue (pairs with .dropzone) ---------------------
 *   <ul class="list file-list">
 *     <li class="is-uploading"><svg class="icon">…</svg>
 *       <span class="list-item-title">report.pdf</span>
 *       <span class="list-item-meta">2.4 MB · 64%</span>
 *       <progress class="progress" value="64" max="100" aria-label="report.pdf"></progress>
 *       <button class="btn btn-icon is-clear" aria-label="Cancel report.pdf">…</button></li>
 *   </ul>
 * Row states: .is-queued, .is-uploading, .is-done, .is-error. Say the
 * state in the meta text too; the colour only reinforces it. */
.dropzone + .file-list { margin-block-start: var(--space-s); }
.file-list > li {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  column-gap: var(--space-s);
  row-gap: var(--space-3xs);
  padding: var(--list-item-padding, 0.55em 0.9em);
}
.file-list > li > .icon { grid-row: 1 / span 3; --icon-size: 1.6em; color: var(--file-icon-fg, var(--muted)); }
.file-list > li > :is(.list-item-title, .list-item-meta, .progress) { grid-column: 2; }
.file-list > li > :is(.btn, button):last-child { grid-column: 3; grid-row: 1 / span 3; }
.file-list .progress { --progress-height: 0.3rem; }
.file-list > li.is-queued > .progress { visibility: hidden; }
.file-list > li.is-done { --progress-fill: var(--success); --file-icon-fg: var(--success-text, var(--success)); }
.file-list > li.is-done > .progress { display: none; }
.file-list > li.is-error {
  --progress-fill: var(--danger);
  --file-icon-fg: var(--danger-text, var(--danger));
  --list-item-meta-fg: var(--danger-text, var(--danger));
  box-shadow: inset 0.2rem 0 0 var(--danger);
}
.file-list > li.is-error:dir(rtl) { box-shadow: inset -0.2rem 0 0 var(--danger); }

/* --- Motion ------------------------------------------------------------------ */
@media (prefers-reduced-motion: no-preference) {
  html:not([data-motion="reduced"]) :is(.btn-split-toggle > .icon) { transition: rotate 0.15s ease; }
  html:not([data-motion="reduced"]) :is(.calendar-day, .tag-remove, .input-clear, .rating > label) {
    transition: background-color 0.12s ease, color 0.12s ease;
  }
}

  html[data-motion="always"]:not([data-motion="reduced"]) :is(.btn-split-toggle > .icon) { transition: rotate 0.15s ease; }
  html[data-motion="always"]:not([data-motion="reduced"]) :is(.calendar-day, .tag-remove, .input-clear, .rating > label) {
    transition: background-color 0.12s ease, color 0.12s ease;
  }

/* --- Graphs (v5.3): small-multiple time-series grids --------------------
 *   <div class="graph-grid">
 *     <figure class="graph-card">
 *       <figcaption class="graph-card-title">CPU per core <span class="graph-card-value">38%</span></figcaption>
 *       <div class="graph-card-plot chart">…inline SVG (.chart hooks) or a uPlot root…</div>
 *       <ul class="chart-legend is-line"><li data-series="1">core 0</li>…</ul>
 *     </figure>
 *   </div>
 * .graph-grid wraps cards onto more rows instead of cramping them.
 * uPlot bridge: uPlot draws on canvas (colours come from JS options — see
 * docs/components/graphs.md for the token-reading snippet); this styles the
 * DOM around it: title, legend, cursor and selection.
 * .system-pane is a collapsible full-width <details> that sits in flow just
 * above the status footer. */
.graph-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, var(--graph-card-min, 16rem)), 1fr));
  gap: var(--graph-grid-gap, var(--space-s, 0.75rem));
}
.graph-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs, 0.35rem);
  min-inline-size: 0;
  margin: 0;
  padding: var(--graph-card-padding, 0.6rem 0.75rem);
  background: var(--graph-card-bg, var(--surface));
  color: var(--text);
  border: 1px solid var(--graph-card-border, var(--border));
  border-radius: var(--graph-card-radius, var(--radius));
  box-shadow: var(--graph-card-shadow, none);
}
.graph-card-title {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  margin: 0;
  font-size: var(--graph-card-title-size, 0.8rem);
  font-weight: 700;
  color: var(--graph-card-title-fg, var(--text));
}
.graph-card-value {
  font-family: var(--font-mono, monospace);
  font-variant-numeric: tabular-nums;
  font-weight: 400;
  color: var(--muted);
}
.graph-card-plot {
  position: relative;
  block-size: var(--graph-card-height, 6.5rem);
  min-block-size: 0;
  background: var(--graph-card-plot-bg, var(--chart-plot-bg, transparent));
  border-radius: calc(var(--radius) * 0.5);
}
.graph-card .graph-card-plot > svg { display: block; inline-size: 100%; block-size: 100%; }
.graph-card.is-tall { --graph-card-height: var(--graph-card-height-tall, 10rem); }
.graph-card.is-full { grid-column: 1 / -1; }
.graph-card > :is(.legend, .chart-legend) { --legend-size: 0.72rem; gap: 0.15rem 0.8rem; margin: 0; padding: 0; list-style: none; color: var(--muted); }

/* --- uPlot bridge (DOM parts only; canvas colours come from options) ----- */
:is(.graph-card, .chart) .uplot { inline-size: 100%; font-family: inherit; line-height: 1.3; color: var(--chart-text, var(--muted)); }
:is(.graph-card, .chart) .u-title { font-size: var(--graph-card-title-size, 0.8rem); font-weight: 700; text-align: start; color: var(--text); }
:is(.graph-card, .chart) .u-legend { font-size: 0.72rem; text-align: start; color: var(--legend-fg, var(--text)); }
:is(.graph-card, .chart) .u-legend .u-series > * { padding: 0.1em 0.35em; }
:is(.graph-card, .chart) .u-legend .u-marker { border-radius: var(--legend-swatch-radius, 2px); }
:is(.graph-card, .chart) .u-legend .u-value { font-family: var(--font-mono, monospace); font-variant-numeric: tabular-nums; }
:is(.graph-card, .chart) .u-legend .u-off > * { opacity: 0.4; }
:is(.graph-card, .chart) .u-select { background: var(--chart-selection, color-mix(in srgb, var(--accent) 14%, transparent)); }
:is(.graph-card, .chart) :is(.u-cursor-x, .u-cursor-y) { border-color: var(--chart-crosshair, var(--muted)); }

/* --- .system-pane --------------------------------------------------------- */
.system-pane {
  background: var(--system-pane-bg, var(--surface));
  color: var(--text);
  border: 1px solid var(--system-pane-border, var(--border));
  border-radius: var(--system-pane-radius, var(--radius));
  box-shadow: var(--system-pane-shadow, var(--panel-shadow, none));
}
.system-pane > summary {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem 1rem;
  min-block-size: var(--tap-min);
  padding: 0.35rem 0.75rem;
  list-style: none;
  font-size: 0.8rem;
}
.system-pane > summary::-webkit-details-marker { display: none; }
.system-pane > summary::after {
  content: "";
  margin-inline-start: auto;
  inline-size: 0.5em;
  block-size: 0.5em;
  border-block-start: 2px solid currentColor;
  border-inline-start: 2px solid currentColor;
  rotate: 45deg;
  translate: 0 0.15em;
}
.system-pane[open] > summary::after { rotate: 225deg; translate: 0 -0.1em; }
.system-pane > summary:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
.system-pane-body {
  max-block-size: var(--system-pane-max, 60vh);
  overflow: auto;
  padding: 0 0.75rem 0.75rem;
}
/* --- v5 game HUD kit (PLAN.md §19 "Game HUD") ------------------------------
 * The overlay layer (.hud) and its six edge regions, resource bars
 * (.hud-bar), the minimap (.map.hud-minimap, built on instruments.css's
 * .map/.map-pin and .compass-strip), the quest tracker (.hud-quests),
 * hotbar and inventory slots (.hud-slot) with rarity tokens, floating
 * damage/heal numbers (.hud-float), the achievement toast
 * (.toast.hud-achievement, in core's .toast-region) and key prompts
 * (.hud-prompt with core <kbd>, gamepad glyphs kbd.hud-pad).
 *
 * Values come in through documented inline APIs: --value / --trail
 * (0–1) on bars, --segments, --cooldown (0–1) on slots, --x/--y on
 * floats and the minimap player, --heading (degrees).
 * Docs: docs/components/hud.md. Demo: hud.html. */

/* Shared HUD surface: the translucent backing that keeps text readable over
   any scene. Read by .hud-bars, .hud-quests and .hud-prompt. */
:is(.hud-bars, .hud-quests, .hud-prompt) {
  background: var(--hud-panel-bg, color-mix(in srgb, var(--surface) 82%, transparent));
  border: var(--hud-panel-border-width, 1px) solid var(--hud-panel-border, color-mix(in srgb, var(--border) 60%, transparent));
  border-radius: var(--hud-panel-radius, var(--radius));
  box-shadow: var(--hud-panel-shadow, none);
  -webkit-backdrop-filter: var(--hud-panel-blur, none);
  backdrop-filter: var(--hud-panel-blur, none);
}

/* ---- The overlay layer ---------------------------------------------------
 * <div class="hud">
 *   <svg|img|video|canvas class="hud-scene">   the game view, behind
 *   <div class="hud-top">  <div class="hud-top-start">  <div class="hud-top-end">
 *   <div class="hud-bottom">  <div class="hud-bottom-start">  <div class="hud-bottom-end">
 *   <span class="hud-float" style="--x:…; --y:…">   anywhere, over the scene
 * Desktop container (> 900px): the four corners plus top and bottom centre;
 * the middle row (--hud-center-min) is kept clear. ≤ 900px: compass row,
 * then the two top corners side by side, the clear middle, the hotbar row,
 * the two bottom corners. ≤ 480px: the bottom corners stack.
 * One grid serves every tier (3 columns, 6 rows, empty tracks collapse), so
 * the regions can re-place themselves from the .hud container. */
.hud {
  container: hud / inline-size;
  position: relative;
  isolation: isolate;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  grid-template-rows: auto auto minmax(var(--hud-center-min, 8rem), 1fr) auto auto auto;
  column-gap: var(--hud-gap, var(--space-s));
  min-inline-size: 0;
  min-block-size: var(--hud-min-height, 34rem);
  padding: var(--hud-pad, var(--space-m));
  overflow: hidden;
  background: var(--hud-scene-bg, linear-gradient(var(--surface-2), var(--bg)));
  color: var(--hud-fg, var(--text));
  border: var(--hud-border-width, 1px) solid var(--hud-border, var(--border));
  border-radius: var(--hud-radius, var(--radius));
  font-family: var(--hud-font, inherit);
  font-size: var(--hud-font-size, 0.875rem);
  line-height: 1.3;
}
/* A real game: the HUD covers the viewport, clear of notches. */
.hud.is-fullscreen {
  position: fixed;
  inset: 0;
  min-block-size: 100dvh;
  padding:
    max(var(--hud-pad, var(--space-m)), env(safe-area-inset-top))
    max(var(--hud-pad, var(--space-m)), env(safe-area-inset-right))
    max(var(--hud-pad, var(--space-m)), env(safe-area-inset-bottom))
    max(var(--hud-pad, var(--space-m)), env(safe-area-inset-left));
  border: 0;
  border-radius: 0;
}
.hud-scene {
  position: absolute;
  inset: 0;
  z-index: -1;
  inline-size: 100%;
  block-size: 100%;
  max-inline-size: none;
  object-fit: cover;
  border: 0;
  filter: var(--hud-scene-filter, none);
}
:is(.hud-top, .hud-top-start, .hud-top-end, .hud-bottom, .hud-bottom-start, .hud-bottom-end) {
  display: flex;
  flex-direction: column;
  gap: var(--hud-gap, var(--space-s));
  min-inline-size: 0;
  max-inline-size: 100%;
}
.hud-top { grid-area: 1 / 2; align-items: center; }
.hud-top-start { grid-area: 1 / 1 / 4 / 2; align-self: start; align-items: flex-start; }
.hud-top-end { grid-area: 1 / 3 / 4 / 4; align-self: start; align-items: flex-end; }
.hud-bottom { grid-area: 6 / 2; align-self: end; align-items: center; }
.hud-bottom-start { grid-area: 3 / 1 / 7 / 2; align-self: end; align-items: flex-start; }
.hud-bottom-end { grid-area: 3 / 3 / 7 / 4; align-self: end; align-items: flex-end; }
.hud-top > .compass-strip { inline-size: var(--hud-compass-width, 22rem); max-inline-size: 100%; }
@container hud (max-width: 900px) {
  .hud-top { grid-area: 1 / 1 / 2 / -1; align-self: start; margin-block-end: var(--hud-gap, var(--space-s)); }
  .hud-top > .compass-strip { inline-size: min(var(--hud-compass-width, 22rem), 100%); }
  .hud-top-start { grid-area: 2 / 1 / 3 / 3; }
  .hud-top-end { grid-area: 2 / 3; }
  .hud-bottom { grid-area: 4 / 1 / 5 / -1; margin-block-end: var(--hud-gap, var(--space-s)); }
  .hud-bottom-start { grid-area: 5 / 1 / 6 / 3; }
  .hud-bottom-end { grid-area: 5 / 3; }
}
@container hud (max-width: 480px) {
  .hud-bottom-start { grid-area: 5 / 1 / 6 / -1; }
  .hud-bottom-end { grid-area: 6 / 1 / 7 / -1; margin-block-start: var(--hud-gap, var(--space-s)); }
}

/* ---- Resource bars --------------------------------------------------------
 * <div class="hud-bar hud-bar-health" role="meter" aria-label="Health"
 *      aria-valuemin="0" aria-valuemax="250" aria-valuenow="180" style="--value:.72">
 *   <span class="hud-bar-label">Health</span><span class="hud-bar-value">180</span>
 * </div>
 * Drawn on ::before: the fill (--value), under it the damage-taken trail
 * (--trail, or automatic: with motion the trail follows --value late), on
 * the track. --hud-bar-anchor: 0 fills from the start, .5 from the centre
 * (Skyrim), 1 from the end. Children are optional: a bare bar is a track. */
@property --hud-bar-v {
  syntax: "<number>";
  inherits: true;
  initial-value: 1;
}
@property --hud-bar-t {
  syntax: "<number>";
  inherits: true;
  initial-value: 1;
}
.hud-bar {
  --hud-bar-v: clamp(0, var(--value, 1), 1);
  --hud-bar-t: clamp(0, var(--trail, var(--value, 1)), 1);
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas: "label value" "track track";
  align-items: center;
  column-gap: var(--space-xs);
  row-gap: var(--space-3xs);
  inline-size: var(--hud-bar-width, 16rem);
  max-inline-size: 100%;
  min-inline-size: 0;
  color: var(--hud-bar-fg, var(--text));
  font-size: var(--hud-bar-font-size, 0.75rem);
  line-height: 1.2;
  font-variant-numeric: tabular-nums;
}
.hud-bar:has(> .icon) {
  grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-areas: "icon label value" "icon track track";
}
.hud-bar > .icon { grid-area: icon; color: var(--hud-bar-fill, var(--accent)); }
.hud-bar-label {
  grid-area: label;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-transform: var(--hud-bar-label-transform, none);
  letter-spacing: var(--hud-bar-label-spacing, normal);
}
.hud-bar-value { grid-area: value; justify-self: end; font-weight: 600; white-space: nowrap; }
.hud-bar::before {
  --hud-bar-at: calc(var(--hud-bar-anchor, 0) * 100%) 0;
  content: "";
  grid-area: track;
  block-size: var(--hud-bar-height, 0.625rem);
  background:
    var(--hud-bar-sheen, linear-gradient(rgba(255, 255, 255, 0.22), transparent 70%)) var(--hud-bar-at) / calc(var(--hud-bar-v) * 100%) 100% no-repeat,
    linear-gradient(var(--hud-bar-fill, var(--accent)) 0 0) var(--hud-bar-at) / calc(var(--hud-bar-v) * 100%) 100% no-repeat,
    linear-gradient(var(--hud-bar-trail, color-mix(in oklab, var(--hud-bar-fill, var(--accent)) 35%, var(--text))) 0 0) var(--hud-bar-at) / calc(var(--hud-bar-t) * 100%) 100% no-repeat,
    var(--hud-bar-track, var(--meter-track, var(--surface-2)));
  border: var(--hud-bar-border-width, 1px) solid var(--hud-bar-border, var(--meter-border, var(--border)));
  border-radius: var(--hud-bar-radius, var(--radius));
  box-shadow: var(--hud-bar-shadow, none);
  clip-path: var(--hud-bar-clip, none);
}
.hud-bar-health { --hud-bar-fill: var(--hud-health, var(--danger)); }
.hud-bar-mana { --hud-bar-fill: var(--hud-mana, var(--accent-2)); }
.hud-bar-stamina { --hud-bar-fill: var(--hud-stamina, var(--success)); }
.hud-bar.is-centered { --hud-bar-anchor: 0.5; }
.hud-bar.is-reversed { --hud-bar-anchor: 1; }
/* Segmented: --segments pips (10), --hud-bar-gap apart. The mask is one gap
   wider than the bar, so the last pip ends flush. */
.hud-bar.is-segmented::before {
  --hud-bar-pip: calc(100% / var(--segments, 10));
  border-color: transparent;
  mask: repeating-linear-gradient(90deg, #000 0 calc(var(--hud-bar-pip) - var(--hud-bar-gap, 3px)), transparent 0 var(--hud-bar-pip)) 0 0 / calc(100% + var(--hud-bar-gap, 3px)) 100% no-repeat;
}
/* Low: the app adds .is-low under its threshold. Red rule and text plus a
   "!" before the value; with motion the bar also pulses. */
.hud-bar.is-low { color: var(--hud-bar-low-fg, var(--danger-text, var(--danger))); }
.hud-bar.is-low::before {
  border-color: var(--hud-bar-low, var(--danger));
  box-shadow: 0 0 0 1px var(--hud-bar-low, var(--danger)), var(--hud-bar-shadow, 0 0 transparent);
}
.hud-bar.is-low > .hud-bar-value::before { content: "!" / ""; margin-inline-end: 0.3em; font-weight: 800; }
/* Regenerating: a ▲ after the value; with motion a light runs along the fill. */
.hud-bar.is-regen > .hud-bar-value::after {
  content: "\25B2" / "";
  margin-inline-start: 0.3em;
  font-size: 0.75em;
  color: var(--hud-bar-regen-fg, var(--success-text, var(--success)));
}

/* A stack of bars on the shared HUD backing. */
.hud-bars {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-xs);
  inline-size: max-content;
  max-inline-size: 100%;
  padding: var(--hud-panel-pad, var(--space-xs) var(--space-s));
}

/* ---- Minimap: core's .map, square or round -------------------------------
 * <figure class="map hud-minimap is-round" style="--heading:300">
 *   <svg|img>…</svg> <span class="hud-minimap-player"></span> .map-pin… .map-north
 * </figure> */
.map.hud-minimap {
  inline-size: var(--hud-minimap-size, 11rem);
  max-inline-size: 100%;
  min-block-size: 0;
  aspect-ratio: 1;
  border-width: var(--hud-minimap-border-width, 2px);
  box-shadow: var(--hud-minimap-shadow, inset 0 0 1.5rem rgba(0, 0, 0, 0.45));
}
.map.hud-minimap.is-round { border-radius: 50%; }
/* In a circle the corners are cut: north sits at the top centre, the
   attribution or place name at the bottom centre. */
.map.hud-minimap.is-round > .map-north { grid-column: 1 / -1; justify-self: center; }
.map.hud-minimap.is-round > .map-attribution { grid-column: 1 / -1; justify-self: center; text-align: center; }
.hud-minimap-player {
  position: absolute;
  z-index: calc(var(--z-raised) + 1);
  left: var(--x, 50%);
  top: var(--y, 50%);
  inline-size: var(--hud-minimap-player-size, 1.1rem);
  aspect-ratio: 1;
  translate: -50% -50%;
  rotate: calc(var(--heading, 0) * 1deg);
  background: var(--hud-minimap-player, var(--accent));
  clip-path: polygon(50% 0, 92% 100%, 50% 78%, 8% 100%);
  filter: drop-shadow(0 0 2px var(--bg));
}
@container hud (max-width: 480px) {
  .map.hud-minimap { --hud-minimap-size: 7.5rem; }
}

/* ---- Quest tracker --------------------------------------------------------
 * <section class="hud-quests" aria-label="Quests">
 *   <h3>The Golden Claw</h3>
 *   <ol class="hud-objectives">
 *     <li class="is-done">…</li> <li aria-current="step">… <span class="hud-count">3/5</span></li>
 *     <li class="is-failed">…</li> <li>…</li>
 *   </ol>
 * </section>
 * Each state has its own glyph (announced as a word: --hud-objective-*-label),
 * so colour is never the only signal. */
.hud-quests {
  display: grid;
  gap: var(--space-2xs);
  inline-size: var(--hud-quests-width, 17rem);
  max-inline-size: 100%;
  padding: var(--hud-panel-pad, var(--space-xs) var(--space-s));
}
.hud-quests > :is(h2, h3, h4, h5) {
  margin: 0;
  color: var(--hud-quests-title-fg, var(--text));
  font-size: 0.85rem;
  text-transform: var(--hud-quests-title-transform, none);
  letter-spacing: var(--hud-quests-title-spacing, normal);
  overflow-wrap: anywhere;
}
.hud-quests > :is(h2, h3, h4, h5):not(:first-child) { margin-block-start: var(--space-2xs); }
.hud-objectives {
  display: grid;
  gap: var(--space-3xs);
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: 0.8rem;
}
.hud-objectives > li {
  display: grid;
  grid-template-columns: 1.1em minmax(0, 1fr) auto;
  gap: var(--space-2xs);
  align-items: baseline;
  color: var(--hud-objective-fg, var(--text));
  overflow-wrap: anywhere;
}
.hud-objectives > li::before {
  content: var(--hud-objective-glyph, "\25CB") / var(--hud-objective-label, "");
  color: var(--muted);
  text-align: center;
}
.hud-objectives > li[aria-current]:not([aria-current="false"]) { font-weight: 600; }
.hud-objectives > li[aria-current]:not([aria-current="false"])::before {
  content: var(--hud-objective-current-glyph, "\25C6") / "";
  color: var(--hud-objective-current, var(--accent));
}
.hud-objectives > li.is-done { color: var(--hud-objective-done-fg, var(--muted)); text-decoration: line-through; }
.hud-objectives > li.is-done::before {
  content: var(--hud-objective-done-glyph, "\2713") / var(--hud-objective-done-label, "Done:");
  color: var(--hud-objective-done, var(--success-text, var(--success)));
}
.hud-objectives > li.is-failed { color: var(--hud-objective-failed-fg, var(--danger-text, var(--danger))); text-decoration: line-through; }
.hud-objectives > li.is-failed::before {
  content: var(--hud-objective-failed-glyph, "\2717") / var(--hud-objective-failed-label, "Failed:");
  color: inherit;
}
.hud-count {
  grid-column: 3;
  font-variant-numeric: tabular-nums;
  color: var(--hud-count-fg, var(--muted));
  text-decoration: none;
  white-space: nowrap;
}

/* ---- Hotbar and inventory slots -------------------------------------------
 * <div class="hud-hotbar" role="radiogroup" aria-label="Hotbar">
 *   <label class="hud-slot" data-rarity="rare" style="--cooldown:.4">
 *     <input type="radio" name="hotbar" aria-label="1: Frost bolt, rare, cooling down" checked>
 *     <svg class="icon" aria-hidden="true">…</svg>
 *     <kbd class="hud-slot-key">1</kbd> <span class="hud-slot-count">12</span>
 *     <span class="hud-slot-timer">4s</span>
 *   </label>
 * </div>
 * The checked radio selects the slot (.is-active also works). --cooldown
 * (0–1, the fraction still to wait) shades a clockwise sweep. */
[data-rarity="common"] { --rarity: var(--rarity-common, var(--muted)); --rarity-rank: 1; }
[data-rarity="uncommon"] { --rarity: var(--rarity-uncommon, #3fae2a); --rarity-rank: 2; }
[data-rarity="rare"] { --rarity: var(--rarity-rare, #2f80e8); --rarity-rank: 3; }
[data-rarity="epic"] { --rarity: var(--rarity-epic, #a64ff0); --rarity-rank: 4; }
[data-rarity="legendary"] { --rarity: var(--rarity-legendary, #f08a12); --rarity-rank: 5; }
.hud-slot {
  --hud-slot-s: max(var(--hud-slot-size, 3rem), var(--tap-min));
  position: relative;
  display: grid;
  place-items: center;
  flex: none;
  inline-size: var(--hud-slot-s);
  block-size: var(--hud-slot-s);
  margin: 0;
  padding: 0;
  overflow: hidden;
  background: var(--hud-slot-bg, color-mix(in srgb, var(--surface) 85%, transparent));
  color: var(--hud-slot-fg, var(--text));
  border: var(--hud-slot-border-width, 2px) solid var(--rarity, var(--hud-slot-border, var(--border)));
  border-radius: var(--hud-slot-radius, var(--radius));
  box-shadow: inset 0 0 var(--hud-slot-glow, 0.9rem) color-mix(in srgb, var(--rarity, transparent) 40%, transparent);
  font-size: 0.7rem;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  touch-action: manipulation;
}
.hud-slot > .icon { --icon-size: calc(var(--hud-slot-s) * 0.5); }
.hud-slot > input:is([type="radio"], [type="checkbox"]) {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  overflow: hidden;
  clip-path: inset(50%);
  opacity: 0;
}
/* Cooldown sweep: the part still to wait is shaded, from 12 o'clock. */
.hud-slot::before {
  content: "";
  position: absolute;
  inset: 0;
  background: conic-gradient(transparent calc((1 - var(--cooldown, 0)) * 360deg), var(--hud-slot-cooldown, rgba(0, 0, 0, 0.62)) 0);
  pointer-events: none;
}
/* Rarity rank pips (1–5), top end: the non-colour rarity signal. */
.hud-slot[data-rarity]::after {
  content: "";
  position: absolute;
  inset-block-start: 3px;
  inset-inline-end: 3px;
  inline-size: calc(var(--rarity-rank) * 5px);
  block-size: 5px;
  background: radial-gradient(circle closest-side, var(--rarity) 85%, transparent) 0 0 / 5px 5px repeat-x;
  pointer-events: none;
}
.hud-slot-key,
.hud-slot-count,
.hud-slot-timer { position: absolute; pointer-events: none; }
kbd.hud-slot-key {
  inset-block-start: 2px;
  inset-inline-start: 2px;
  padding: 0.1em 0.3em;
  font-size: 0.62rem;
  line-height: 1.2;
}
.hud-slot-count {
  inset-block-end: calc(var(--space-2xs) + 1px);
  inset-inline-end: calc(var(--space-2xs) + 1px);
  font-weight: 700;
  text-shadow: 0 0 2px var(--bg), 0 0 2px var(--bg);
}
.hud-slot-timer {
  inset: 0;
  display: grid;
  place-items: center;
  font-size: 0.85rem;
  font-weight: 800;
  text-shadow: 0 0 3px var(--bg), 0 0 3px var(--bg);
}
.hud-slot.is-empty { border-style: dashed; background: var(--hud-slot-empty-bg, color-mix(in srgb, var(--surface) 50%, transparent)); }
.hud-slot:is(:is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))), :is([aria-selected="true"], :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked)))) {
  border-color: var(--hud-slot-selected, var(--accent));
  box-shadow:
    0 0 0 var(--hud-slot-selected-width, 2px) var(--hud-slot-selected, var(--accent)),
    inset 0 0 var(--hud-slot-glow, 0.9rem) color-mix(in srgb, var(--rarity, var(--hud-slot-selected, var(--accent))) 50%, transparent);
}
/* Selected also lifts the slot and marks it, so the state isn't colour alone. */
.hud-slot:is(:is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))), :is([aria-selected="true"], :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked)))) > kbd.hud-slot-key {
  background: var(--hud-slot-selected, var(--accent));
  color: var(--hud-slot-selected-fg, var(--on-accent));
  border-color: var(--hud-slot-selected, var(--accent));
}
.hud-slot:has(> input:focus-visible) { outline: 2px solid var(--focus); outline-offset: 3px; }
.hud-slot:has(> input:disabled), .hud-slot[aria-disabled="true"] { opacity: 0.55; }
@media (hover: hover) {
  .hud-slot:has(> input:not(:disabled)):hover { background: var(--hud-slot-bg-hover, color-mix(in srgb, var(--surface-2) 92%, transparent)); }
}
.hud-hotbar {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--hud-slot-gap, var(--space-2xs));
  max-inline-size: 100%;
}
.hud-inventory {
  display: grid;
  grid-template-columns: repeat(auto-fill, max(var(--hud-slot-size, 3rem), var(--tap-min)));
  gap: var(--hud-slot-gap, var(--space-2xs));
  min-inline-size: 0;
}

/* ---- Floating damage / heal numbers ---------------------------------------
 * <span class="hud-float" style="--x:48%; --y:40%" aria-hidden="true">−48</span>
 * Absolutely placed in the nearest positioned box (.hud is one). With motion
 * it rises and fades once (--hud-float-duration); the app removes it on
 * animationend, or after a timeout when motion is off (it stays put). */
.hud-float {
  position: absolute;
  z-index: calc(var(--z-raised) + 1);
  left: var(--x, 50%);
  top: var(--y, 50%);
  translate: -50% -50%;
  color: var(--hud-float-damage, var(--danger));
  font-size: var(--hud-float-size, 1.5rem);
  font-weight: 800;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  pointer-events: none;
  paint-order: stroke fill;
  -webkit-text-stroke: var(--hud-float-stroke-width, 3px) var(--hud-float-stroke, rgba(0, 0, 0, 0.8));
  text-shadow: var(--hud-float-shadow, 0 2px 6px rgba(0, 0, 0, 0.5));
}
.hud-float.is-heal { color: var(--hud-float-heal, var(--success)); }
.hud-float.is-crit { --hud-float-size: var(--hud-float-crit-size, 2.25rem); color: var(--hud-float-crit, var(--warning)); }
.hud-float.is-mana { color: var(--hud-float-mana, var(--hud-mana, var(--accent-2))); }
.hud-float.is-miss { --hud-float-size: 1.1rem; color: var(--hud-float-miss, var(--muted)); font-style: italic; }

/* ---- Achievement toast: a .toast in core's .toast-region -------------------
 * <div class="toast-region" popover="manual" id="ach">
 *   <div class="toast hud-achievement" role="status">
 *     <svg class="icon hud-achievement-icon" aria-hidden="true">…</svg>
 *     <div class="hud-achievement-body">
 *       <span class="hud-achievement-kicker">Achievement unlocked</span>
 *       <strong class="hud-achievement-title">…</strong>
 *       <span class="hud-achievement-text">…</span>
 *       <progress class="progress" value="7" max="10">7 of 10</progress>
 *     </div>
 *     <span class="hud-achievement-score">20G</span>
 *   </div>
 * </div> */
.toast.hud-achievement {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-auto-columns: auto;
  grid-auto-flow: column;
  align-items: center;
  gap: var(--space-s);
  inline-size: min(var(--hud-achievement-width, 22rem), 100%);
  border-color: var(--hud-achievement-border, var(--toast-border, var(--border)));
}
.hud-achievement-icon {
  inline-size: var(--hud-achievement-icon-size, 2.75rem);
  block-size: var(--hud-achievement-icon-size, 2.75rem);
  min-inline-size: var(--hud-achievement-icon-size, 2.75rem);
  padding: 0.55rem;
  background: var(--hud-achievement-icon-bg, var(--accent));
  color: var(--hud-achievement-icon-fg, var(--on-accent));
  border-radius: var(--hud-achievement-icon-radius, 50%);
}
.hud-achievement-body { display: grid; gap: var(--space-3xs); min-inline-size: 0; }
.hud-achievement-kicker {
  color: var(--muted);
  font-size: 0.7rem;
  text-transform: var(--hud-achievement-kicker-transform, uppercase);
  letter-spacing: 0.06em;
}
.hud-achievement-title { overflow-wrap: anywhere; }
.hud-achievement-text { color: var(--muted); overflow-wrap: anywhere; }
.hud-achievement-body > .progress { margin-block-start: var(--space-3xs); inline-size: 100%; }
.hud-achievement-score { font-weight: 800; font-variant-numeric: tabular-nums; white-space: nowrap; }

/* ---- Key prompts ------------------------------------------------------------
 * <p class="hud-prompt"><kbd>E</kbd> Open</p>
 * <button class="hud-prompt" popovertarget="inv"><kbd>I</kbd> Inventory</button>
 * <p class="hud-prompt"><kbd class="hud-pad" data-button="south">A</kbd> Open</p>
 * Core's <kbd> draws the key; kbd.hud-pad is a gamepad button (round, pill
 * for "LB"/"RT"), ringed in the face-button colour of data-button
 * south|east|west|north. The letter is the non-colour signal. */
.hud-prompt {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  max-inline-size: 100%;
  margin: 0;
  padding: var(--hud-prompt-pad, var(--space-2xs) var(--space-xs));
  background: var(--hud-prompt-bg, var(--hud-panel-bg, color-mix(in srgb, var(--surface) 82%, transparent)));
  color: var(--hud-prompt-fg, var(--text));
  font: inherit;
  font-size: var(--hud-prompt-font-size, 0.85rem);
  line-height: 1.3;
  text-align: start;
  overflow-wrap: anywhere;
}
button.hud-prompt { min-block-size: var(--tap-min); touch-action: manipulation; }
@media (hover: hover) {
  button.hud-prompt:hover { background: var(--hud-prompt-bg-hover, var(--surface-2)); }
}
.hud-prompt > kbd { flex: none; }
kbd.hud-pad {
  display: inline-grid;
  place-items: center;
  min-inline-size: 1.75em;
  block-size: 1.75em;
  padding: 0 0.45em;
  background: var(--hud-pad-bg, var(--kbd-bg, var(--surface-2)));
  color: var(--hud-pad-fg, var(--kbd-fg, var(--text)));
  border: 2px solid var(--hud-pad-ring, var(--kbd-border, var(--border)));
  border-radius: 999px;
  box-shadow: none;
  font-family: var(--hud-pad-font, var(--font));
  font-weight: 700;
  line-height: 1;
}
kbd.hud-pad[data-button="south"] { --hud-pad-ring: var(--hud-pad-south, var(--success)); }
kbd.hud-pad[data-button="east"] { --hud-pad-ring: var(--hud-pad-east, var(--danger)); }
kbd.hud-pad[data-button="west"] { --hud-pad-ring: var(--hud-pad-west, var(--accent-2)); }
kbd.hud-pad[data-button="north"] { --hud-pad-ring: var(--hud-pad-north, var(--warning)); }

/* ---- Motion: only with no-preference, never under data-motion reduced/none */
@keyframes hud-bar-low {
  to { filter: brightness(1.6); }
}
@keyframes hud-bar-regen {
  from { background-position-x: -150%; }
  to { background-position-x: 250%; }
}
@keyframes hud-float-rise {
  0% { opacity: 0; translate: -50% -20%; scale: 0.7; }
  12% { opacity: 1; translate: -50% -60%; scale: 1.1; }
  25% { scale: 1; }
  70% { opacity: 1; }
  100% { opacity: 0; translate: -50% calc(-50% - var(--hud-float-rise, 3rem)); }
}
@keyframes hud-achievement-in {
  from { opacity: 0; translate: 0 -0.75rem; }
}
@media (prefers-reduced-motion: no-preference) {
  :root:not([data-motion="reduced"], [data-motion="none"]) .hud-bar {
    transition:
      --hud-bar-v var(--hud-bar-speed, 0.15s) ease-out,
      --hud-bar-t var(--hud-bar-trail-speed, 0.6s) ease-in var(--hud-bar-trail-delay, 0.5s);
  }
  :root:not([data-motion="reduced"], [data-motion="none"]) .hud-bar.is-low::before {
    animation: hud-bar-low 0.8s ease-in-out infinite alternate;
  }
  :root:not([data-motion="reduced"], [data-motion="none"]) .hud-bar.is-regen::after {
    content: "";
    grid-area: track;
    align-self: stretch;
    inline-size: calc(var(--hud-bar-v) * 100%);
    margin-inline-start: calc((1 - var(--hud-bar-v)) * var(--hud-bar-anchor, 0) * 100%);
    background: linear-gradient(90deg, transparent, var(--hud-bar-regen-glow, rgba(255, 255, 255, 0.55)), transparent) -150% 0 / 40% 100% no-repeat;
    clip-path: var(--hud-bar-clip, none);
    animation: hud-bar-regen 1.8s linear infinite;
    pointer-events: none;
  }
  :root:not([data-motion="reduced"], [data-motion="none"]) .hud-float {
    animation: hud-float-rise var(--hud-float-duration, 1.2s) ease-out var(--hud-float-delay, 0s) var(--hud-float-repeat, 1) both;
  }
  :root:not([data-motion="reduced"], [data-motion="none"]) .hud-achievement {
    animation: hud-achievement-in 0.35s ease-out;
  }
}

  :root[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) .hud-bar {
    transition:
      --hud-bar-v var(--hud-bar-speed, 0.15s) ease-out,
      --hud-bar-t var(--hud-bar-trail-speed, 0.6s) ease-in var(--hud-bar-trail-delay, 0.5s);
  }
  :root[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) .hud-bar.is-low::before {
    animation: hud-bar-low 0.8s ease-in-out infinite alternate;
  }
  :root[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) .hud-bar.is-regen::after {
    content: "";
    grid-area: track;
    align-self: stretch;
    inline-size: calc(var(--hud-bar-v) * 100%);
    margin-inline-start: calc((1 - var(--hud-bar-v)) * var(--hud-bar-anchor, 0) * 100%);
    background: linear-gradient(90deg, transparent, var(--hud-bar-regen-glow, rgba(255, 255, 255, 0.55)), transparent) -150% 0 / 40% 100% no-repeat;
    clip-path: var(--hud-bar-clip, none);
    animation: hud-bar-regen 1.8s linear infinite;
    pointer-events: none;
  }
  :root[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) .hud-float {
    animation: hud-float-rise var(--hud-float-duration, 1.2s) ease-out var(--hud-float-delay, 0s) var(--hud-float-repeat, 1) both;
  }
  :root[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) .hud-achievement {
    animation: hud-achievement-in 0.35s ease-out;
  }

/* --- v5 instruments and data display (PLAN.md §9 #1–16, §18 Charts) -------
 * Dial, arc and linear gauges, compass and heading strip, map frame and
 * pins, legend, progress ring, sparkline, donut, heatmap, timeline,
 * property grid, battery and signal glyphs, analog clock and countdown,
 * plus the library-neutral .chart contract.
 *
 * Every look reads a --<component>-* token with a fallback to the base
 * tokens; values come in through documented inline APIs (--value, --heading,
 * --x/--y, --seg-n, --v, --at) or native elements (<meter>, <ol>, <dl>).
 * See docs/components/instruments.md. */

/* --- Chart tokens (§18) ----------------------------------------------------
 * Series colours stay --chart-series-1..6 (core.css). New here, with
 * defaults derived from them so every theme has working scales:
 *   --chart-seq-0..8   sequential, one hue from the chart surface to full
 *   --chart-div-neg|mid|pos  diverging poles and a neutral midpoint
 *   --chart-dash-1..6  stroke-dasharray per series (tiny-palette themes)
 * Themes override any of them at root scope. */
:root {
  --chart-seq-0: color-mix(in oklab, var(--chart-seq-hue, var(--chart-series-1)) 8%, var(--chart-seq-base, var(--surface)));
  --chart-seq-1: color-mix(in oklab, var(--chart-seq-hue, var(--chart-series-1)) 18%, var(--chart-seq-base, var(--surface)));
  --chart-seq-2: color-mix(in oklab, var(--chart-seq-hue, var(--chart-series-1)) 30%, var(--chart-seq-base, var(--surface)));
  --chart-seq-3: color-mix(in oklab, var(--chart-seq-hue, var(--chart-series-1)) 42%, var(--chart-seq-base, var(--surface)));
  --chart-seq-4: color-mix(in oklab, var(--chart-seq-hue, var(--chart-series-1)) 54%, var(--chart-seq-base, var(--surface)));
  --chart-seq-5: color-mix(in oklab, var(--chart-seq-hue, var(--chart-series-1)) 66%, var(--chart-seq-base, var(--surface)));
  --chart-seq-6: color-mix(in oklab, var(--chart-seq-hue, var(--chart-series-1)) 78%, var(--chart-seq-base, var(--surface)));
  --chart-seq-7: color-mix(in oklab, var(--chart-seq-hue, var(--chart-series-1)) 90%, var(--chart-seq-base, var(--surface)));
  --chart-seq-8: var(--chart-seq-hue, var(--chart-series-1));
  --chart-div-neg: var(--chart-series-4);
  --chart-div-mid: var(--surface-2);
  --chart-div-pos: var(--chart-series-1);
  --chart-dash-1: none;
  --chart-dash-2: none;
  --chart-dash-3: none;
  --chart-dash-4: none;
  --chart-dash-5: none;
  --chart-dash-6: none;
}

/* data-series="1..6" picks a series colour (and dash) for anything that
   draws one: chart series, legend items, map pins, compass markers. */
[data-series="1"] { --series: var(--chart-series-1); --series-dash: var(--chart-dash-1); }
[data-series="2"] { --series: var(--chart-series-2); --series-dash: var(--chart-dash-2); }
[data-series="3"] { --series: var(--chart-series-3); --series-dash: var(--chart-dash-3); }
[data-series="4"] { --series: var(--chart-series-4); --series-dash: var(--chart-dash-4); }
[data-series="5"] { --series: var(--chart-series-5); --series-dash: var(--chart-dash-5); }
[data-series="6"] { --series: var(--chart-series-6); --series-dash: var(--chart-dash-6); }

/* --- .chart: the library-neutral contract (§18) ---------------------------
 * color is the chart foreground, so anything a library draws with
 * currentColor (axes, ticks, labels) follows the theme; the plot is
 * transparent. Plain-SVG hooks: .chart-grid, .chart-axis, .chart-series
 * [data-series], .chart-point, .chart-label, .chart-legend, plus
 * .chart-frame, .chart-crosshair, .chart-annotation, .chart-selection and
 * an HTML .chart-tooltip placed by the app at --x/--y. */
.chart {
  position: relative;
  min-inline-size: 0;
  color: var(--chart-fg, var(--chart-text));
  background: var(--chart-bg, transparent);
  font-size: var(--chart-font-size, 0.85rem);
}
.chart svg { inline-size: 100%; block-size: auto; overflow: visible; font-family: var(--chart-font, var(--font)); }
.chart text { fill: currentColor; stroke: none; font-size: var(--chart-text-size, 11px); }
/* SVG text scales with the chart; on a phone-width chart print it larger in
   user units so it stays readable. */
@container (max-width: 480px) { .chart text { font-size: var(--chart-text-size-narrow, 16px); } }
@media (max-width: 480px) { .chart text { font-size: var(--chart-text-size-narrow, 16px); } }
.chart-grid { fill: none; stroke: var(--chart-grid); stroke-width: 1; shape-rendering: crispEdges; }
.chart-axis { fill: none; stroke: var(--chart-axis, currentColor); stroke-width: 1; shape-rendering: crispEdges; }
.chart-frame { fill: var(--chart-plot-bg, none); stroke: var(--chart-frame, none); }
:is(.chart-grid, .chart-axis, .chart-frame, .chart-series, .chart-crosshair, .chart-annotation),
:is(.chart-grid, .chart-axis, .chart-series, .chart-annotation) * { vector-effect: non-scaling-stroke; }
.chart-series {
  color: var(--series, var(--chart-series-1));
  fill: none;
  stroke: currentColor;
  stroke-width: var(--chart-line-width, 2);
  stroke-dasharray: var(--series-dash, none);
  stroke-linejoin: round;
  stroke-linecap: round;
}
/* Bars and areas are fills; a bar takes the series colour, not a stroke. */
:is(rect.chart-series, .chart-series rect) { fill: currentColor; stroke: none; }
:is(.chart-series.is-area, .chart-series .is-area) { fill: currentColor; fill-opacity: var(--chart-area-opacity, 0.16); stroke: none; }
.chart-point {
  fill: var(--series, currentColor);
  stroke: var(--chart-point-ring, var(--surface));
  stroke-width: 2;
  stroke-dasharray: none;
}
.chart-point:is(:focus-visible, :is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))), :is([aria-selected="true"], :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked)))) { stroke: var(--chart-highlight, var(--text)); }
@media (hover: hover) {
  .chart-point:hover { stroke: var(--chart-highlight, var(--text)); }
}
/* Direct labels and values wear text colours, never the series colour. */
.chart .chart-label { fill: var(--chart-label-fg, var(--text)); }
.chart-crosshair { fill: none; stroke: var(--chart-crosshair, var(--muted)); stroke-width: 1; stroke-dasharray: 3 3; }
.chart-annotation { fill: none; stroke: var(--chart-annotation, var(--flare)); stroke-dasharray: 4 3; }
.chart text.chart-annotation, .chart-annotation text { fill: var(--chart-annotation-fg, var(--text)); stroke: none; }
.chart-selection { fill: var(--chart-selection, color-mix(in srgb, var(--accent) 14%, transparent)); stroke: none; }
.chart-tooltip {
  position: absolute;
  z-index: var(--z-tooltip);
  left: var(--x, 50%);
  top: var(--y, 0);
  translate: -50% calc(-100% - var(--space-2xs));
  max-inline-size: min(16rem, 90%);
  inline-size: max-content;
  padding: var(--space-2xs) var(--space-xs);
  background: var(--tooltip-bg, var(--surface));
  color: var(--tooltip-fg, var(--text));
  border: var(--tooltip-border-width, 1px) solid var(--tooltip-border, var(--border));
  border-radius: var(--tooltip-radius, var(--radius));
  font-size: 0.8rem;
  pointer-events: none;
}

/* --- .legend / .chart-legend (§9 #11) --------------------------------------
 *   <ul class="legend"><li data-series="1">Revenue</li>…</ul>
 * Items may be toggle buttons: <li><button aria-pressed="true">…</button>. */
:is(.legend, .chart-legend) {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3xs) var(--space-m);
  font-size: var(--legend-size, 0.85em);
  color: var(--legend-fg, var(--text));
}
:is(.legend, .chart-legend) > li { display: inline-flex; align-items: center; gap: 0.45em; min-inline-size: 0; }
:is(.legend, .chart-legend) > li:not(:has(> button))::before,
:is(.legend, .chart-legend) > li > button::before {
  content: "";
  flex: none;
  inline-size: var(--legend-swatch-size, 0.8em);
  block-size: var(--legend-swatch-size, 0.8em);
  border-radius: var(--legend-swatch-radius, 2px);
  background: var(--series, var(--chart-series-1));
}
:is(.legend, .chart-legend).is-line > li:not(:has(> button))::before,
:is(.legend, .chart-legend).is-line > li > button::before { block-size: 3px; inline-size: 1.4em; }
:is(.legend, .chart-legend).is-vertical { flex-direction: column; }
/* A colour scale (heatmap, choropleth): each item's --v (0–1) picks the step. */
:is(.legend, .chart-legend).is-scale > li::before {
  background: color-mix(in oklab, var(--chart-seq-8) calc(var(--v, 0) * 100%), var(--chart-seq-0));
}
:is(.legend, .chart-legend) > li > button {
  display: inline-flex;
  align-items: center;
  gap: 0.45em;
  min-block-size: var(--tap-min);
  padding: 0 var(--space-3xs);
  border-radius: var(--radius);
  touch-action: manipulation;
}
:is(.legend, .chart-legend) > li > button[aria-pressed="false"] { color: var(--muted); text-decoration: line-through; }
:is(.legend, .chart-legend) > li > button[aria-pressed="false"]::before { opacity: 0.3; }

/* --- Gauges: shared dial geometry (§9 #1, #2, #4, #16) -----------------------
 * .gauge-dial (needle, 270°), .gauge-arc (semicircle, no needle), .compass
 * (rose turned by --heading) and .clock (12-hour face) share one face:
 *   ::before   the arc track
 *   ::after    tick marks (major --gauge-ticks, minor --gauge-minor) and the
 *              redline band from --gauge-redline (0–1) to the end
 *   ol.gauge-ticks  printed numbers, placed round the arc by their index
 *   <meter> or .gauge-needle   the value layer: fill arc, needle and hub,
 *              turned as one. A <meter>'s value/min/max drive it where
 *              typed attr() is supported; --value (0–1) on the gauge drives
 *              it everywhere, so set both for every engine.
 *   .gauge-readout  the value printed in the lower centre
 * Sizes are in cqi of the gauge, so one --gauge-size scales everything. */
@property --gauge-v {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}
:is(.gauge-dial, .gauge-arc, .compass, .clock) {
  --gauge-from: var(--gauge-start, calc(var(--gauge-sweep, 270deg) / -2));
  --gauge-span: var(--gauge-sweep, 270deg);
  --gauge-in: var(--gauge-inset, 3cqi);
  --gauge-arc: var(--gauge-arc-width, 5cqi);
  container-type: inline-size;
  position: relative;
  isolation: isolate;
  display: inline-block;
  vertical-align: middle;
  flex: none;
  inline-size: var(--gauge-size, 12rem);
  max-inline-size: 100%;
  aspect-ratio: 1;
  overflow: hidden;
  background: var(--gauge-face, var(--surface-2));
  color: var(--gauge-label-fg, var(--text));
  border: var(--gauge-border-width, 1px) solid var(--gauge-border, var(--border));
  border-radius: 50%;
  box-shadow: var(--gauge-shadow, none);
  font-variant-numeric: tabular-nums;
}
:is(.gauge-dial, .gauge-arc, .compass, .clock)::before,
:is(.gauge-dial, .gauge-arc, .compass, .clock)::after,
:is(.gauge-dial, .gauge-arc, .compass, .clock) > :is(meter, .gauge-needle) {
  position: absolute;
  inset: var(--gauge-in) auto auto var(--gauge-in);
  inline-size: calc(100% - 2 * var(--gauge-in));
  block-size: auto;
  aspect-ratio: 1;
  border-radius: 50%;
}
:is(.gauge-dial, .gauge-arc, .compass, .clock)::before,
:is(.gauge-dial, .gauge-arc, .compass, .clock)::after { content: ""; }
/* Track */
:is(.gauge-dial, .gauge-arc, .compass, .clock)::before {
  background: conic-gradient(from var(--gauge-from), var(--gauge-track, var(--meter-track, var(--bg))) 0 var(--gauge-span), transparent 0);
  mask: radial-gradient(farthest-side, transparent calc(100% - var(--gauge-arc)), #000 calc(100% - var(--gauge-arc) + 0.5px));
}
/* Ticks and redline, in a ring just inside the track */
:is(.gauge-dial, .gauge-arc, .compass, .clock)::after {
  --gauge-tick-in: calc(var(--gauge-in) + var(--gauge-arc) + 1cqi);
  --gauge-major: calc(var(--gauge-span) / var(--gauge-ticks, 8));
  --gauge-minor-step: calc(var(--gauge-major) / var(--gauge-minor, 5));
  inset: var(--gauge-tick-in) auto auto var(--gauge-tick-in);
  inline-size: calc(100% - 2 * var(--gauge-tick-in));
  background:
    repeating-conic-gradient(from calc(var(--gauge-from) - 0.8deg), var(--gauge-tick, var(--text)) 0 1.6deg, transparent 0 var(--gauge-major)),
    repeating-conic-gradient(from calc(var(--gauge-from) - 0.3deg), var(--gauge-tick-minor, var(--muted)) 0 0.6deg, transparent 0 var(--gauge-minor-step)),
    conic-gradient(from var(--gauge-from), transparent 0 calc(var(--gauge-redline, 1) * var(--gauge-span)), var(--gauge-redline-color, var(--danger)) 0 var(--gauge-span), transparent 0);
  mask:
    radial-gradient(farthest-side, transparent calc(100% - var(--gauge-tick-length, 5cqi)), #000 calc(100% - var(--gauge-tick-length, 5cqi) + 0.5px)),
    conic-gradient(from calc(var(--gauge-from) - 1deg), #000 0 calc(var(--gauge-span) + 2deg), transparent 0);
  mask-composite: intersect;
}
/* The value layer: a <meter> (native semantics) or a span. Drawn pointing
   up, then turned to the value, so the fill arc is laid out backwards from
   the needle. */
:is(.gauge-dial, .gauge-arc, .compass, .clock) > :is(meter, .gauge-needle) {
  --gauge-v: clamp(0, var(--value, 0), 1);
  --gauge-needle-image: linear-gradient(to top,
    transparent calc(50% - var(--gauge-needle-tail, 8%) / 2),
    var(--gauge-needle, var(--danger)) 0 calc(50% + var(--gauge-needle-length, 82%) / 2),
    transparent 0);
  z-index: var(--z-raised);
  -webkit-appearance: none;
  appearance: none;
  margin: 0;
  padding: 0;
  border: 0;
  color: transparent;
  rotate: calc(var(--gauge-from) + var(--gauge-v) * var(--gauge-span));
  background:
    radial-gradient(circle closest-side, var(--gauge-hub, var(--text)) 0 var(--gauge-hub-size, 12%), transparent calc(var(--gauge-hub-size, 12%) + 1%)) center / 100% 100% no-repeat,
    var(--gauge-needle-image) center / var(--gauge-needle-width, 2.5cqi) 100% no-repeat,
    conic-gradient(from calc(var(--gauge-v) * var(--gauge-span) * -1), var(--gauge-fill, var(--accent)) 0 calc(var(--gauge-v) * var(--gauge-span)), transparent 0);
  mask:
    radial-gradient(circle closest-side, #000 0 var(--gauge-hub-size, 12%), transparent calc(var(--gauge-hub-size, 12%) + 1%)) center / 100% 100% no-repeat,
    var(--gauge-needle-image) center / var(--gauge-needle-width, 2.5cqi) 100% no-repeat,
    radial-gradient(farthest-side, transparent calc(100% - var(--gauge-arc)), #000 calc(100% - var(--gauge-arc) + 0.5px));
}
:is(.gauge-dial, .gauge-arc, .compass, .clock) > meter::-webkit-meter-bar { background: none; border: 0; }
:is(.gauge-dial, .gauge-arc, .compass, .clock) > meter::-webkit-meter-optimum-value,
:is(.gauge-dial, .gauge-arc, .compass, .clock) > meter::-webkit-meter-suboptimum-value,
:is(.gauge-dial, .gauge-arc, .compass, .clock) > meter::-webkit-meter-even-less-good-value { background: none; }
:is(.gauge-dial, .gauge-arc, .compass, .clock) > meter::-moz-meter-bar { background: none; }
/* Typed attr(): the <meter>'s own value, min and max drive the needle. */
@supports (x: attr(x type(*))) {
  :is(.gauge-dial, .gauge-arc) > meter[value] {
    --gauge-v: clamp(0, calc((attr(value type(<number>), 0) - attr(min type(<number>), 0)) / (attr(max type(<number>), 1) - attr(min type(<number>), 0))), 1);
  }
}
@media (prefers-reduced-motion: no-preference) {
  :is(.gauge-dial, .gauge-arc, .compass, .clock) > :is(meter, .gauge-needle) { transition: --gauge-v var(--gauge-speed, 0.4s) ease-out; }
}

  :root[data-motion="always"] :is(.gauge-dial, .gauge-arc, .compass, .clock) > :is(meter, .gauge-needle) { transition: --gauge-v var(--gauge-speed, 0.4s) ease-out; }

.gauge-dial.is-warn, .gauge-arc.is-warn { --gauge-fill: var(--gauge-fill-warn, var(--warning)); }
.gauge-dial.is-error, .gauge-arc.is-error { --gauge-fill: var(--gauge-fill-error, var(--danger)); }

/* Printed numbers: an <ol> whose items sit round the arc, upright. */
.gauge-ticks { display: contents; }
.gauge-ticks > li {
  --gauge-a: calc(var(--gauge-from) + var(--i, 0) * var(--gauge-span) / (var(--n, 1) + var(--gauge-closed, 0)));
  position: absolute;
  top: 50cqi;
  left: 50%;
  font-size: var(--gauge-label-size, 7cqi);
  line-height: 1;
  white-space: nowrap;
  transform: translate(-50%, -50%) rotate(var(--gauge-a)) translateY(calc(-1 * var(--gauge-label-r, 29cqi))) rotate(calc(-1 * var(--gauge-a)));
}
.gauge-ticks > li.is-redline { color: var(--gauge-redline-fg, var(--danger-text, var(--danger))); }
/* Item index (--i) and count − 1 (--n), shared with .compass-strip. */
:is(.gauge-ticks, .compass-strip) > li:nth-child(2) { --i: 1; }
:is(.gauge-ticks, .compass-strip) > li:nth-child(3) { --i: 2; }
:is(.gauge-ticks, .compass-strip) > li:nth-child(4) { --i: 3; }
:is(.gauge-ticks, .compass-strip) > li:nth-child(5) { --i: 4; }
:is(.gauge-ticks, .compass-strip) > li:nth-child(6) { --i: 5; }
:is(.gauge-ticks, .compass-strip) > li:nth-child(7) { --i: 6; }
:is(.gauge-ticks, .compass-strip) > li:nth-child(8) { --i: 7; }
:is(.gauge-ticks, .compass-strip) > li:nth-child(9) { --i: 8; }
:is(.gauge-ticks, .compass-strip) > li:nth-child(10) { --i: 9; }
:is(.gauge-ticks, .compass-strip) > li:nth-child(11) { --i: 10; }
:is(.gauge-ticks, .compass-strip) > li:nth-child(12) { --i: 11; }
:is(.gauge-ticks, .compass-strip) > li:nth-child(13) { --i: 12; }
.gauge-ticks:has(> li:nth-child(3):last-child) { --n: 2; }
.gauge-ticks:has(> li:nth-child(4):last-child) { --n: 3; }
.gauge-ticks:has(> li:nth-child(5):last-child) { --n: 4; }
.gauge-ticks:has(> li:nth-child(6):last-child) { --n: 5; }
.gauge-ticks:has(> li:nth-child(7):last-child) { --n: 6; }
.gauge-ticks:has(> li:nth-child(8):last-child) { --n: 7; }
.gauge-ticks:has(> li:nth-child(9):last-child) { --n: 8; }
.gauge-ticks:has(> li:nth-child(10):last-child) { --n: 9; }
.gauge-ticks:has(> li:nth-child(11):last-child) { --n: 10; }
.gauge-ticks:has(> li:nth-child(12):last-child) { --n: 11; }
.gauge-ticks:has(> li:nth-child(13):last-child) { --n: 12; }

.gauge-readout {
  position: absolute;
  z-index: var(--z-raised);
  inset: 62cqi 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  font-size: var(--gauge-readout-size, 6cqi);
  line-height: 1.15;
  color: var(--gauge-readout-fg, var(--muted));
  text-align: center;
}
.gauge-readout .readout {
  --readout-size: 11cqi;
  /* A theme's boxed readout (an LCD window) keeps 4px round its digits. */
  padding: max(4px, 0.1em) max(4px, 0.25em);
}
/* The arc's readout sits between its end labels: a size smaller. */
.gauge-arc .gauge-readout .readout { --readout-size: 9.5cqi; }

/* Arc gauge: the top half of the same face, no needle. */
.gauge-arc {
  --gauge-sweep: 180deg;
  --gauge-arc-width: 9cqi;
  --gauge-hub-size: 0%;
  --gauge-needle-width: 0px;
  --gauge-ticks: 4;
  --gauge-minor: 2;
  aspect-ratio: 100 / 64;
  border-radius: 50% 50% var(--radius) var(--radius) / 78% 78% var(--radius) var(--radius);
}
.gauge-arc .gauge-readout { inset: auto 0 max(5px, 3cqi); }
.gauge-arc .gauge-ticks > li { --gauge-label-r: 25cqi; }

/* --- .compass (§9 #4): a rose turned by --heading (degrees). The needle
   layer is the lubber line pointing at the heading; .is-north-up keeps the
   rose still and turns the needle instead. */
.compass {
  --gauge-start: calc(var(--heading, 0) * -1deg);
  --gauge-sweep: 360deg;
  --gauge-closed: 1;
  --gauge-ticks: 8;
  --gauge-minor: 3;
  --gauge-arc-width: 0px;
  --value: calc(var(--heading, 0) / 360);
  --gauge-hub-size: 9%;
  --gauge-needle-width: 5cqi;
  --gauge-needle-image: linear-gradient(to top,
    transparent calc(50% - var(--compass-needle-tail-length, 30%) / 2),
    var(--compass-needle-tail, var(--muted)) 0 50%,
    var(--compass-needle, var(--danger)) 0 calc(50% + var(--compass-needle-length, 70%) / 2),
    transparent 0);
}
.compass.is-north-up { --gauge-start: 0deg; }
.compass .gauge-ticks > li { --gauge-label-r: 30cqi; --gauge-label-size: 8cqi; font-weight: 600; }
.compass .gauge-ticks > li:first-child { color: var(--compass-north, var(--danger-text, var(--danger))); }

/* .compass-strip: a heading bar (Skyrim style). An <ol> of cardinal points
   every --compass-step degrees (45); items with --at are markers placed at
   their own bearing. --compass-strip-span degrees are visible (180). */
.compass-strip {
  --compass-h: var(--heading, 0);
  --compass-deg: calc(100cqi / var(--compass-strip-span, 180));
  container-type: inline-size;
  position: relative;
  overflow: hidden;
  min-inline-size: 0;
  block-size: var(--compass-strip-height, 2.5rem);
  background: var(--compass-strip-bg, var(--surface-2));
  color: var(--compass-strip-fg, var(--text));
  border: var(--compass-strip-border-width, 1px) solid var(--compass-strip-border, var(--border));
  border-radius: var(--compass-strip-radius, var(--radius));
  mask: var(--compass-strip-mask, none);
  font-variant-numeric: tabular-nums;
}
.compass-strip::before {
  content: "";
  position: absolute;
  inset: auto 0 0;
  block-size: 40%;
  background:
    linear-gradient(90deg, var(--compass-tick, var(--muted)) 0 1px, transparent 0),
    linear-gradient(90deg, var(--compass-tick-minor, var(--muted)) 0 1px, transparent 0);
  background-size: calc(var(--compass-deg) * 45) 100%, calc(var(--compass-deg) * 15) 50%;
  background-repeat: repeat-x;
  background-position-x: calc(50cqi - var(--compass-h) * var(--compass-deg)), calc(50cqi - var(--compass-h) * var(--compass-deg));
  background-position-y: 100%;
}
.compass-strip::after {
  content: "";
  position: absolute;
  inset-block: 0;
  left: 50%;
  inline-size: 2px;
  translate: -50% 0;
  background: var(--compass-lubber, var(--accent));
}
.compass-strip > li {
  --compass-a: var(--at, calc(var(--i, 0) * var(--compass-step, 45)));
  --compass-off: calc(mod(var(--compass-a) - var(--compass-h) + 180, 360) - 180);
  position: absolute;
  top: var(--space-2xs);
  left: calc(50% + var(--compass-off) * var(--compass-deg));
  translate: -50% 0;
  font-size: 0.8rem;
  font-weight: 600;
  line-height: 1.4;
  white-space: nowrap;
}
.compass-strip > li:first-child { color: var(--compass-north, var(--danger-text, var(--danger))); }
.compass-strip > li.is-marker { color: var(--series, var(--accent)); }
.compass-strip > li.is-marker .icon { --icon-size: 1rem; }

/* --- .clock (§9 #16): the dial face with 12 numbers and three hands.
   Set --h, --m and --s (numbers) on it; the app updates them. */
.clock {
  --gauge-start: 0deg;
  --gauge-sweep: 360deg;
  --gauge-closed: 1;
  --gauge-ticks: 12;
  --gauge-minor: 5;
  --gauge-arc-width: 0px;
}
.clock .gauge-ticks > li { --gauge-label-r: 32cqi; --gauge-label-size: 8cqi; }
.clock-hand {
  --clock-a: 0deg;
  position: absolute;
  z-index: var(--z-raised);
  left: 50%;
  bottom: 50%;
  inline-size: var(--clock-hand-width, 3cqi);
  block-size: 30cqi;
  translate: -50% 0;
  transform-origin: 50% 100%;
  rotate: var(--clock-a);
  background: var(--clock-hand, var(--text));
  border-radius: 1cqi;
}
.clock-hand.is-hour { --clock-a: calc((mod(var(--h, 0), 12) + var(--m, 0) / 60) * 30deg); block-size: 24cqi; inline-size: var(--clock-hour-width, 4cqi); }
.clock-hand.is-minute { --clock-a: calc((var(--m, 0) + var(--s, 0) / 60) * 6deg); block-size: 36cqi; }
.clock-hand.is-second {
  --clock-a: calc(var(--s, 0) * 6deg);
  block-size: 40cqi;
  inline-size: var(--clock-second-width, 1cqi);
  background: var(--clock-second-hand, var(--danger));
}
/* The pivot cap over the hands. */
.clock-hand.is-second::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 0;
  inline-size: 5cqi;
  aspect-ratio: 1;
  translate: -50% 50%;
  border-radius: 50%;
  background: inherit;
}

/* --- .gauge-linear (§9 #3): a .meter (div or native <meter class="meter">)
   with markers at --at (percent). Plain marks are scale labels; .is-target
   and .is-warn sit below the bar, .is-min and .is-max (recorded extremes)
   above it. Each mark's text is its label. */
.gauge-linear {
  position: relative;
  min-inline-size: 0;
  padding-block: var(--gauge-linear-label-room, calc(1.8 * var(--gauge-linear-label-size, 0.75rem)));
  font-size: var(--gauge-linear-label-size, 0.75rem);
  font-variant-numeric: tabular-nums;
  /* The bands of a .meter-fill are sized to the fill, not the track; inside
     a linear gauge the track width is known, so they land on-scale. */
  container-type: inline-size;
}
.gauge-linear .meter-fill { --meter-span: calc(100cqi - 2 * var(--meter-border-width, 1px)); }
.gauge-mark {
  position: absolute;
  inset-block: 0;
  left: var(--at, 0%);
  display: flex;
  align-items: flex-end;
  translate: calc(-1 * var(--at, 0%)) 0;
  color: var(--gauge-mark-fg, var(--muted));
  line-height: 1.2;
  white-space: nowrap;
  pointer-events: none;
}
.gauge-mark::before {
  content: "";
  position: absolute;
  left: var(--at, 0%);
  top: calc(var(--gauge-linear-label-room, calc(1.8 * var(--gauge-linear-label-size, 0.75rem))) + var(--meter-thickness, 0.6rem));
  inline-size: 1px;
  block-size: 0.35rem;
  translate: -50% 0;
  background: currentColor;
}
.gauge-mark:is(.is-target, .is-warn, .is-min, .is-max)::before {
  top: calc(var(--gauge-linear-label-room, calc(1.8 * var(--gauge-linear-label-size, 0.75rem))) - 0.3rem);
  inline-size: 2px;
  block-size: calc(var(--meter-thickness, 0.6rem) + 0.6rem);
}
.gauge-mark.is-target { color: var(--gauge-target, var(--text)); font-weight: 600; }
/* The target also gets a head, so it isn't told apart by colour alone. */
.gauge-mark.is-target::after {
  content: "";
  position: absolute;
  left: var(--at, 0%);
  top: calc(var(--gauge-linear-label-room, calc(1.8 * var(--gauge-linear-label-size, 0.75rem))) - 0.75rem);
  inline-size: 0.6rem;
  block-size: 0.45rem;
  translate: -50% 0;
  background: currentColor;
  clip-path: polygon(0 0, 100% 0, 50% 100%);
}
.gauge-mark.is-warn { color: var(--gauge-warn, var(--warning-text, var(--warning))); }
.gauge-mark.is-warn::before { background: repeating-linear-gradient(currentColor 0 3px, transparent 3px 5px); }
.gauge-mark:is(.is-min, .is-max) { align-items: flex-start; color: var(--gauge-minmax, var(--muted)); }
/* .is-needle: a "now" line with a head and no label. */
.gauge-mark.is-needle { color: var(--gauge-needle, var(--accent)); font-size: 0; }
.gauge-mark.is-needle::before {
  top: calc(var(--gauge-linear-label-room, calc(1.8 * var(--gauge-linear-label-size, 0.75rem))) - 0.3rem);
  inline-size: 2px;
  block-size: calc(var(--meter-thickness, 0.6rem) + 0.6rem);
}
.gauge-mark.is-needle::after {
  content: "";
  position: absolute;
  left: var(--at, 0%);
  top: calc(var(--gauge-linear-label-room, calc(1.8 * var(--gauge-linear-label-size, 0.75rem))) - 0.75rem);
  inline-size: 0.6rem;
  block-size: 0.45rem;
  translate: -50% 0;
  background: currentColor;
  clip-path: polygon(0 0, 100% 0, 50% 100%);
}

/* --- .map (§9 #5–7): a themed frame for an <img>, <svg>, <iframe> or
   <canvas> map. Overlays sit in a grid: .map-north top-start,
   .map-controls top-end, .map-legend above .map-scale bottom-start,
   .map-attribution bottom-end. Pins are absolutely placed at --x/--y. */
.map {
  position: relative;
  isolation: isolate;
  display: grid;
  grid-template: auto 1fr auto auto / minmax(0, 1fr) auto;
  gap: var(--space-2xs);
  min-inline-size: 0;
  min-block-size: var(--map-min-height, 14rem);
  aspect-ratio: var(--map-ratio, 16 / 10);
  padding: var(--map-pad, var(--space-xs));
  overflow: hidden;
  background: var(--map-bg, var(--surface-2));
  border: var(--map-border-width, 1px) solid var(--map-border, var(--border));
  border-radius: var(--map-radius, var(--radius));
  box-shadow: var(--map-shadow, none);
}
.map > :is(img, svg, iframe, canvas, video) {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  max-inline-size: none;
  object-fit: cover;
  border: 0;
  filter: var(--map-media-filter, none);
}
.map > :is(.map-north, .map-controls, .map-legend, .map-scale, .map-attribution) {
  position: relative;
  z-index: calc(var(--z-raised) + 1);
}
.map > :is(.map-north, .map-legend, .map-scale, .map-attribution) {
  padding: var(--space-2xs) var(--space-xs);
  background: var(--map-overlay-bg, color-mix(in srgb, var(--surface) 88%, transparent));
  color: var(--map-overlay-fg, var(--text));
  border-radius: var(--map-overlay-radius, var(--radius));
  font-size: var(--map-overlay-size, 0.75rem);
}
.map-north {
  grid-area: 1 / 1;
  justify-self: start;
  align-self: start;
  display: grid;
  justify-items: center;
  min-inline-size: 2rem;
  font-weight: 700;
  line-height: 1.3;
  rotate: calc(var(--map-bearing, 0) * -1deg);
}
.map-north::before {
  content: "";
  inline-size: 0.7rem;
  block-size: 0.8rem;
  background: var(--map-north, var(--danger));
  clip-path: polygon(50% 0, 100% 100%, 50% 75%, 0 100%);
}
.map-controls {
  grid-area: 1 / 2 / span 2;
  justify-self: end;
  align-self: start;
  display: flex;
  flex-direction: column;
  gap: var(--space-3xs);
}
.map > .map-legend {
  grid-area: 3 / 1;
  justify-self: start;
  align-self: end;
  flex-direction: column;
  gap: var(--space-3xs);
}
.map-scale {
  grid-area: 4 / 1;
  justify-self: start;
  align-self: end;
  display: grid;
  gap: 2px;
  font-variant-numeric: tabular-nums;
}
.map-scale::before {
  content: "";
  inline-size: var(--map-scale-width, 5rem);
  block-size: 0.35rem;
  border: 1px solid var(--map-scale-fg, var(--text));
  background: linear-gradient(90deg, var(--map-scale-fg, var(--text)) 0 25%, transparent 0 50%, var(--map-scale-fg, var(--text)) 0 75%, transparent 0);
}
.map-attribution {
  grid-area: 4 / 2;
  justify-self: end;
  align-self: end;
  max-inline-size: 22rem;
  --map-overlay-size: 0.68rem;
  --map-overlay-fg: var(--map-attribution-fg, var(--muted));
}
.map-attribution a { text-decoration: underline; }

/* .map-pin: a button at --x/--y (percent of the frame). Its hit area is
   --tap-min square; the drawn pin is --map-pin-size. data-series colours it;
   aria-expanded / .is-active / aria-current mark the selected one. An
   optional .map-pin-label rides beside it; a popover opens from it. */
.map-pin {
  position: absolute;
  z-index: var(--z-raised);
  left: var(--x, 50%);
  top: var(--y, 50%);
  display: grid;
  place-items: end center;
  inline-size: var(--tap-min);
  block-size: var(--tap-min);
  translate: -50% -100%;
  padding: 0;
  touch-action: manipulation;
}
.map-pin::before {
  content: "";
  inline-size: var(--map-pin-size, 1.4rem);
  block-size: var(--map-pin-size, 1.4rem);
  margin-block-end: calc(var(--map-pin-size, 1.4rem) * 0.21);
  border: 2px solid var(--map-pin-border, var(--surface));
  border-radius: 50% 50% 50% 0;
  rotate: -45deg;
  background: radial-gradient(circle, var(--map-pin-dot, var(--surface)) 0 22%, var(--series, var(--map-pin-bg, var(--accent))) 26%);
  box-shadow: var(--map-pin-shadow, 0 1px 3px rgba(0, 0, 0, 0.45));
}
.map-pin:is([aria-expanded="true"], :is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))), [aria-current]:not([aria-current="false"])) {
  z-index: calc(var(--z-raised) + 1);
  --map-pin-size: 1.8rem;
  --map-pin-border: var(--map-pin-border-active, var(--text));
}
.map-pin-label {
  position: absolute;
  left: calc(50% + var(--map-pin-size, 1.4rem) / 2 + 0.2rem);
  bottom: calc(var(--map-pin-size, 1.4rem) * 0.55);
  padding: var(--space-2xs);
  background: var(--map-overlay-bg, color-mix(in srgb, var(--surface) 88%, transparent));
  color: var(--map-overlay-fg, var(--text));
  border-radius: var(--radius);
  font-size: 0.72rem;
  line-height: 1.4;
  white-space: nowrap;
}

/* --- .progress-ring (§9 #8) -------------------------------------------------
 *   <div class="progress-ring" role="progressbar" aria-valuenow="64"
 *        aria-valuemin="0" aria-valuemax="100" style="--value:.64">64%</div>
 * Indeterminate: .is-indeterminate, or a progressbar with no aria-valuenow. */
.progress-ring {
  --progress-ring-v: clamp(0, var(--value, 0), 1);
  position: relative;
  display: inline-grid;
  place-items: center;
  flex: none;
  inline-size: var(--progress-ring-size, 3rem);
  aspect-ratio: 1;
  vertical-align: middle;
  color: var(--progress-ring-fg, var(--text));
  font-size: var(--progress-ring-text, 0.75rem);
  font-variant-numeric: tabular-nums;
  line-height: 1;
  text-align: center;
}
.progress-ring::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: conic-gradient(var(--progress-ring-fill, var(--progress-fill, var(--accent))) calc(var(--progress-ring-v) * 360deg), var(--progress-ring-track, var(--progress-bg, var(--surface-2))) 0);
  mask: radial-gradient(farthest-side, transparent calc(100% - var(--progress-ring-width, 0.3rem)), #000 calc(100% - var(--progress-ring-width, 0.3rem) + 0.5px));
}
.progress-ring:is(.is-indeterminate, [role="progressbar"]:not([aria-valuenow]))::before {
  background: conic-gradient(var(--progress-ring-fill, var(--progress-fill, var(--accent))) 0 25%, var(--progress-ring-track, var(--progress-bg, var(--surface-2))) 0);
}
@media (prefers-reduced-motion: no-preference) {
  .progress-ring:is(.is-indeterminate, [role="progressbar"]:not([aria-valuenow]))::before { animation: spinner-rotate 1s linear infinite; }
}

  :root[data-motion="always"] .progress-ring:is(.is-indeterminate, [role="progressbar"]:not([aria-valuenow]))::before { animation: spinner-rotate 1s linear infinite; }

.progress-ring.is-success { --progress-ring-fill: var(--success); }
.progress-ring.is-error { --progress-ring-fill: var(--danger); }
.progress-ring > * { position: relative; }

/* --- .sparkline (§9 #9): inline SVG, styling only ---------------------------
 *   <svg class="sparkline" viewBox="0 0 100 24" preserveAspectRatio="none"
 *        role="img" aria-label="…"><polyline points="…"/></svg>
 * Optional .sparkline-area (a filled polygon) and .sparkline-baseline. */
.sparkline {
  display: inline-block;
  vertical-align: middle;
  inline-size: var(--sparkline-width, 6em);
  block-size: var(--sparkline-height, 1.5em);
  overflow: visible;
  color: var(--sparkline-stroke, var(--chart-series-1));
  fill: none;
  stroke: currentColor;
  stroke-width: var(--sparkline-line-width, 1.5);
  stroke-linejoin: round;
  stroke-linecap: round;
}
.sparkline * { vector-effect: non-scaling-stroke; }
.sparkline .sparkline-area { fill: currentColor; fill-opacity: var(--sparkline-area-opacity, 0.16); stroke: none; }
.sparkline .sparkline-baseline { stroke: var(--chart-grid); stroke-width: 1; stroke-dasharray: 2 2; }
.sparkline.is-up { --sparkline-stroke: var(--success-text, var(--success)); }
.sparkline.is-down { --sparkline-stroke: var(--danger-text, var(--danger)); }

/* --- .donut (§9 #10): a conic-gradient ring of up to six segments, sized by
   --seg-1..--seg-6 (percentages) and coloured --chart-series-1..6, so it
   matches a .legend with data-series. --donut-hole: 0% makes a pie. */
.donut {
  --donut-a1: var(--seg-1, 0%);
  --donut-a2: calc(var(--donut-a1) + var(--seg-2, 0%));
  --donut-a3: calc(var(--donut-a2) + var(--seg-3, 0%));
  --donut-a4: calc(var(--donut-a3) + var(--seg-4, 0%));
  --donut-a5: calc(var(--donut-a4) + var(--seg-5, 0%));
  --donut-a6: calc(var(--donut-a5) + var(--seg-6, 0%));
  --donut-g: var(--donut-gap, 0.4%);
  --donut-sep: var(--donut-gap-color, var(--surface));
  position: relative;
  display: inline-grid;
  place-content: center;
  flex: none;
  inline-size: var(--donut-size, 9rem);
  max-inline-size: 100%;
  aspect-ratio: 1;
  vertical-align: middle;
  text-align: center;
  line-height: 1.2;
  font-size: 0.8rem;
  color: var(--donut-fg, var(--muted));
}
.donut::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: conic-gradient(
    var(--chart-series-1) 0 calc(var(--donut-a1) - var(--donut-g)), var(--donut-sep) 0 var(--donut-a1),
    var(--chart-series-2) 0 calc(var(--donut-a2) - var(--donut-g)), var(--donut-sep) 0 var(--donut-a2),
    var(--chart-series-3) 0 calc(var(--donut-a3) - var(--donut-g)), var(--donut-sep) 0 var(--donut-a3),
    var(--chart-series-4) 0 calc(var(--donut-a4) - var(--donut-g)), var(--donut-sep) 0 var(--donut-a4),
    var(--chart-series-5) 0 calc(var(--donut-a5) - var(--donut-g)), var(--donut-sep) 0 var(--donut-a5),
    var(--chart-series-6) 0 calc(var(--donut-a6) - var(--donut-g)), var(--donut-sep) 0 var(--donut-a6),
    var(--donut-track, var(--surface-2)) 0);
  mask: radial-gradient(farthest-side, transparent var(--donut-hole, 58%), #000 calc(var(--donut-hole, 58%) + 0.5px));
}
.donut > * { position: relative; }
.donut strong, .donut .donut-value { display: block; font-size: 1.6em; color: var(--donut-value-fg, var(--text)); font-variant-numeric: tabular-nums; }

/* --- .heatmap (§9 #12): calendar-style grid; each cell's --v (0–1) picks a
   colour on the sequential scale (.is-diverging: --v −1..1 on the diverging
   scale). Columns flow in --heatmap-rows rows (7, a week). Scrolls sideways
   inside itself when wider than its box. */
.heatmap {
  display: grid;
  grid-auto-flow: column;
  grid-template-rows: repeat(var(--heatmap-rows, 7), var(--heatmap-cell, 0.85rem));
  grid-auto-columns: var(--heatmap-cell, 0.85rem);
  gap: var(--heatmap-gap, 3px);
  justify-content: start;
  max-inline-size: 100%;
  overflow-x: auto;
  padding: 2px;
  scrollbar-width: thin;
}
.heatmap > * {
  border-radius: var(--heatmap-radius, 2px);
  background: color-mix(in oklab, var(--chart-seq-8) calc(var(--v, 0) * 100%), var(--chart-seq-0));
}
.heatmap.is-diverging > * {
  background: color-mix(in oklab, var(--chart-div-pos) calc(max(var(--v, 0), 0) * 100%),
    color-mix(in oklab, var(--chart-div-neg) calc(max(var(--v, 0) * -1, 0) * 100%), var(--chart-div-mid)));
}
.heatmap > .is-empty { background: transparent; outline: 1px dashed var(--hairline); outline-offset: -1px; }
.heatmap > :is(:focus-visible, :is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked)))) { outline: 2px solid var(--chart-highlight, var(--text)); outline-offset: 1px; }
@media (hover: hover) {
  .heatmap > :hover { outline: 2px solid var(--chart-highlight, var(--text)); outline-offset: 1px; }
}

/* --- .timeline (§9 #13): an <ol>; each <li> gets a dot on a rail.
   States: .is-done (✓), aria-current (ringed), .is-warn (!), .is-error (✕).
   .is-horizontal lays it out across on desktop and wider. */
.timeline {
  --timeline-dot-size: 1rem;
  display: grid;
  gap: var(--timeline-gap, var(--space-m));
  min-inline-size: 0;
}
.timeline > li {
  position: relative;
  min-inline-size: 0;
  padding-inline-start: calc(var(--timeline-dot-size) + var(--space-s));
  overflow-wrap: anywhere;
}
.timeline > li::before {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  top: 0.15em;
  display: grid;
  place-items: center;
  inline-size: var(--timeline-dot-size);
  block-size: var(--timeline-dot-size);
  border: 2px solid var(--timeline-dot-border, var(--border));
  border-radius: 50%;
  background: var(--timeline-dot-bg, var(--surface));
  color: var(--timeline-dot-fg, var(--on-accent));
  font-size: 0.6rem;
  font-weight: 700;
  line-height: 1;
}
.timeline > li:not(:last-child)::after {
  content: "";
  position: absolute;
  inset-inline-start: calc(var(--timeline-dot-size) / 2 - 1px);
  top: calc(0.15em + var(--timeline-dot-size));
  bottom: calc(-1 * var(--timeline-gap, var(--space-m)) - 0.15em);
  inline-size: 2px;
  background: var(--timeline-line, var(--hairline));
}
.timeline > li > time { display: block; font-size: 0.8em; color: var(--timeline-time-fg, var(--muted)); font-variant-numeric: tabular-nums; }
.timeline > li > p { color: var(--muted); }
.timeline > li.is-done::before { content: "\2713"; border-color: var(--timeline-done, var(--success)); background: var(--timeline-done, var(--success)); color: var(--on-success); }
.timeline > li.is-done:not(:last-child)::after { background: var(--timeline-done, var(--success)); }
.timeline > li[aria-current]:not([aria-current="false"])::before {
  border-color: var(--timeline-current, var(--accent));
  background: var(--timeline-current, var(--accent));
  box-shadow: 0 0 0 3px var(--timeline-current-ring, color-mix(in srgb, var(--accent) 30%, transparent));
}
.timeline > li.is-warn::before { content: "!"; border-color: var(--warning); background: var(--warning); color: var(--on-accent); }
.timeline > li.is-error::before { content: "\2715"; border-color: var(--danger); background: var(--danger); color: var(--on-danger); }
@media (min-width: 901px) {
  .timeline.is-horizontal {
    grid-auto-flow: column;
    grid-auto-columns: minmax(9rem, 1fr);
    overflow-x: auto;
    padding-block-start: 0.25rem;
  }
  .timeline.is-horizontal > li { padding-inline-start: 0; padding-block-start: calc(var(--timeline-dot-size) + var(--space-xs)); }
  .timeline.is-horizontal > li::before { top: 0; }
  .timeline.is-horizontal > li:not(:last-child)::after {
    top: calc(var(--timeline-dot-size) / 2 - 1px);
    bottom: auto;
    inset-inline-start: var(--timeline-dot-size);
    inset-inline-end: calc(-1 * var(--timeline-gap, var(--space-m)));
    inline-size: auto;
    block-size: 2px;
  }
}

/* --- dl.props (§9 #14): key/value inspector rows. <div> row wrappers are
   allowed. Keys take what they need up to 40%; long values wrap. */
.props {
  display: grid;
  grid-template-columns: fit-content(40%) minmax(0, 1fr);
  margin: 0;
  font-size: var(--props-size, 0.9em);
}
.props > div { display: contents; }
.props :is(dt, dd) {
  margin: 0;
  min-inline-size: 0;
  padding: var(--props-pad-y, var(--space-2xs)) var(--props-pad-x, var(--space-xs));
  border-block-start: 1px solid var(--props-rule, var(--hairline));
  overflow-wrap: anywhere;
}
.props dt { grid-column: 1; padding-inline-start: 0; color: var(--props-key-fg, var(--muted)); }
.props dd { grid-column: 2; padding-inline-end: 0; color: var(--props-value-fg, var(--text)); }
.props > :is(dt:first-child, dt:first-child + dd, div:first-child > *) { border-block-start: 0; }

/* --- .battery and .signal (§9 #15): status-strip glyphs sized in em.
 *   <span class="battery" role="img" aria-label="Battery 64%" style="--value:.64"></span>
 *   <span class="signal" role="img" aria-label="Signal 3 of 4" data-bars="3"></span> */
.battery {
  position: relative;
  display: inline-block;
  vertical-align: middle;
  flex: none;
  inline-size: var(--battery-width, 1.6em);
  block-size: var(--battery-height, 0.85em);
  margin-inline-end: var(--battery-nub, 0.15em);
  padding: 1.5px;
  border: var(--battery-border-width, 1.5px) solid var(--battery-border, currentColor);
  border-radius: var(--battery-radius, 0.2em);
  background: linear-gradient(var(--battery-fill, currentColor), var(--battery-fill, currentColor)) content-box no-repeat;
  background-size: calc(clamp(0, var(--value, 1), 1) * 100%) 100%;
}
.battery::after {
  content: "";
  position: absolute;
  inset-inline-start: 100%;
  top: 50%;
  inline-size: var(--battery-nub, 0.15em);
  block-size: 40%;
  translate: 1px -50%;
  border-radius: 0 1px 1px 0;
  background: var(--battery-border, currentColor);
}
.battery.is-low { --battery-fill: var(--danger); }
.battery.is-charging::before {
  content: "";
  position: absolute;
  inset: -1px 25%;
  background: var(--battery-bolt, var(--warning));
  clip-path: polygon(60% 0, 10% 58%, 45% 58%, 35% 100%, 90% 40%, 55% 40%);
}
.signal {
  --signal-on: var(--signal-fg, currentColor);
  --signal-off: var(--signal-off-fg, color-mix(in srgb, currentColor 25%, transparent));
  --signal-b1: var(--signal-off);
  --signal-b2: var(--signal-off);
  --signal-b3: var(--signal-off);
  --signal-b4: var(--signal-off);
  display: inline-block;
  vertical-align: middle;
  flex: none;
  inline-size: var(--signal-width, 1.1em);
  block-size: var(--signal-height, 0.9em);
  background:
    linear-gradient(var(--signal-b1), var(--signal-b1)) 0% 100% / 20% 25% no-repeat,
    linear-gradient(var(--signal-b2), var(--signal-b2)) 33.33% 100% / 20% 50% no-repeat,
    linear-gradient(var(--signal-b3), var(--signal-b3)) 66.67% 100% / 20% 75% no-repeat,
    linear-gradient(var(--signal-b4), var(--signal-b4)) 100% 100% / 20% 100% no-repeat;
}
.signal:is([data-bars="1"], [data-bars="2"], [data-bars="3"], [data-bars="4"]) { --signal-b1: var(--signal-on); }
.signal:is([data-bars="2"], [data-bars="3"], [data-bars="4"]) { --signal-b2: var(--signal-on); }
.signal:is([data-bars="3"], [data-bars="4"]) { --signal-b3: var(--signal-on); }
.signal[data-bars="4"] { --signal-b4: var(--signal-on); }

/* --- .countdown (§9 #16): readout-styled parts with unit labels.
 *   <div class="countdown" role="timer" aria-label="2 days 4 hours left">
 *     <span data-unit="days">02</span><span data-unit="hrs">04</span>…</div>
 * .is-urgent (final stretch) and .is-done (elapsed); change the text too. */
.countdown {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--countdown-gap, var(--space-s));
  color: var(--countdown-fg, var(--readout-fg, var(--accent-text, var(--accent))));
  background: var(--countdown-bg, var(--readout-bg, transparent));
  font-family: var(--countdown-font, var(--readout-font, var(--font-mono)));
  font-variant-numeric: tabular-nums;
  text-shadow: var(--countdown-glow, var(--readout-glow, none));
}
.countdown > * {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  font-size: var(--countdown-size, 1.75rem);
  font-weight: var(--readout-weight, 600);
  line-height: 1.1;
}
.countdown > [data-unit]::after {
  content: attr(data-unit);
  font-family: var(--font);
  font-size: 0.7rem;
  font-weight: 400;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-shadow: none;
  color: var(--countdown-unit-fg, var(--muted));
}
.countdown.is-urgent { --countdown-fg: var(--danger-text, var(--danger)); }
.countdown.is-done { --countdown-fg: var(--muted); --countdown-glow: none; }

/* --- Forced colours: keep the instruments legible with system colours. */
@media (forced-colors: active) {
  :is(.gauge-dial, .gauge-arc, .compass, .clock) { --gauge-fill: Highlight; --gauge-needle: CanvasText; --gauge-hub: CanvasText; --gauge-tick: CanvasText; --gauge-track: GrayText; forced-color-adjust: none; background: Canvas; color: CanvasText; }
  .clock-hand { forced-color-adjust: none; background: CanvasText; }
  :is(.progress-ring, .donut, .heatmap, .battery, .signal, .map-pin, .compass-strip, .legend, .chart-legend) { forced-color-adjust: none; }
}
/* --- v5 live data and dashboards (PLAN.md §18 "Live-data states",
 * "Dashboard widget grid") ---------------------------------------------------
 * State classes an app toggles as data streams in: .is-updated (a brief
 * flash), .is-up / .is-down (direction), .is-stale + data-age, the
 * .connection lamp, time.updated, the .feed stream, and the .dashboard
 * grid of .widgets with a styled edit mode.
 *
 * Nothing here needs JavaScript to render; the app adds and removes the
 * state classes (.is-updated for ~1s, .is-paused, .is-editing). Every look
 * reads a --live-* / --connection-* / --feed-* / --dashboard-* / --widget-*
 * token with a fallback to the base tokens. See docs/components/live.md. */

/* --- .is-updated: a value just changed ----------------------------------
 * The flash is a low-alpha tint (text keeps its contrast), done in the first
 * fifth of the animation, so a demo can loop it by timing alone
 * (.is-demo-ticking below). Without motion it is a static underline marker
 * for as long as the app leaves the class on. */
.is-updated {
  text-decoration-line: underline;
  text-decoration-style: var(--live-mark-style, solid);
  text-decoration-color: var(--live-mark, var(--accent));
  text-decoration-thickness: 2px;
  text-underline-offset: 0.2em;
}
@keyframes live-flash {
  0%, 12% {
    background-color: var(--live-flash-bg, color-mix(in srgb, var(--flare, var(--accent)) 28%, transparent));
    color: var(--live-flash-fg, inherit);
    box-shadow: var(--live-flash-shadow, none);
  }
}
@media (prefers-reduced-motion: no-preference) {
  :root:not([data-motion="reduced"], [data-motion="none"]) .is-updated {
    text-decoration-line: none;
    animation: live-flash var(--live-flash-duration, 1.2s) ease-out 1;
  }
  /* Demo / screenshot loop: re-trigger the flash forever, staggered by an
     inline --tick-delay per value. Real apps toggle the class instead. */
  :root:not([data-motion="reduced"], [data-motion="none"]) .is-demo-ticking .is-updated {
    animation-duration: var(--live-tick, 5s);
    animation-delay: var(--tick-delay, 0s);
    animation-iteration-count: infinite;
  }
}

  :root[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) .is-updated {
    text-decoration-line: none;
    animation: live-flash var(--live-flash-duration, 1.2s) ease-out 1;
  }
  /* Demo / screenshot loop: re-trigger the flash forever, staggered by an
     inline --tick-delay per value. Real apps toggle the class instead. */
  :root[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) .is-demo-ticking .is-updated {
    animation-duration: var(--live-tick, 5s);
    animation-delay: var(--tick-delay, 0s);
    animation-iteration-count: infinite;
  }


/* --- .is-up / .is-down: direction of change ------------------------------
 * Semantic colour plus an arrow, so it's never colour alone. .stat-trend
 * (core) keeps its own colour rule and its typed arrow; .sparkline (SVG)
 * recolours its stroke in instruments.css. Don't type your own arrow. */
/* A .readout keeps its own display colour (it may sit on a lit panel);
   the arrow carries the direction there. */
:where(.is-up, .is-down):not(svg, .sparkline, .readout) { color: var(--state-fg); }
:where(.is-up):not(svg, .sparkline) { --state-fg: var(--live-up, var(--success-text, var(--success)) ); }
:where(.is-down):not(svg, .sparkline) { --state-fg: var(--live-down, var(--danger-text, var(--danger))); }
:where(.is-up, .is-down):not(svg, .sparkline, .stat-trend, tr, tbody, table)::before {
  display: inline-block;
  margin-inline-end: 0.25em;
  font-size: 0.75em;
  vertical-align: 0.08em;
}
:where(.is-up):not(svg, .sparkline, .stat-trend, tr, tbody, table)::before { content: var(--live-up-glyph, "\25B2") / "up "; }
:where(.is-down):not(svg, .sparkline, .stat-trend, tr, tbody, table)::before { content: var(--live-down-glyph, "\25BC") / "down "; }
/* .is-flat: no change (a neutral dash). */
:where(.is-flat):not(svg)::before { content: "\25AC" / "unchanged "; margin-inline-end: 0.25em; font-size: 0.6em; vertical-align: 0.2em; color: var(--muted); }

/* --- .is-stale + data-age: out-of-date data -------------------------------
 * Muted text (a contrast-checked token, never opacity) plus a dashed
 * outline and a clock marker printing data-age, so it reads without colour. */
.is-stale:not(.readout) { color: var(--live-stale-fg, var(--muted)); }
.is-stale {
  outline: var(--live-stale-border-width, 1px) dashed var(--live-stale-border, var(--muted));
  outline-offset: -2px;
  font-style: var(--live-stale-style, normal);
}
.is-stale::after, tr.is-stale > :first-child::after {
  content: "\25F7\00a0" attr(data-age) / "out of date, " attr(data-age);
  display: inline-block;
  margin-inline-start: 0.4em;
  padding: 0 0.3em;
  border-radius: var(--radius-sm, 2px);
  background: var(--live-stale-mark-bg, var(--surface-2));
  color: var(--live-stale-mark-fg, var(--text));
  font-size: 0.72em;
  font-style: normal;
  font-weight: 600;
  line-height: 1.4;
  white-space: nowrap;
  vertical-align: 0.1em;
}
.is-stale:not([data-age])::after, tr.is-stale > :first-child::after { content: "\25F7\00a0stale" / "out of date"; }
/* Rows: mark the row; a "stale" marker lands in its first cell (put
   data-age on a cell to print the age). */
tr.is-stale { outline: none; }
tr.is-stale::after { content: none; }
tr.is-stale > td { color: var(--live-stale-fg, var(--muted)); border-block-end-style: dashed; }

/* --- .connection[data-state]: live / reconnecting / offline -------------
 *   <span class="connection" data-state="live" role="status">Live</span>
 * The lamp is drawn; the label is the element's own text. Shape differs
 * per state (filled, ring, crossed ring) so it isn't colour alone. */
.connection {
  display: inline-flex;
  align-items: center;
  gap: 0.45em;
  font-size: var(--connection-size, 0.85em);
  color: var(--connection-fg, inherit);
  white-space: nowrap;
}
.connection::before {
  content: "";
  flex: none;
  inline-size: var(--connection-lamp-size, 0.8em);
  block-size: var(--connection-lamp-size, 0.8em);
  border-radius: var(--lamp-radius, 50%);
  border: 2px solid var(--connection-lamp, var(--muted));
  background: var(--connection-lamp, var(--muted));
  box-shadow: var(--lamp-glow, 0 0 0.4rem var(--connection-lamp, transparent));
}
.connection[data-state="live"] { --connection-lamp: var(--connection-live, var(--lamp-on, var(--success))); }
.connection[data-state="reconnecting"] { --connection-lamp: var(--connection-reconnecting, var(--warning)); }
.connection[data-state="reconnecting"]::before { background: transparent; border-style: dotted; }
/* connecting: the first connect, neutral (not a fault): a muted dotted ring that turns. */
.connection[data-state="connecting"] { --connection-lamp: var(--connection-connecting, var(--muted)); }
.connection[data-state="connecting"]::before { background: transparent; border-style: dotted; box-shadow: none; }
.connection[data-state="offline"] { --connection-lamp: var(--connection-offline, var(--danger)); }
.connection[data-state="offline"]::before {
  background: linear-gradient(45deg, transparent 42%, var(--connection-lamp) 42% 58%, transparent 58%);
  box-shadow: none;
}
.connection:is([data-state="initializing"], [data-state="delayed"], [data-state="degraded"], [data-state="maintenance"], [data-state="recovering"]) {
  --connection-lamp: var(--connection-attention, var(--warning));
}
.connection:is([data-state="initializing"], [data-state="delayed"], [data-state="degraded"], [data-state="maintenance"])::before {
  background: transparent;
  border-style: dashed;
}
.connection[data-state="recovering"]::before {
  background: transparent;
  border-style: dotted;
}
@media (prefers-reduced-motion: no-preference) {
  :root:not([data-motion="reduced"], [data-motion="none"]) .connection[data-state="live"]::before {
    animation: lamp-pulse var(--lamp-pulse-duration, 2.4s) ease-in-out infinite;
  }
  :root:not([data-motion="reduced"], [data-motion="none"]) .connection:is([data-state="reconnecting"], [data-state="connecting"])::before {
    animation: connection-spin 1.6s linear infinite;
  }
}

  :root[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) .connection[data-state="live"]::before {
    animation: lamp-pulse var(--lamp-pulse-duration, 2.4s) ease-in-out infinite;
  }
  :root[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) .connection:is([data-state="reconnecting"], [data-state="connecting"])::before {
    animation: connection-spin 1.6s linear infinite;
  }

@keyframes connection-spin { to { rotate: 1turn; } }

/* --- time.updated: "Updated 5s ago" --------------------------------------
 * The app rewrites the text; keep datetime current. */
time.updated {
  display: inline-flex;
  align-items: center;
  gap: 0.3em;
  color: var(--live-updated-fg, var(--muted));
  font-size: 0.8em;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
time.updated::before { content: "\21BB" / ""; font-size: 1.05em; }

/* --- .feed: a stream of items, newest appended ---------------------------
 * <ol class="feed"> of <li>s (any content; .feed-time and .feed-source are
 * optional parts). .is-new slides an item in. The app owns auto-scroll and
 * sets .is-paused (+ data-new="3") while the reader has scrolled away. */
.feed {
  list-style: none;
  margin: 0;
  padding: 0;
  max-block-size: var(--feed-height, 20rem);
  overflow: auto;
  overflow-anchor: auto;
  scrollbar-width: thin;
  font-size: var(--feed-size, 0.88em);
}
.feed > li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.15em 0.6em;
  padding: var(--feed-pad, 0.45em 0.5em);
  border-block-end: 1px solid var(--feed-rule, var(--hairline));
  border-inline-start: 3px solid transparent;
  overflow-wrap: anywhere;
}
.feed > li > :last-child { flex: 1 1 12rem; min-inline-size: 0; }
.feed-time { flex: none; color: var(--feed-time-fg, var(--muted)); font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 0.9em; }
.feed-source { flex: none; font-weight: 700; color: var(--feed-source-fg, var(--accent-text, var(--text))); text-transform: var(--label-transform, none); letter-spacing: var(--label-tracking, normal); }
.feed > li.is-new { border-inline-start-color: var(--feed-new, var(--accent)); }
.feed > li[data-level="warn"] { border-inline-start-color: var(--warning); }
.feed > li[data-level="error"] { border-inline-start-color: var(--danger); }
@keyframes feed-in {
  from { opacity: 0; translate: 0 -0.6em; }
}
@keyframes feed-in-loop {
  0% { opacity: 0; translate: 0 -0.6em; }
  8%, 100% { opacity: 1; translate: 0; }
}
@media (prefers-reduced-motion: no-preference) {
  :root:not([data-motion="reduced"], [data-motion="none"]) .feed > li.is-new {
    animation: feed-in var(--feed-in-duration, 0.35s) ease-out both;
  }
  :root:not([data-motion="reduced"], [data-motion="none"]) .is-demo-ticking .feed > li.is-new {
    animation: feed-in-loop var(--live-tick, 5s) ease-out var(--tick-delay, 0s) infinite;
  }
}

  :root[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) .feed > li.is-new {
    animation: feed-in var(--feed-in-duration, 0.35s) ease-out both;
  }
  :root[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) .is-demo-ticking .feed > li.is-new {
    animation: feed-in-loop var(--live-tick, 5s) ease-out var(--tick-delay, 0s) infinite;
  }

/* Paused: a sticky marker at the end of the scroller. */
:is(.feed, .log).is-paused::after {
  content: "\2193\00a0" attr(data-new) " new \00b7 paused" / attr(data-new) " new items, auto-scroll paused";
  position: sticky;
  inset-block-end: 0;
  display: block;
  margin-block-start: 0.25em;
  padding: 0.3em 0.6em;
  background: var(--feed-paused-bg, var(--accent));
  color: var(--feed-paused-fg, var(--on-accent));
  font-size: 0.85em;
  font-weight: 600;
  text-align: center;
}
:is(.feed, .log).is-paused:not([data-new])::after { content: "Auto-scroll paused" / "Auto-scroll paused"; }

/* --- .depth-bar: order-book / queue depth ------------------------------
 *   <span class="depth-bar is-bid" style="--depth:.62"></span>
 * A bar of --depth (0–1) of its cell. .is-bid grows from the inline end
 * (towards the price column), .is-ask from the start. Decorative: the size
 * is in the next cell as text, so give it aria-hidden="true". */
.depth-bar {
  display: flex;
  block-size: var(--depth-bar-height, 0.9em);
  min-inline-size: var(--depth-bar-min, 3rem);
  background: var(--depth-track, transparent);
  border-radius: var(--depth-radius, 0);
}
.depth-bar::before {
  content: "";
  inline-size: calc(var(--depth, 0) * 100%);
  background: var(--depth-fill, var(--accent));
  border-radius: inherit;
}
.depth-bar.is-bid { justify-content: flex-end; --depth-fill: var(--depth-bid, var(--success)); }
.depth-bar.is-ask { --depth-fill: var(--depth-ask, var(--danger)); }

/* --- .dashboard: a grid of .widgets ---------------------------------------
 *   <div class="dashboard">
 *     <section class="panel widget" style="--cols:4; --rows:2">…</section>
 *   </div>
 * 12 columns on desktop and XL, 2 on tablet (a widget of 7+ columns spans
 * both), 1 on mobile, where --rows is dropped. --cols-tablet overrides. */
.dashboard {
  --dashboard-cols: 12;
  display: grid;
  grid-template-columns: repeat(var(--dashboard-cols), minmax(0, 1fr));
  grid-auto-rows: var(--dashboard-row, minmax(var(--dashboard-row-min, 9rem), auto));
  grid-auto-flow: row dense;
  gap: var(--dashboard-gap, var(--space-m));
  min-inline-size: 0;
}
.widget {
  grid-column: span min(var(--cols, 3), var(--dashboard-cols));
  grid-row: span var(--rows, 1);
  display: flex;
  flex-direction: column;
  min-inline-size: 0;
  min-block-size: 0;
  position: relative;
  container-type: inline-size;
}
/* A .panel-header inside keeps its pull-out; the body fills and scrolls. */
.widget-body { flex: 1 1 auto; min-block-size: 0; overflow: auto; }
.widget > .table-wrap, .widget-body > .table-wrap { max-block-size: 100%; }
@media (min-width: 481px) and (max-width: 900px) {
  .dashboard { --dashboard-cols: 2; }
  .widget { grid-column: span var(--cols-tablet, min(round(up, var(--cols, 3) / 6), 2)); }
}
@media (max-width: 480px) {
  .dashboard { --dashboard-cols: 1; }
  .widget { grid-column: 1 / -1; grid-row: auto; }
}

/* .dashboard.is-fixed — a fixed canvas (a TV board): 12 columns on every
   screen, --dashboard-rows stretched rows, no back-filling, and each widget
   placed at --x/--y (0-based) spanning --cols/--rows. Widgets keep the
   app's own display and are size containers (type in cqh). The edit-mode
   guides and placeholder follow the same grid. */
.dashboard.is-fixed {
  --dashboard-cols: 12;
  grid-template-rows: repeat(var(--dashboard-rows, 8), minmax(0, 1fr));
  grid-auto-flow: row;
  --dashboard-row-min: 0px;
}
.dashboard.is-fixed > :is(.widget, .widget-placeholder) {
  grid-column: calc(var(--x, 0) + 1) / span var(--cols, 3);
  grid-row: calc(var(--y, 0) + 1) / span var(--rows, 1);
}
:where(.dashboard.is-fixed) > .widget { display: block; container-type: size; }
/* The unsaved marker may sit outside the grid, e.g. in a toolbar: any
   .is-editing ancestor (or an edit checkbox's container) shows it. */
.is-editing :is(.dashboard-dirty) { display: flex; }

/* Inside a widget: lay out for the widget's own size. .widget-split puts
   two parts side by side from 28rem of widget width; .widget-wide hides a
   part below it. */
.widget-split { display: grid; gap: var(--space-m); align-items: center; }
.widget-wide { display: none; }
@container (min-width: 28rem) {
  .widget-split { grid-template-columns: auto minmax(0, 1fr); }
  .widget-wide { display: revert; }
}

/* Toolbar above the grid: the edit toggle, an unsaved marker, reset. */
/* The bar is the first child; its row sizes to it, not to --dashboard-row. */
.dashboard:has(> .dashboard-bar:first-child) { grid-template-rows: auto; }
.dashboard-bar {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-s);
}
.dashboard-bar > .is-pushed { margin-inline-start: auto; }

/* --- Edit mode ------------------------------------------------------------
 * .dashboard.is-editing, or CSS-only: a checked input.dashboard-edit inside
 * the dashboard (e.g. in its .dashboard-bar). The app moves things; this
 * styles the states: column guides, grab handle, resize corner, the
 * .widget.is-dragging lift, the .widget-placeholder drop target, the
 * .widget-add tile and .dashboard-dirty (unsaved) marker. */
.widget-add, .widget-placeholder, .dashboard-dirty, .dashboard-edit-only { display: none; }
.dashboard:is(.is-editing, :has(input.dashboard-edit:checked)) {
  --widget-edit-line: var(--dashboard-edit-line, var(--accent));
  background-image: repeating-linear-gradient(90deg,
    color-mix(in srgb, var(--widget-edit-line) 9%, transparent) 0 calc((100% - (var(--dashboard-cols) - 1) * var(--dashboard-gap, var(--space-m))) / var(--dashboard-cols)),
    transparent 0 calc((100% - (var(--dashboard-cols) - 1) * var(--dashboard-gap, var(--space-m))) / var(--dashboard-cols) + var(--dashboard-gap, var(--space-m))));
  border-radius: var(--radius);
}
.dashboard:is(.is-editing, :has(input.dashboard-edit:checked)) :is(.widget-add, .widget-placeholder, .dashboard-dirty) { display: flex; }
.dashboard:is(.is-editing, :has(input.dashboard-edit:checked)) .dashboard-edit-only { display: revert; }
.dashboard:is(.is-editing, :has(input.dashboard-edit:checked)) > .widget {
  outline: 2px dashed var(--widget-edit-line);
  outline-offset: 2px;
  cursor: var(--cursor-grab, grab);
}
/* Grab handle: a dotted grip centred on the top edge. */
.dashboard:is(.is-editing, :has(input.dashboard-edit:checked)) > .widget::before {
  content: "";
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 50%;
  translate: -50% -50%;
  inline-size: max(3rem, var(--tap-min, 24px));
  block-size: 0.9rem;
  border-radius: 999px;
  background: radial-gradient(circle, var(--on-accent) 1.5px, transparent 2px) 0 0 / 0.5rem 0.45rem, var(--widget-edit-line);
  z-index: var(--z-raised);
}
/* Resize corner: diagonal grip lines in the end-end corner. */
.dashboard:is(.is-editing, :has(input.dashboard-edit:checked)) > .widget::after {
  content: "";
  position: absolute;
  inset-block-end: 0;
  inset-inline-end: 0;
  inline-size: max(1.4rem, calc(var(--tap-min, 24px) * .6));
  aspect-ratio: 1;
  background: repeating-linear-gradient(135deg, transparent 0 3px, var(--widget-edit-line) 3px 5px);
  clip-path: polygon(100% 0, 100% 100%, 0 100%);
  cursor: var(--cursor-resize-diagonal, nwse-resize);
  z-index: var(--z-raised);
}
.widget.is-dragging {
  box-shadow: var(--shadow-lg, 0 1rem 2rem color-mix(in srgb, var(--text) 25%, transparent));
  rotate: -1deg;
  scale: 1.02;
  cursor: var(--cursor-grabbing, grabbing);
  z-index: var(--z-sticky);
}
.widget-placeholder, .widget-add {
  grid-column: span min(var(--cols, 3), var(--dashboard-cols));
  grid-row: span var(--rows, 1);
  align-items: center;
  justify-content: center;
  gap: 0.5em;
  min-block-size: var(--dashboard-row-min, 9rem);
  border: 2px dashed var(--widget-edit-line, var(--accent));
  border-radius: var(--panel-radius, var(--radius));
  color: var(--muted);
}
.widget-placeholder {
  background: repeating-linear-gradient(45deg, color-mix(in srgb, var(--accent) 10%, transparent) 0 6px, transparent 6px 12px);
}
.widget-add {
  font: inherit;
  background: transparent;
  color: var(--text);
  min-inline-size: var(--tap-min, 24px);
}
@media (hover: hover) {
  .widget-add:hover { background: color-mix(in srgb, var(--accent) 10%, transparent); }
}
.widget-add:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.dashboard-dirty { align-items: center; gap: 0.4em; font-size: 0.85em; }
.dashboard-dirty::before {
  content: "";
  inline-size: 0.55em;
  block-size: 0.55em;
  border-radius: 50%;
  background: var(--warning);
}
@media (max-width: 480px) {
  .widget-placeholder, .widget-add { grid-column: 1 / -1; grid-row: auto; }
}

@media (forced-colors: active) {
  .is-updated { text-decoration-color: Highlight; }
  .connection::before, .depth-bar::before { forced-color-adjust: none; }
}

/* --- App alert level: <html data-alert="yellow|red"> ---------------------
 * The app's whole-page condition (an incident, a failed deploy, red alert).
 * Core frames the viewport in --warning or --danger, pulsing at red; a
 * theme answers in its own idiom (LCARS strobes, Windows 95 blue-screens
 * its panels). The frame never takes input. Announcing the reason is the
 * app's job (a role="alert" message), not the frame's. */
:root[data-alert="yellow"] { --alert-level: var(--warning); }
:root[data-alert="red"] { --alert-level: var(--danger); }
:root[data-alert]::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: var(--z-toast);
  pointer-events: none;
  box-shadow: inset 0 0 0 4px var(--alert-level),
    inset 0 0 6rem color-mix(in srgb, var(--alert-level) 35%, transparent);
}
@keyframes alert-pulse { 50% { opacity: 0.35; } }
@media (prefers-reduced-motion: no-preference) {
  :root[data-alert="red"]::after { animation: alert-pulse 2s ease-in-out infinite; }
}

  :root[data-motion="always"][data-alert="red"]::after { animation: alert-pulse 2s ease-in-out infinite; }

/* --- v5 music and media kit (PLAN.md §19 "Music and media") -------------
 * Track rows, the playing equaliser glyph, media toggles (shuffle, repeat,
 * like, mute), the seek scrubber and volume, the now-playing bar and its
 * mini variant, the waveform scrubber, the album grid and lyrics.
 * Built on core's .btn/.btn-icon/.btn-ghost, .transport + .btn-go, .badge,
 * .knob and surfaces.css's .card.has-media. Playlist Lab is the first user.
 * Docs: docs/components/media.md. Demo: player.html. */

/* ---- Equaliser glyph -----------------------------------------------------
 * <span class="equaliser" aria-hidden="true"></span>: three bars drawn as
 * background layers (no child markup). A playing track row draws the same
 * glyph in its play button. Bars bounce only for users who allow motion;
 * .is-paused (on the glyph or the row) holds them still. */
.equaliser,
.track:is(.is-playing, [aria-current="true"]) > .track-play::before {
  content: "";
  display: inline-block;
  flex: none;
  inline-size: var(--equaliser-size, 0.9em);
  block-size: var(--equaliser-size, 0.9em);
  color: var(--equaliser-color, var(--accent-text, var(--accent)));
  background:
    linear-gradient(currentColor 0 0) 0 100% / 24% 45% no-repeat,
    linear-gradient(currentColor 0 0) 50% 100% / 24% 90% no-repeat,
    linear-gradient(currentColor 0 0) 100% 100% / 24% 65% no-repeat;
}
@keyframes equaliser {
  0%, 100% { background-size: 24% 45%, 24% 90%, 24% 65%; }
  25% { background-size: 24% 85%, 24% 40%, 24% 95%; }
  50% { background-size: 24% 25%, 24% 70%, 24% 35%; }
  75% { background-size: 24% 70%, 24% 55%, 24% 80%; }
}
@media (prefers-reduced-motion: no-preference) {
  .equaliser:not(.is-paused),
  .track:is(.is-playing, [aria-current="true"]):not(.is-paused) > .track-play::before {
    animation: equaliser var(--equaliser-speed, 0.9s) ease-in-out infinite;
  }
}

  :root[data-motion="always"] .equaliser:not(.is-paused),:root[data-motion="always"] .track:is(.is-playing, [aria-current="true"]):not(.is-paused) > .track-play::before {
    animation: equaliser var(--equaliser-speed, 0.9s) ease-in-out infinite;
  }

html[data-motion="reduced"] .equaliser,
html[data-motion="reduced"] .track > .track-play::before { animation: none; }

/* ---- Track rows ------------------------------------------------------------
 * <ol class="tracklist">
 *   <li class="track">
 *     <button class="track-play" aria-label="Play Night Drive"><span class="track-num">1</span><svg class="icon">…#icon-play</svg></button>
 *     <img src="art.jpg" alt="">
 *     <span class="track-main"><span class="track-title">Night Drive</span><span class="track-artist">Lumen Coast</span></span>
 *     <span class="track-album">Harbour Lights</span>
 *     <label class="btn btn-ghost btn-icon media-toggle is-like">…</label>
 *     <time class="track-time" datetime="PT4M3S">4:03</time>
 *   </li>
 * </ol>
 * Every cell but .track-main is optional; rows in one list should carry the
 * same cells so the columns line up. The play button shows the number at
 * rest and the play icon on row hover or keyboard focus; on touch the
 * number is the visible button. */
.tracklist {
  container-type: inline-size;
  display: grid;
  gap: var(--tracklist-gap, 0);
  margin: 0;
  padding: 0;
  list-style: none;
}
.track {
  display: flex;
  align-items: center;
  gap: var(--track-gap, var(--space-s));
  min-inline-size: 0;
  padding: var(--track-pad, var(--space-2xs) var(--space-xs));
  border-radius: var(--track-radius, var(--radius));
  color: var(--track-fg, var(--text));
}
@media (hover: hover) {
  .track:hover { background: var(--track-bg-hover, var(--row-hover-bg, color-mix(in srgb, var(--text) 6%, transparent))); }
}
.track:focus-within { background: var(--track-bg-hover, var(--row-hover-bg, color-mix(in srgb, var(--text) 6%, transparent))); }
.track:is(:is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))), :is([aria-selected="true"], :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked)))) {
  background: var(--track-bg-selected, var(--row-selected-bg, color-mix(in srgb, var(--text) 14%, transparent)));
  box-shadow: var(--track-selected-marker, var(--row-active-marker, inset 0.25rem 0 0 var(--accent)));
  color: var(--row-selected-fg, var(--track-fg, var(--text)));
  /* Secondary text follows the selected foreground, or it can vanish on a
     strong selection fill (windows95's navy). */
  --track-meta-fg: var(--row-selected-fg, var(--muted));
  --track-num-fg: var(--row-selected-fg, var(--muted));
}
.track:is(.is-playing, [aria-current="true"]) .track-title {
  color: var(--track-playing-fg, var(--accent-text, var(--accent)));
}

.track-play {
  appearance: none;
  display: inline-grid;
  place-items: center;
  flex: none;
  inline-size: var(--track-num-size, 2em);
  min-inline-size: var(--tap-min);
  min-block-size: var(--tap-min);
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: var(--radius);
  background: none;
  color: var(--track-num-fg, var(--muted));
  font: inherit;
  font-variant-numeric: tabular-nums;
  touch-action: manipulation;
}
.track-play > *,
.track-play::before { grid-area: 1 / 1; }
.track-play > .icon { visibility: hidden; color: var(--track-fg, var(--text)); }
.track:not(.is-playing, [aria-current="true"]) > .track-play:not(:has(> .track-num)) > .icon { visibility: visible; }
.track:is(.is-playing, [aria-current="true"]) > .track-play > .track-num { visibility: hidden; }
.track-play:focus-visible > .icon { visibility: visible; }
.track-play:focus-visible > .track-num,
.track-play:focus-visible::before { visibility: hidden; }
@media (hover: hover) {
  .track:hover > .track-play > .icon { visibility: visible; }
  .track:hover > .track-play > .track-num,
  .track:hover > .track-play::before { visibility: hidden; }
}

.track > img {
  flex: none;
  inline-size: var(--track-art-size, 2.5rem);
  block-size: auto;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: var(--track-art-radius, var(--radius));
  background: var(--surface-2);
}
.track-main {
  display: grid;
  flex: 2 1 0;
  min-inline-size: 0;
}
:is(.track-title, .track-artist, .track-album) {
  min-inline-size: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
:is(.track-artist, .track-album, .track-time) { color: var(--track-meta-fg, var(--muted)); }
/* On a selected row's fill, muted can drop under 4.5:1: follow the row ink. */
.track:is(:is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))), :is([aria-selected="true"], :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked)))) :is(.track-artist, .track-album, .track-time) { color: var(--track-meta-fg-active, var(--row-selected-fg, var(--text))); }
.track-artist { font-size: 0.875em; }
.track-album { flex: 1 1 0; font-size: 0.875em; }
.track-time {
  flex: none;
  min-inline-size: 5ch;
  font-size: 0.875em;
  font-variant-numeric: tabular-nums;
  text-align: end;
}
/* Explicit-content marker: <span class="badge is-explicit" role="img" aria-label="Explicit">E</span> */
.badge.is-explicit {
  margin-inline-end: 0.35em;
  padding: 0 0.3em;
  font-size: 0.7em;
  font-weight: 700;
  line-height: 1.5;
  vertical-align: 0.1em;
}

/* Row actions (.btn-ghost: like, more, remove) appear on row hover or
   focus; a pressed toggle (a liked track) stays. Touch devices can't hover,
   so they always show. */
@media (hover: hover) {
  .track:not(:hover, :focus-within) > .btn-ghost:not([aria-pressed="true"], :has(> input:checked)) { opacity: 0; }
}

/* Queue rows: a drag handle (the app does the dragging) and the drag and
   drop states. The dots are the visual; padding makes up the touch target. */
.track-handle {
  flex: none;
  inline-size: 0.75rem;
  /* Border-box, as apps' resets usually make it: the box is the target, the
     content box is the 2 x 3 dots. */
  box-sizing: border-box;
  block-size: max(var(--tap-min), 1.125rem);
  padding-block: max(0px, calc((var(--tap-min) - 1.125rem) / 2));
  background: radial-gradient(circle, currentColor 1.5px, transparent 2px) 0 0 / 0.375rem 0.375rem;
  background-origin: content-box;
  background-clip: content-box;
  color: var(--track-handle-fg, var(--muted));
  cursor: var(--cursor-grab, grab);
  touch-action: none;
}
.track.is-dragging {
  background: var(--track-drag-bg, var(--surface-2));
  box-shadow: var(--track-drag-shadow, 0 0.5rem 1.5rem rgba(0, 0, 0, 0.35));
  cursor: var(--cursor-grabbing, grabbing);
}
.track.is-drop-target { box-shadow: inset 0 var(--track-drop-width, 2px) 0 var(--track-drop-color, var(--accent)); }

@container (max-width: 480px) {
  .track-album,
  .track-time { display: none; }
  .track { gap: var(--track-gap, var(--space-xs)); }
}

/* ---- Media toggles: shuffle, repeat, like, mute ---------------------------
 * A state layer on an icon button, so it matches the theme's .btn-ghost:
 *   <label class="btn btn-ghost btn-icon media-toggle"><input type="checkbox" aria-label="Shuffle"><svg class="icon">…</svg></label>
 *   <button class="btn btn-ghost btn-icon media-toggle" aria-pressed="true">…</button>
 * On: accent plus a dot under the icon (never colour alone). .is-like fills
 * the heart instead. Two icons swap (volume / muted). Repeat off/all/one with
 * no JS: three radios in a <span>; a click hits the next one (below). The
 * box never changes width with its state. */
.media-toggle {
  position: relative;
  --btn-fg: var(--media-toggle-fg, var(--muted));
}
.media-toggle > input {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  margin: 0;
  opacity: 0;
  appearance: none;
}
.media-toggle:has(> input:focus-visible) { outline: 2px solid var(--focus); outline-offset: 2px; }
.media-toggle:is([aria-pressed="true"], :has(> input[type="checkbox"]:checked), :has(> input[type="radio"]:not(:first-of-type):checked)) {
  --btn-fg: var(--media-toggle-on, var(--accent-text, var(--accent)));
}
.media-toggle:is([aria-pressed="true"], :has(> input[type="checkbox"]:checked), :has(> input[type="radio"]:not(:first-of-type):checked))::after {
  content: var(--_dot, "");
  position: absolute;
  inset-block-start: calc(50% + 0.7em);
  inset-inline-start: 50%;
  inline-size: 0.25rem;
  block-size: 0.25rem;
  margin-inline-start: -0.125rem;
  border-radius: 50%;
  background: currentColor;
}
.media-toggle > .icon + .icon { display: none; }
.media-toggle:is([aria-pressed="true"], :has(> input[type="checkbox"]:checked), :has(> input[type="radio"]:not(:first-of-type):checked)) > .icon:has(+ .icon) { display: none; }
.media-toggle:is([aria-pressed="true"], :has(> input[type="checkbox"]:checked), :has(> input[type="radio"]:not(:first-of-type):checked)) > .icon + .icon { display: inline-block; }
/* Like: a filled heart in --media-like-on. */
.media-toggle.is-like:is([aria-pressed="true"], :has(> input:checked)) {
  --btn-fg: var(--media-like-on, var(--media-toggle-on, var(--accent-text, var(--accent))));
  --icon-fill: currentColor;
}
.media-toggle.is-like { --_dot: none; }
/* Repeat one: a small "1" at the icon's top-right corner (a theme's own
   repeat icon may fill the middle). */
.media-toggle:is(.is-one, :has(> input[type="radio"]:nth-of-type(3):checked))::before {
  content: var(--media-repeat-one-glyph, "1");
  position: absolute;
  inset-block-start: calc(50% - 1.75em);
  inset-inline-start: calc(50% + 0.55em);
  font-size: 0.6em;
  font-weight: 700;
  line-height: 1;
}
/* The no-JS cycle: only the radio after the checked one takes clicks (the
   first after the last), so each click steps off, all, one, off. Arrow
   keys move through the group as usual. */
.media-toggle > input[type="radio"] { pointer-events: none; }
.media-toggle > input[type="radio"]:checked + input[type="radio"],
.media-toggle:has(> input[type="radio"]:last-of-type:checked) > input[type="radio"]:first-of-type { pointer-events: auto; }

/* ---- Scrubber and volume ---------------------------------------------------
 * <div class="scrubber"><time>1:02</time><input type="range" …><time>-3:01</time></div>
 * <div class="volume"><label class="btn btn-ghost btn-icon media-toggle">…</label><input type="range" …></div>
 * Real range inputs. The played fill needs no script: the thumb's
 * border-image paints the track out to both sides (fill before it, track
 * after it) and the input clips the overflow. */
:is(.scrubber, .volume) {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  min-inline-size: 0;
}
.volume { gap: var(--space-2xs); }
.scrubber > time {
  flex: none;
  min-inline-size: 4.5ch;
  color: var(--scrubber-time-fg, var(--muted));
  font-size: 0.78em;
  font-variant-numeric: tabular-nums;
}
.scrubber > time:first-child { text-align: end; }
:is(.scrubber, .volume) > input[type="range"] {
  --_track: var(--scrubber-height, 0.25rem);
  --_thumb: var(--scrubber-thumb-size, 0.75rem);
  -webkit-appearance: none;
  appearance: none;
  flex: 1 1 0;
  min-inline-size: 0;
  inline-size: 100%;
  block-size: max(var(--tap-min), var(--_thumb));
  margin: 0;
  background: none;
  overflow: hidden;
  touch-action: none;
}
.volume > input[type="range"] { flex: 0 1 var(--volume-size, 6rem); }
:is(.scrubber, .volume) > input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  inline-size: var(--_thumb);
  block-size: var(--_thumb);
  border: 0;
  border-radius: var(--scrubber-thumb-radius, 50%);
  background: var(--scrubber-thumb, var(--text));
  border-image: linear-gradient(90deg, var(--scrubber-fill, var(--accent)) 50%, var(--scrubber-track, color-mix(in srgb, var(--text) 25%, transparent)) 0) 0 1 / calc(50% - var(--_track) / 2) 100vw / 0 100vw;
}
:is(.scrubber, .volume, .waveform) > input[type="range"]::-moz-range-track { background: none; }
:is(.scrubber, .volume) > input[type="range"]::-moz-range-thumb {
  inline-size: var(--_thumb);
  block-size: var(--_thumb);
  border: 0;
  border-radius: var(--scrubber-thumb-radius, 50%);
  background: var(--scrubber-thumb, var(--text));
  border-image: linear-gradient(90deg, var(--scrubber-fill, var(--accent)) 50%, var(--scrubber-track, color-mix(in srgb, var(--text) 25%, transparent)) 0) 0 1 / calc(50% - var(--_track) / 2) 100vw / 0 100vw;
}
:is(.scrubber, .volume) > input[type="range"]:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
/* The waveform clips its input, so the ring goes on the waveform. */
.waveform:has(> input:focus-visible) { outline: 2px solid var(--focus); outline-offset: 2px; }

/* ---- Waveform scrubber -----------------------------------------------------
 * <div class="waveform">
 *   <input type="range" min="0" max="243" value="88" aria-label="Seek" aria-valuetext="1:28 of 4:03">
 *   <span class="waveform-bars" aria-hidden="true"><i style="--level:.4"></i>…</span>
 * </div>
 * The range input underneath paints played and unplayed (the same thumb
 * trick as .scrubber, full height); the bars on top are windows cut into a
 * --waveform-bg mask, one <i> per bar with its --level (0–1). The app may
 * set --hover (0–1, the pointer position) for the hover preview. */
.waveform {
  --_h: var(--waveform-height, 3rem);
  display: grid;
  min-inline-size: 0;
  block-size: var(--_h);
  overflow: hidden;
  background: var(--waveform-bg, var(--surface-2));
  border-radius: var(--waveform-radius, var(--radius));
}
.scrubber > .waveform { flex: 1 1 0; }
.waveform > *,
.waveform::before { grid-area: 1 / 1; }
.waveform > input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  inline-size: 100%;
  block-size: 100%;
  margin: 0;
  background: none;
  overflow: hidden;
  touch-action: none;
}
.waveform > input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  inline-size: var(--waveform-head-width, 2px);
  block-size: var(--_h);
  border: 0;
  background: var(--waveform-head, var(--waveform-played, var(--accent)));
  /* The paint reaches past both ends however far the waveform is zoomed. */
  border-image: linear-gradient(90deg, var(--waveform-played, var(--accent)) 50%, var(--waveform-unplayed, var(--muted)) 0) 0 1 / 0 calc(100vw / (var(--zoom-to, 1) - var(--zoom-from, 0))) / 0 calc(100vw / (var(--zoom-to, 1) - var(--zoom-from, 0)));
}
.waveform > input[type="range"]::-moz-range-thumb {
  inline-size: var(--waveform-head-width, 2px);
  block-size: var(--_h);
  border: 0;
  border-radius: 0;
  background: var(--waveform-head, var(--waveform-played, var(--accent)));
  border-image: linear-gradient(90deg, var(--waveform-played, var(--accent)) 50%, var(--waveform-unplayed, var(--muted)) 0) 0 1 / 0 calc(100vw / (var(--zoom-to, 1) - var(--zoom-from, 0))) / 0 calc(100vw / (var(--zoom-to, 1) - var(--zoom-from, 0)));
}
/* Hover preview: a tint from the start to --hover, under the bar mask. */
.waveform::before {
  content: "";
  order: 1;
  justify-self: start;
  inline-size: calc(var(--hover, 0) * 100%);
  background: var(--waveform-preview, color-mix(in srgb, var(--waveform-played, var(--accent)) 45%, transparent));
  pointer-events: none;
}
.waveform-bars {
  order: 2;
  display: flex;
  pointer-events: none;
}
.waveform-bars > * {
  flex: 1 1 0;
  box-sizing: border-box;
  border-block: calc((1 - var(--_l, var(--level, 0.5))) * var(--_h) / 2) solid var(--waveform-bg, var(--surface-2));
  border-inline-end: var(--waveform-gap, 2px) solid var(--waveform-bg, var(--surface-2));
}
/* Variants: bars rising from the bottom edge, or a solid envelope. */
.waveform.is-bottom .waveform-bars > * {
  border-block-start-width: calc((1 - var(--_l, var(--level, 0.5))) * var(--_h));
  border-block-end-width: 0;
}
.waveform.is-envelope { --waveform-gap: 0px; }
/* Loading: a shimmer where the bars will be; disabled: dimmed. */
.waveform.is-loading .waveform-bars { visibility: hidden; }
.waveform.is-loading::before {
  inline-size: 100%;
  background: linear-gradient(90deg, transparent 30%, color-mix(in srgb, var(--text) 10%, transparent) 50%, transparent 70%) 0 0 / 200% 100%;
}
@media (prefers-reduced-motion: no-preference) {
  .waveform.is-loading::before { animation: waveform-shimmer 1.4s linear infinite; }
}

  :root[data-motion="always"] .waveform.is-loading::before { animation: waveform-shimmer 1.4s linear infinite; }

@keyframes waveform-shimmer { to { background-position: -200% 0; } }
.waveform:has(> input:disabled) { opacity: 0.5; }

/* ---- Waveform regions, markers and hover time -------------------------------
 * <div class="waveform" data-hover-time="0:41" style="--hover: .68">
 *   <input type="range" …seek…>
 *   <span class="waveform-bars" aria-hidden="true">…</span>
 *   <div class="waveform-region" role="group" aria-label="Chorus" style="--start: .15; --end: .37">
 *     <input type="range" min="0" max="60" step="0.1" value="9" aria-label="Chorus start">
 *     <input type="range" min="0" max="60" step="0.1" value="22" aria-label="Chorus end">
 *     <span class="waveform-region-label">Chorus</span>
 *   </div>
 *   <button class="waveform-marker" type="button" style="--at: .66">Intro ends</button>
 * </div>
 * A region's two inputs are its edges: both span the whole clip (same min
 * and max as the seek input, in seconds) so each thumb lines up with the
 * waveform; --start/--end are the same values as 0–1 fractions (controls.js
 * keeps them in step and the edges in order). Drag the region's body to
 * move it (controls.js), unless it is .is-static. A region without inputs is
 * a fixed highlight. Markers are buttons when they seek, spans otherwise;
 * .is-end puts the label on the left. data-color="1..6" picks a per-user
 * colour for a region or marker. --hover and data-hover-time are the
 * app's (the pointer position as 0–1 and its time as text). */
.waveform { position: relative; isolation: isolate; }
.waveform-bars { position: relative; z-index: var(--z-raised); }
.waveform-region {
  --_c: var(--user-color, var(--waveform-region, var(--accent)));
  --_hw: var(--waveform-handle-width, 0.5rem);
  position: relative;
  z-index: calc(var(--z-raised) + 1);
  display: grid;
  pointer-events: none;
}
/* The tint: from the start thumb's centre to the end thumb's centre. */
.waveform-region::before {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: calc(var(--_hw) / 2 + var(--start, 0) * (100% - var(--_hw)));
  inline-size: calc((var(--end, 1) - var(--start, 0)) * (100% - var(--_hw)));
  background: color-mix(in srgb, var(--_c) var(--waveform-region-alpha, 22%), transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--_c) 55%, transparent);
  pointer-events: auto;
  cursor: var(--cursor-grab, grab);
}
.waveform-region.is-static::before,
.waveform-region:not(:has(> input))::before { pointer-events: none; cursor: auto; }
.waveform-region.is-dragging::before { cursor: var(--cursor-grabbing, grabbing); }
.waveform-region > input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  grid-area: 1 / 1;
  position: relative;
  inline-size: 100%;
  block-size: 100%;
  margin: 0;
  background: none;
  pointer-events: none;
  touch-action: none;
}
.waveform-region > input[type="range"]::-webkit-slider-runnable-track { background: none; }
.waveform-region > input[type="range"]::-moz-range-track { background: none; }
.waveform-region > input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  inline-size: var(--_hw);
  block-size: var(--_h);
  border: 0;
  border-radius: var(--waveform-handle-radius, 2px);
  background: var(--_c);
  pointer-events: auto;
  cursor: var(--cursor-resize-x, ew-resize);
}
.waveform-region > input[type="range"]::-moz-range-thumb {
  inline-size: var(--_hw);
  block-size: var(--_h);
  border: 0;
  border-radius: var(--waveform-handle-radius, 2px);
  background: var(--_c);
  pointer-events: auto;
  cursor: var(--cursor-resize-x, ew-resize);
}
.waveform-region > input[type="range"]:focus-visible { outline: none; z-index: var(--z-raised); }
.waveform-region > input[type="range"]:focus-visible::-webkit-slider-thumb { outline: 2px solid var(--focus); outline-offset: 1px; }
.waveform-region > input[type="range"]:focus-visible::-moz-range-thumb { outline: 2px solid var(--focus); outline-offset: 1px; }
@media (any-pointer: coarse) {
  .waveform-region { --_hw: var(--waveform-handle-width-touch, 1.25rem); }
}
.waveform-region-label {
  position: absolute;
  inset-block-start: 0.15rem;
  inset-inline-start: max(calc(var(--_hw) + var(--start, 0) * (100% - var(--_hw)) + 0.15rem), calc(var(--zoom-from, 0) * 100% + 0.15rem));
  max-inline-size: calc((var(--end, 1) - var(--start, 0)) * (100% - var(--_hw)) - var(--_hw) - 0.3rem);
  overflow: hidden;
  padding-inline: 0.25rem;
  border-radius: var(--radius-sm, 2px);
  background: var(--_c);
  color: var(--waveform-region-fg, var(--on-accent));
  font-size: 0.7rem;
  line-height: 1.4;
  white-space: nowrap;
  text-overflow: ellipsis;
  pointer-events: none;
}
/* A marker is a chip at the top with its line running down from it. */
.waveform-marker {
  --_c: var(--user-color, var(--waveform-marker, var(--warning)));
  position: absolute;
  z-index: calc(var(--z-raised) + 1);
  inset-block-start: 0;
  inset-inline-start: calc((var(--at, 0) - var(--zoom-from, 0)) / (var(--zoom-to, 1) - var(--zoom-from, 0)) * 100%);
  margin: 0;
  padding: 0 0.3rem;
  border: 0;
  border-radius: 0 var(--radius-sm, 2px) var(--radius-sm, 2px) 0;
  background: color-mix(in srgb, var(--_c) 30%, var(--waveform-bg, var(--surface-2)));
  color: var(--waveform-marker-fg, var(--text));
  font: 600 0.7rem/1.4 var(--font);
  white-space: nowrap;
}
.waveform-marker::before {
  content: "";
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
  block-size: var(--_h);
  border-inline-start: var(--waveform-marker-width, 2px) solid var(--_c);
  pointer-events: none;
}
.waveform-marker:empty { padding: 0; }
.waveform-marker.is-end { translate: -100%; border-radius: var(--radius-sm, 2px) 0 0 var(--radius-sm, 2px); }
.waveform-marker.is-end::before { inset-inline: auto 0; }
span.waveform-marker { pointer-events: none; }
/* A seeking marker is a target: at least 24px tall (WCAG 2.5.8). */
button.waveform-marker:not(:empty) { min-block-size: 24px; }
button.waveform-marker:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
/* ---- Gain, ghost and fades (#69) -------------------------------------------
 * --gain (0–1) on a bar scales it: the visible height is --level × --gain,
 * so the waveform shows what will be heard (a fade's gain at that bar).
 * .waveform.has-ghost keeps each bar's full --level as a faint ghost behind
 * the gained bar: the gap between them is what the fade removes.
 * A .waveform-region.is-fade-in / .is-fade-out draws its tint in the shape
 * of the fade: data-curve="linear" (default) | "smooth" | "log" | "exp",
 * the curves smooth = t²(3−2t), log = log10(1+9t), exp = (e^3t−1)/(e^3−1).
 * A fade region with one input is a draggable fade length: on .is-fade-in
 * the input is the end (the start stays where the app put it), on
 * .is-fade-out the start. Markers take data-row="2" (or "3") to sit on a
 * lower row, so fade chips don't cover time chips. */
.waveform-bars > * { --_l: calc(var(--level, 0.5) * var(--gain, 1)); }
.waveform.has-ghost .waveform-bars > * {
  --_l: var(--level, 0.5);
  --_ghost: var(--waveform-ghost, color-mix(in srgb, var(--waveform-bg, var(--surface-2)) 72%, transparent));
  background: linear-gradient(var(--_ghost) 0 calc((1 - var(--gain, 1)) * 50%), transparent 0 calc((1 + var(--gain, 1)) * 50%), var(--_ghost) 0);
}
.waveform.is-bottom.has-ghost .waveform-bars > * { background: linear-gradient(var(--_ghost) 0 calc((1 - var(--gain, 1)) * 100%), transparent 0); }
.waveform-region:is(.is-fade-in, .is-fade-out) {
  --_c: var(--user-color, var(--waveform-fade, var(--waveform-region, var(--accent))));
}
.waveform-region:is(.is-fade-in, .is-fade-out)::before {
  background: color-mix(in srgb, var(--_c) var(--waveform-fade-alpha, 38%), transparent);
  box-shadow: none;
  pointer-events: none;
}
.waveform-region.is-fade-in::before { clip-path: polygon(0.00% 50.00%, 6.25% 46.88%, 12.50% 43.75%, 18.75% 40.62%, 25.00% 37.50%, 31.25% 34.38%, 37.50% 31.25%, 43.75% 28.12%, 50.00% 25.00%, 56.25% 21.88%, 62.50% 18.75%, 68.75% 15.62%, 75.00% 12.50%, 81.25% 9.38%, 87.50% 6.25%, 93.75% 3.12%, 100.00% 0.00%, 100.00% 100.00%, 93.75% 96.88%, 87.50% 93.75%, 81.25% 90.62%, 75.00% 87.50%, 68.75% 84.38%, 62.50% 81.25%, 56.25% 78.12%, 50.00% 75.00%, 43.75% 71.88%, 37.50% 68.75%, 31.25% 65.62%, 25.00% 62.50%, 18.75% 59.38%, 12.50% 56.25%, 6.25% 53.12%, 0.00% 50.00%); }
.waveform.is-bottom > .waveform-region.is-fade-in::before { clip-path: polygon(0.00% 100.00%, 6.25% 93.75%, 12.50% 87.50%, 18.75% 81.25%, 25.00% 75.00%, 31.25% 68.75%, 37.50% 62.50%, 43.75% 56.25%, 50.00% 50.00%, 56.25% 43.75%, 62.50% 37.50%, 68.75% 31.25%, 75.00% 25.00%, 81.25% 18.75%, 87.50% 12.50%, 93.75% 6.25%, 100.00% 0.00%, 100% 100%, 0% 100%); }
.waveform-region.is-fade-out::before { clip-path: polygon(0.00% 0.00%, 6.25% 3.12%, 12.50% 6.25%, 18.75% 9.38%, 25.00% 12.50%, 31.25% 15.62%, 37.50% 18.75%, 43.75% 21.88%, 50.00% 25.00%, 56.25% 28.12%, 62.50% 31.25%, 68.75% 34.38%, 75.00% 37.50%, 81.25% 40.62%, 87.50% 43.75%, 93.75% 46.88%, 100.00% 50.00%, 100.00% 50.00%, 93.75% 53.12%, 87.50% 56.25%, 81.25% 59.38%, 75.00% 62.50%, 68.75% 65.62%, 62.50% 68.75%, 56.25% 71.88%, 50.00% 75.00%, 43.75% 78.12%, 37.50% 81.25%, 31.25% 84.38%, 25.00% 87.50%, 18.75% 90.62%, 12.50% 93.75%, 6.25% 96.88%, 0.00% 100.00%); }
.waveform.is-bottom > .waveform-region.is-fade-out::before { clip-path: polygon(0.00% 0.00%, 6.25% 6.25%, 12.50% 12.50%, 18.75% 18.75%, 25.00% 25.00%, 31.25% 31.25%, 37.50% 37.50%, 43.75% 43.75%, 50.00% 50.00%, 56.25% 56.25%, 62.50% 62.50%, 68.75% 68.75%, 75.00% 75.00%, 81.25% 81.25%, 87.50% 87.50%, 93.75% 93.75%, 100.00% 100.00%, 100% 100%, 0% 100%); }
.waveform-region.is-fade-in[data-curve="smooth"]::before { clip-path: polygon(0.00% 50.00%, 6.25% 49.44%, 12.50% 47.85%, 18.75% 45.39%, 25.00% 42.19%, 31.25% 38.40%, 37.50% 34.18%, 43.75% 29.66%, 50.00% 25.00%, 56.25% 20.34%, 62.50% 15.82%, 68.75% 11.60%, 75.00% 7.81%, 81.25% 4.61%, 87.50% 2.15%, 93.75% 0.56%, 100.00% 0.00%, 100.00% 100.00%, 93.75% 99.44%, 87.50% 97.85%, 81.25% 95.39%, 75.00% 92.19%, 68.75% 88.40%, 62.50% 84.18%, 56.25% 79.66%, 50.00% 75.00%, 43.75% 70.34%, 37.50% 65.82%, 31.25% 61.60%, 25.00% 57.81%, 18.75% 54.61%, 12.50% 52.15%, 6.25% 50.56%, 0.00% 50.00%); }
.waveform.is-bottom > .waveform-region.is-fade-in[data-curve="smooth"]::before { clip-path: polygon(0.00% 100.00%, 6.25% 98.88%, 12.50% 95.70%, 18.75% 90.77%, 25.00% 84.38%, 31.25% 76.81%, 37.50% 68.36%, 43.75% 59.33%, 50.00% 50.00%, 56.25% 40.67%, 62.50% 31.64%, 68.75% 23.19%, 75.00% 15.62%, 81.25% 9.23%, 87.50% 4.30%, 93.75% 1.12%, 100.00% 0.00%, 100% 100%, 0% 100%); }
.waveform-region.is-fade-out[data-curve="smooth"]::before { clip-path: polygon(0.00% 0.00%, 6.25% 0.56%, 12.50% 2.15%, 18.75% 4.61%, 25.00% 7.81%, 31.25% 11.60%, 37.50% 15.82%, 43.75% 20.34%, 50.00% 25.00%, 56.25% 29.66%, 62.50% 34.18%, 68.75% 38.40%, 75.00% 42.19%, 81.25% 45.39%, 87.50% 47.85%, 93.75% 49.44%, 100.00% 50.00%, 100.00% 50.00%, 93.75% 50.56%, 87.50% 52.15%, 81.25% 54.61%, 75.00% 57.81%, 68.75% 61.60%, 62.50% 65.82%, 56.25% 70.34%, 50.00% 75.00%, 43.75% 79.66%, 37.50% 84.18%, 31.25% 88.40%, 25.00% 92.19%, 18.75% 95.39%, 12.50% 97.85%, 6.25% 99.44%, 0.00% 100.00%); }
.waveform.is-bottom > .waveform-region.is-fade-out[data-curve="smooth"]::before { clip-path: polygon(0.00% 0.00%, 6.25% 1.12%, 12.50% 4.30%, 18.75% 9.23%, 25.00% 15.62%, 31.25% 23.19%, 37.50% 31.64%, 43.75% 40.67%, 50.00% 50.00%, 56.25% 59.33%, 62.50% 68.36%, 68.75% 76.81%, 75.00% 84.38%, 81.25% 90.77%, 87.50% 95.70%, 93.75% 98.88%, 100.00% 100.00%, 100% 100%, 0% 100%); }
.waveform-region.is-fade-in[data-curve="log"]::before { clip-path: polygon(0.00% 50.00%, 6.25% 40.31%, 12.50% 33.63%, 18.75% 28.53%, 25.00% 24.41%, 31.25% 20.94%, 37.50% 17.95%, 43.75% 15.32%, 50.00% 12.98%, 56.25% 10.87%, 62.50% 8.94%, 68.75% 7.17%, 75.00% 5.53%, 81.25% 4.01%, 87.50% 2.59%, 93.75% 1.26%, 100.00% 0.00%, 100.00% 100.00%, 93.75% 98.74%, 87.50% 97.41%, 81.25% 95.99%, 75.00% 94.47%, 68.75% 92.83%, 62.50% 91.06%, 56.25% 89.13%, 50.00% 87.02%, 43.75% 84.68%, 37.50% 82.05%, 31.25% 79.06%, 25.00% 75.59%, 18.75% 71.47%, 12.50% 66.37%, 6.25% 59.69%, 0.00% 50.00%); }
.waveform.is-bottom > .waveform-region.is-fade-in[data-curve="log"]::before { clip-path: polygon(0.00% 100.00%, 6.25% 80.62%, 12.50% 67.26%, 18.75% 57.07%, 25.00% 48.81%, 31.25% 41.88%, 37.50% 35.90%, 43.75% 30.65%, 50.00% 25.96%, 56.25% 21.73%, 62.50% 17.88%, 68.75% 14.34%, 75.00% 11.07%, 81.25% 8.03%, 87.50% 5.18%, 93.75% 2.51%, 100.00% 0.00%, 100% 100%, 0% 100%); }
.waveform-region.is-fade-out[data-curve="log"]::before { clip-path: polygon(0.00% 0.00%, 6.25% 1.26%, 12.50% 2.59%, 18.75% 4.01%, 25.00% 5.53%, 31.25% 7.17%, 37.50% 8.94%, 43.75% 10.87%, 50.00% 12.98%, 56.25% 15.32%, 62.50% 17.95%, 68.75% 20.94%, 75.00% 24.41%, 81.25% 28.53%, 87.50% 33.63%, 93.75% 40.31%, 100.00% 50.00%, 100.00% 50.00%, 93.75% 59.69%, 87.50% 66.37%, 81.25% 71.47%, 75.00% 75.59%, 68.75% 79.06%, 62.50% 82.05%, 56.25% 84.68%, 50.00% 87.02%, 43.75% 89.13%, 37.50% 91.06%, 31.25% 92.83%, 25.00% 94.47%, 18.75% 95.99%, 12.50% 97.41%, 6.25% 98.74%, 0.00% 100.00%); }
.waveform.is-bottom > .waveform-region.is-fade-out[data-curve="log"]::before { clip-path: polygon(0.00% 0.00%, 6.25% 2.51%, 12.50% 5.18%, 18.75% 8.03%, 25.00% 11.07%, 31.25% 14.34%, 37.50% 17.88%, 43.75% 21.73%, 50.00% 25.96%, 56.25% 30.65%, 62.50% 35.90%, 68.75% 41.88%, 75.00% 48.81%, 81.25% 57.07%, 87.50% 67.26%, 93.75% 80.62%, 100.00% 100.00%, 100% 100%, 0% 100%); }
.waveform-region.is-fade-in[data-curve="exp"]::before { clip-path: polygon(0.00% 50.00%, 6.25% 49.46%, 12.50% 48.81%, 18.75% 48.02%, 25.00% 47.07%, 31.25% 45.93%, 37.50% 44.55%, 43.75% 42.89%, 50.00% 40.88%, 56.25% 38.46%, 62.50% 35.54%, 68.75% 32.01%, 75.00% 27.76%, 81.25% 22.64%, 87.50% 16.45%, 93.75% 9.00%, 100.00% 0.00%, 100.00% 100.00%, 93.75% 91.00%, 87.50% 83.55%, 81.25% 77.36%, 75.00% 72.24%, 68.75% 67.99%, 62.50% 64.46%, 56.25% 61.54%, 50.00% 59.12%, 43.75% 57.11%, 37.50% 55.45%, 31.25% 54.07%, 25.00% 52.93%, 18.75% 51.98%, 12.50% 51.19%, 6.25% 50.54%, 0.00% 50.00%); }
.waveform.is-bottom > .waveform-region.is-fade-in[data-curve="exp"]::before { clip-path: polygon(0.00% 100.00%, 6.25% 98.92%, 12.50% 97.62%, 18.75% 96.04%, 25.00% 94.15%, 31.25% 91.86%, 37.50% 89.10%, 43.75% 85.77%, 50.00% 81.76%, 56.25% 76.91%, 62.50% 71.07%, 68.75% 64.03%, 75.00% 55.53%, 81.25% 45.28%, 87.50% 32.91%, 93.75% 17.99%, 100.00% 0.00%, 100% 100%, 0% 100%); }
.waveform-region.is-fade-out[data-curve="exp"]::before { clip-path: polygon(0.00% 0.00%, 6.25% 9.00%, 12.50% 16.45%, 18.75% 22.64%, 25.00% 27.76%, 31.25% 32.01%, 37.50% 35.54%, 43.75% 38.46%, 50.00% 40.88%, 56.25% 42.89%, 62.50% 44.55%, 68.75% 45.93%, 75.00% 47.07%, 81.25% 48.02%, 87.50% 48.81%, 93.75% 49.46%, 100.00% 50.00%, 100.00% 50.00%, 93.75% 50.54%, 87.50% 51.19%, 81.25% 51.98%, 75.00% 52.93%, 68.75% 54.07%, 62.50% 55.45%, 56.25% 57.11%, 50.00% 59.12%, 43.75% 61.54%, 37.50% 64.46%, 31.25% 67.99%, 25.00% 72.24%, 18.75% 77.36%, 12.50% 83.55%, 6.25% 91.00%, 0.00% 100.00%); }
.waveform.is-bottom > .waveform-region.is-fade-out[data-curve="exp"]::before { clip-path: polygon(0.00% 0.00%, 6.25% 17.99%, 12.50% 32.91%, 18.75% 45.28%, 25.00% 55.53%, 31.25% 64.03%, 37.50% 71.07%, 43.75% 76.91%, 50.00% 81.76%, 56.25% 85.77%, 62.50% 89.10%, 68.75% 91.86%, 75.00% 94.15%, 81.25% 96.04%, 87.50% 97.62%, 93.75% 98.92%, 100.00% 100.00%, 100% 100%, 0% 100%); }
.waveform-marker[data-row="2"] { --_row: 1; }
.waveform-marker[data-row="3"] { --_row: 2; }
.waveform-marker[data-row] { inset-block-start: calc(var(--_row) * var(--waveform-marker-row, 1.1rem)); }
.waveform-marker[data-row]::before { inset-block-start: calc(var(--_row) * var(--waveform-marker-row, 1.1rem) * -1); }
/* ---- Zoom: a window onto part of the clip (#70) -----------------------------
 * <div class="waveform" id="wf" style="--zoom-from:.2; --zoom-to:.35">…</div>
 * <div class="waveform-ruler" data-for="wf"></div>
 * <div class="waveform-zoom btn-group" data-for="wf"><button data-zoom="in">…
 * --zoom-from / --zoom-to are the visible part as 0–1 of the clip. The seek
 * input, the regions and the bars are stretched to the whole clip at that
 * scale and slid left, so every input keeps its full min, max and value
 * (the browser never clamps one) and region dragging still works. Markers
 * map --at into the window. controls.js drives the window (buttons, wheel
 * pan, box zoom, the overview), marks regions running out of view
 * (.is-before / .is-after: an arrow at that side) and fills the ruler.
 * Bars swapped for finer data covering only the window take .is-window. */
@property --zoom-from { syntax: "<number>"; inherits: true; initial-value: 0; }
@property --zoom-to { syntax: "<number>"; inherits: true; initial-value: 1; }
.waveform > :is(input[type="range"], .waveform-region, .waveform-bars:not(.is-window)) {
  justify-self: start;
  inline-size: calc(100% / (var(--zoom-to) - var(--zoom-from)));
  margin-inline-start: calc(-100% * var(--zoom-from) / (var(--zoom-to) - var(--zoom-from)));
}
@media (prefers-reduced-motion: no-preference) {
  :root:not([data-motion="reduced"], [data-motion="none"]) .waveform { transition: --zoom-from 0.2s ease, --zoom-to 0.2s ease; }
}

  :root[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) .waveform { transition: --zoom-from 0.2s ease, --zoom-to 0.2s ease; }

.waveform-region.is-before { --_arrow-start: var(--_c); }
.waveform-region.is-after { --_arrow-end: var(--_c); }
.waveform-region:is(.is-before, .is-after)::after {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: calc(var(--zoom-from) * 100%);
  inline-size: calc((var(--zoom-to) - var(--zoom-from)) * 100%);
  background:
    conic-gradient(from 45deg at 0 50%, var(--_arrow-start, transparent) 0 90deg, transparent 0) 0 50% / 0.55rem 1.1rem no-repeat,
    conic-gradient(from 225deg at 100% 50%, var(--_arrow-end, transparent) 0 90deg, transparent 0) 100% 50% / 0.55rem 1.1rem no-repeat;
  pointer-events: none;
}
.waveform.is-zoom-select { cursor: crosshair; }
.waveform.is-zoom-select > * { pointer-events: none; }
.waveform.is-zoom-select::before {
  position: relative;
  z-index: calc(var(--z-raised) + 3);
  margin-inline-start: calc(var(--select-a, 0) * 100%);
  inline-size: calc((var(--select-b, 0) - var(--select-a, 0)) * 100%);
  background: color-mix(in srgb, var(--waveform-select, var(--accent)) 30%, transparent);
  box-shadow: inset 0 0 0 1px var(--waveform-select, var(--accent));
}
.waveform-ruler {
  position: relative;
  block-size: 1.3em;
  overflow: hidden;
  border-block-start: 1px solid var(--waveform-ruler-rule, var(--hairline));
  color: var(--waveform-ruler-fg, var(--muted));
  font: 0.7rem/1 var(--font-mono);
  font-variant-numeric: tabular-nums;
}
.waveform-ruler > span { position: absolute; inset-block-start: 0; inset-inline-start: calc(var(--at, 0) * 100%); padding-block-start: 0.35em; translate: calc(var(--at, 0) * -100%) 0; white-space: nowrap; }
.waveform-ruler > span::before { content: ""; position: absolute; inset-block-start: 0; inset-inline-start: calc(var(--at, 0) * 100%); block-size: 0.25em; border-inline-start: 1px solid currentColor; }
/* The overview: the whole clip, small, with the window as a draggable region. */
/* No seek input underneath, so the bars show the unplayed colour. */
.waveform-overview { --waveform-height: var(--waveform-overview-height, max(1.6rem, 24px)); background: var(--waveform-unplayed, var(--muted)); }
.waveform-overview .waveform-window { --waveform-region-alpha: 30%; --_hw: var(--waveform-handle-width, 0.4rem); }
/* Hover time: a chip at the pointer, text from the app. */
.waveform[data-hover-time]::after {
  content: attr(data-hover-time);
  position: absolute;
  z-index: calc(var(--z-raised) + 2);
  inset-block-end: 0.15rem;
  inset-inline-start: clamp(0px, calc(var(--hover, 0) * 100% - 1.5rem), calc(100% - 3rem));
  padding: 0 0.3rem;
  border-radius: var(--radius-sm, 2px);
  background: var(--text);
  color: var(--bg);
  font: 0.7rem/1.4 var(--font-mono);
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}

/* Forced colours drop the painted track: fall back to the native slider. */
@media (forced-colors: active) {
  :is(.scrubber, .volume, .waveform) > input[type="range"] { appearance: auto; overflow: visible; }
  .waveform-region::before { background: none; box-shadow: inset 0 0 0 2px Highlight; }
  .waveform-region > input[type="range"]::-webkit-slider-thumb { forced-color-adjust: none; background: Highlight; }
  .waveform-region > input[type="range"]::-moz-range-thumb { forced-color-adjust: none; background: Highlight; }
  .waveform-marker::before { border-color: CanvasText; }
}

/* ---- Now-playing bar ---------------------------------------------------------
 * <footer class="now-playing" aria-label="Now playing">
 *   <img src="art.jpg" alt="">
 *   <span class="track-main"><span class="track-title">…</span><span class="track-artist">…</span></span>
 *   <label class="btn btn-ghost btn-icon media-toggle is-like">…</label>
 *   <div class="transport" role="group" aria-label="Playback">shuffle, previous, .btn-go play, next, repeat</div>
 *   <div class="scrubber">…</div>
 *   <div class="now-playing-extras">queue, lyrics, <div class="volume">…</div></div>
 * </footer>
 * Desktop: art and title | transport over the scrubber | extras. Tablet: the
 * scrubber takes its own row and the extras (volume, queue…) hide. Mobile and .is-mini: art, title, like and play/next only. As a
 * direct child of .app it takes the shell's status slot and sticks to the
 * bottom of the screen, clear of the home indicator. */
.now-playing {
  --_areas: "art meta like transport extras" "art meta like progress extras";
  --_cols: auto minmax(0, 1fr) auto minmax(0, 2fr) minmax(0, 1fr);
  display: grid;
  grid-template-areas: var(--_areas);
  grid-template-columns: var(--_cols);
  align-items: center;
  gap: 0 var(--now-playing-gap, var(--space-s));
  min-inline-size: 0;
  padding: var(--now-playing-pad-block, var(--space-xs)) var(--now-playing-pad-inline, var(--space-m));
  background: var(--now-playing-bg, var(--surface));
  color: var(--now-playing-fg, var(--text));
  border-block-start: var(--now-playing-rule-width, 1px) solid var(--now-playing-rule, var(--border));
  box-shadow: var(--now-playing-shadow, none);
}
.now-playing > img {
  grid-area: art;
  inline-size: var(--now-playing-art-size, 3.5rem);
  block-size: auto;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: var(--now-playing-art-radius, var(--radius));
  background: var(--surface-2);
}
.now-playing > .track-main { grid-area: meta; }
.now-playing > .media-toggle { grid-area: like; }
.now-playing > .transport {
  grid-area: transport;
  justify-content: center;
  gap: var(--space-2xs);
  --transport-bg: transparent;
  --transport-border: transparent;
  --transport-edge: transparent;
  --transport-shadow: none;
  --transport-padding: 0;
}
.now-playing > .transport > :not(.btn-go, .btn-go + *) { display: var(--_np-secondary, inline-flex); }
.now-playing > .scrubber { grid-area: progress; }
.now-playing-extras {
  grid-area: extras;
  display: var(--_np-extras, flex);
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-2xs);
  min-inline-size: 0;
}
.app > .now-playing {
  grid-area: status;
  position: sticky;
  inset-block-end: 0;
  z-index: var(--z-sticky);
  padding-block-end: calc(var(--now-playing-pad-block, var(--space-xs)) + env(safe-area-inset-bottom, 0px));
  padding-inline: max(var(--now-playing-pad-inline, var(--space-m)), env(safe-area-inset-left, 0px)) max(var(--now-playing-pad-inline, var(--space-m)), env(safe-area-inset-right, 0px));
}
@media (max-width: 900px) {
  /* Tablets have hardware volume keys: the extras go, so the full
     transport and a readable title fit down to 481px. */
  .now-playing {
    --_areas: "art meta like transport" "progress progress progress progress";
    --_cols: auto minmax(0, 1fr) auto auto;
    --_np-extras: none;
  }
}
@media (max-width: 480px) {
  .now-playing {
    --_areas: "art meta like transport" "progress progress progress progress";
    --_cols: auto minmax(0, 1fr) auto auto;
    --_np-secondary: none;
    --_np-extras: none;
  }
}
/* Mini player: the mobile layout at any width, as a card. */
.now-playing.is-mini {
  --_areas: "art meta like transport" "progress progress progress progress";
  --_cols: auto minmax(0, 1fr) auto auto;
  --_np-secondary: none;
  --_np-extras: none;
  max-inline-size: var(--now-playing-mini-max, 24rem);
  border: var(--now-playing-rule-width, 1px) solid var(--now-playing-rule, var(--border));
  border-radius: var(--now-playing-mini-radius, var(--radius));
  box-shadow: var(--now-playing-mini-shadow, var(--panel-shadow, none));
  --now-playing-art-size: 2.75rem;
}

/* ---- Album grid ------------------------------------------------------------
 * <ul class="album-grid"><li><article class="card has-media">
 *   <div class="card-media"><img src="…" alt=""><button class="btn btn-go btn-icon card-play" aria-label="Play Harbour Lights">…</button></div>
 *   <div class="card-body"><a class="stretched-link" href="…">Harbour Lights</a><span class="text-muted">Lumen Coast · 2024</span></div>
 * </article></li></ul>
 * Square art (surfaces.css's image card). .card-play sits on the art, above
 * the stretched link; it shows on hover or focus, always on touch and on a
 * playing album (.is-playing). */
.album-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, var(--album-min, 9.5rem)), 1fr));
  gap: var(--album-gap, var(--space-m));
  margin: 0;
  padding: 0;
  list-style: none;
}
.album-grid > li { display: grid; min-inline-size: 0; }
.album-grid .card.has-media { --card-media-ratio: 1; }
.card-media:has(> .card-play) { position: relative; }
.card-play {
  position: absolute;
  inset-block-end: var(--space-xs);
  inset-inline-end: var(--space-xs);
  z-index: var(--z-raised);
  --btn-bg: var(--card-play-bg, var(--accent));
  --btn-fg: var(--card-play-fg, var(--on-accent));
  --btn-bg-hover: var(--card-play-bg, var(--accent));
  --btn-shadow: var(--card-play-shadow, 0 0.25rem 0.75rem rgba(0, 0, 0, 0.4));
}
@media (hover: hover) {
  .card:not(:hover, :focus-within, .is-playing) .card-play { opacity: 0; }
}

/* ---- Lyrics ----------------------------------------------------------------
 * <ol class="lyrics"><li>…</li><li aria-current="true">…</li><li></li>…</ol>
 * The current line (aria-current) is full strength with a marker; an empty
 * <li> is an instrumental gap. The app scrolls the current line into view;
 * scroll-margin keeps it off the edge. */
.lyrics {
  display: grid;
  gap: var(--lyrics-gap, var(--space-2xs));
  margin: 0;
  padding: 0;
  list-style: none;
  color: var(--lyrics-fg, var(--muted));
  font-size: var(--lyrics-size, 1.375rem);
  font-weight: var(--lyrics-weight, 700);
  line-height: 1.3;
}
.lyrics > li {
  padding: var(--space-2xs) var(--space-s);
  border-radius: var(--radius);
  scroll-margin-block: var(--lyrics-scroll-margin, 35%);
  overflow-wrap: anywhere;
}
.lyrics > li:empty::before { content: var(--lyrics-gap-glyph, "\266A  \266A  \266A"); white-space: pre; }
.lyrics > [aria-current="true"] {
  color: var(--lyrics-current-fg, var(--text));
  box-shadow: var(--lyrics-current-marker, inset 0.25rem 0 0 var(--accent));
}
@media (prefers-reduced-motion: no-preference) {
  .lyrics > li { transition: color 0.3s ease, box-shadow 0.3s ease; }
}

  :root[data-motion="always"] .lyrics > li { transition: color 0.3s ease, box-shadow 0.3s ease; }

/* Physical media transports. The SVG drawings are presentational; apps own
   playback/recording and set one state class on .media-deck — or, with no
   JavaScript at all, check one radio in its .transport-keys
   (value="playing|recording|paused|stopped"); :has() reads it.
     .media-deck          deck shell + SVG drawing hooks
     [data-lamp]          deck lamps (SVG circle or HTML .deck-lamp), .deck-hud band
     [data-when]          text/annunciators shown (or lit) in one state
     .transport-keys      Rec / Stop / Play / Pause hardware keys
     .seg7                seven-segment digits with ghost segments
   See docs/components/media-devices.md. */
.media-decks { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,30rem),1fr)); gap:var(--space-m,1rem); }
.media-deck {
  --deck-highlight:var(--accent);
  color:var(--text);
  background:var(--surface);
  border:1px solid var(--deck-trim,var(--border));
  border-radius:var(--radius);
  padding:var(--space-m,1rem);
  box-shadow:var(--panel-shadow,none);
}
.media-deck h2 { margin:0 0 .75rem; font-size:1rem; }
.media-deck-visual { display:block; width:100%; height:auto; }
.media-deck .plate { fill:var(--deck-face,var(--surface-2)); }
.media-deck .plate-bezel,.media-deck .deck-frame { fill:none; stroke:var(--deck-trim,var(--border)); stroke-width:2; }
.media-deck .deck-grid,.media-deck .head-edge,.media-deck .head-grid { fill:none; stroke:var(--deck-trim,var(--border)); stroke-width:1; opacity:.45; }
.media-deck .deck-screw { fill:var(--deck-well,var(--surface)); stroke:var(--deck-trim,var(--border)); }
.media-deck .device-guide { fill:var(--deck-well,var(--surface)); stroke:var(--deck-highlight); stroke-width:1.5; }
.media-deck .deck-text { fill:var(--muted); font:700 12px var(--font-mono,monospace); letter-spacing:.16em; }
.media-deck .reel-disc,.media-deck .spool-disc { fill:var(--deck-face,var(--surface-2)); stroke:var(--deck-trim,var(--border)); stroke-width:2; }
.media-deck .reel-ring { fill:none; stroke:var(--deck-trim,var(--border)); stroke-width:1.5; }
.media-deck .reel-wind { fill:none; stroke:var(--deck-highlight); stroke-width:2; opacity:.38; }
.media-deck .reel-hub { fill:var(--deck-hub,var(--surface)); stroke:var(--deck-highlight); stroke-width:1.5; }
.media-deck .reel-center { fill:var(--deck-well,var(--surface)); }
.media-deck .reel-spoke { fill:var(--deck-spoke,var(--surface-2)); stroke:var(--deck-highlight); stroke-width:1.2; }
.media-deck .tape-shadow { fill:none; stroke:var(--deck-shadow,var(--bg)); stroke-width:6; stroke-linecap:round; stroke-linejoin:round; }
.media-deck .tape { fill:none; stroke:var(--deck-tape,var(--deck-trim,var(--border))); stroke-width:3; stroke-linecap:round; stroke-linejoin:round; }
.media-deck .head-plate { fill:var(--deck-face,var(--surface-2)); stroke:var(--border); stroke-width:1.5; }
.media-deck .head-window,.media-deck .lcd { fill:var(--deck-well,var(--bg)); stroke:var(--deck-trim,var(--border)); stroke-width:1.5; }
.media-deck .head-grid { fill:none; stroke:var(--deck-trim,var(--border)); stroke-width:1; opacity:.35; }
.media-deck .head-gap { fill:none; stroke:var(--deck-highlight); stroke-width:3; stroke-linecap:round; }
.media-deck .device-counter { fill:var(--accent); font:italic 700 24px var(--font-mono,monospace); }
/* A <foreignObject class="device-counter-slot"> in the head window holds an HTML .seg7. */
.media-deck .device-counter-slot > * { display:flex; align-items:center; justify-content:center; block-size:100%; font-size:var(--deck-counter-size,30px); line-height:1; }
.media-deck .tonearm { fill:none; stroke:var(--deck-highlight); stroke-width:5; stroke-linecap:round; }
.media-deck .platter { fill:var(--deck-well,var(--bg)); stroke:var(--deck-trim,var(--border)); stroke-width:2; }
.media-deck .groove { fill:none; stroke:var(--deck-spoke,var(--border)); stroke-width:1; opacity:.8; }
.media-deck .record-label { fill:var(--deck-highlight); }
.media-deck .disc { fill:var(--deck-face,var(--surface-2)); stroke:var(--deck-trim,var(--border)); stroke-width:2; }
.media-deck .disc-groove { fill:none; stroke:var(--deck-trim,var(--border)); stroke-width:1; opacity:.75; }
.media-deck .disc-hole { fill:var(--deck-well,var(--bg)); }
.media-deck .device-lamp { fill:var(--muted); }
.media-deck .device-controls { display:flex; flex-wrap:wrap; align-items:center; gap:.5rem; margin-top:.75rem; }
.media-deck .device-controls .status { margin-left:auto; }
.media-deck.device-vinyl .tonearm { transform-box:view-box; transform-origin:430px 74px; }

/* --- State. Each state is the class OR the matching checked radio. -------- */
@media (prefers-reduced-motion:no-preference) {
  html:not([data-motion="reduced"]) .media-deck:is(.is-playing,.is-recording,:has(.transport-keys :is([value="playing"],[value="recording"]):checked)) :is(.reel-spin,.spool-spin,.vinyl-disc,.md-disc) { animation:media-deck-spin 2.2s linear infinite; }
  html:not([data-motion="reduced"]) .media-deck:is(.is-playing,.is-recording,:has(.transport-keys :is([value="playing"],[value="recording"]):checked)) .reel-supply .reel-spin { animation-direction:reverse; }
  html:not([data-motion="reduced"]) .media-deck:is(.is-playing,.is-recording,:has(.transport-keys :is([value="playing"],[value="recording"]):checked)) .tonearm { animation:media-tonearm 2s ease-in-out infinite alternate; }
  html:not([data-motion="reduced"]) .media-deck:is(.is-playing,:has(.transport-keys [value="playing"]:checked)) .tape.is-moving { stroke:var(--deck-highlight); stroke-dasharray:22 14; animation:media-tape-flow .55s linear infinite; }
  html:not([data-motion="reduced"]) .media-deck:is(.is-recording,:has(.transport-keys [value="recording"]:checked)) .tape.is-moving { stroke:var(--danger); stroke-dasharray:22 14; animation:media-tape-flow .55s linear infinite; }
}

  html[data-motion="always"]:not([data-motion="reduced"]) .media-deck:is(.is-playing,.is-recording,:has(.transport-keys :is([value="playing"],[value="recording"]):checked)) :is(.reel-spin,.spool-spin,.vinyl-disc,.md-disc) { animation:media-deck-spin 2.2s linear infinite; }
  html[data-motion="always"]:not([data-motion="reduced"]) .media-deck:is(.is-playing,.is-recording,:has(.transport-keys :is([value="playing"],[value="recording"]):checked)) .reel-supply .reel-spin { animation-direction:reverse; }
  html[data-motion="always"]:not([data-motion="reduced"]) .media-deck:is(.is-playing,.is-recording,:has(.transport-keys :is([value="playing"],[value="recording"]):checked)) .tonearm { animation:media-tonearm 2s ease-in-out infinite alternate; }
  html[data-motion="always"]:not([data-motion="reduced"]) .media-deck:is(.is-playing,:has(.transport-keys [value="playing"]:checked)) .tape.is-moving { stroke:var(--deck-highlight); stroke-dasharray:22 14; animation:media-tape-flow .55s linear infinite; }
  html[data-motion="always"]:not([data-motion="reduced"]) .media-deck:is(.is-recording,:has(.transport-keys [value="recording"]:checked)) .tape.is-moving { stroke:var(--danger); stroke-dasharray:22 14; animation:media-tape-flow .55s linear infinite; }

.media-deck:is(.is-recording,:has(.transport-keys [value="recording"]:checked)) .device-counter { fill:var(--danger); }
.media-deck:is(.is-recording,:has(.transport-keys [value="recording"]:checked)) .device-counter-slot .seg7 { --seg7-lit:var(--deck-counter-rec,var(--danger)); }
.media-deck:is(.is-recording,:has(.transport-keys [value="recording"]:checked)) .head-gap { stroke:var(--danger); }
.media-deck:is(.is-recording,:has(.transport-keys [value="recording"]:checked)) .device-lamp { fill:var(--danger); }
.media-deck:is(.is-playing,:has(.transport-keys [value="playing"]:checked)) .device-lamp { fill:var(--success); }
.media-deck :is(.reel-spin,.spool-spin,.vinyl-disc,.md-disc,.tonearm) { transform-box:fill-box; transform-origin:center; }
.media-deck.is-paused :is(.reel-spin,.spool-spin,.vinyl-disc,.md-disc,.tonearm) { animation:none !important; }
@keyframes media-deck-spin { to { transform:rotate(360deg); } }
@keyframes media-tape-flow { to { stroke-dashoffset:-36; } }
@keyframes media-tonearm { to { transform:rotate(4deg); } }

@media (prefers-reduced-motion:reduce) {
  :root:not([data-motion="always"]) .media-deck * { animation:none !important; transition:none !important; }
}

/* [data-when="playing recording"] — shown only in those states (stopped =
   none of the others). An .is-annunciator stays in place and dims instead.
   Don't mix a state class with a checked state radio on one deck. */
.media-deck:is(.is-playing,:has(.transport-keys [value="playing"]:checked)) [data-when]:not([data-when~="playing"]):not(.is-annunciator),
.media-deck:is(.is-recording,:has(.transport-keys [value="recording"]:checked)) [data-when]:not([data-when~="recording"]):not(.is-annunciator),
.media-deck:is(.is-paused,:has(.transport-keys [value="paused"]:checked)) [data-when]:not([data-when~="paused"]):not(.is-annunciator),
.media-deck:not(.is-playing,.is-recording,.is-paused,:has(.transport-keys :is([value="playing"],[value="recording"],[value="paused"]):checked)) [data-when]:not([data-when~="stopped"]):not(.is-annunciator) { display:none; }
.media-deck:is(.is-playing,:has(.transport-keys [value="playing"]:checked)) [data-when]:not([data-when~="playing"]).is-annunciator,
.media-deck:is(.is-recording,:has(.transport-keys [value="recording"]:checked)) [data-when]:not([data-when~="recording"]).is-annunciator,
.media-deck:is(.is-paused,:has(.transport-keys [value="paused"]:checked)) [data-when]:not([data-when~="paused"]).is-annunciator,
.media-deck:not(.is-playing,.is-recording,.is-paused,:has(.transport-keys :is([value="playing"],[value="recording"],[value="paused"]):checked)) [data-when]:not([data-when~="stopped"]).is-annunciator { opacity:var(--deck-annunciator-off,.18); text-shadow:none; }

/* --- Lamps: <circle data-lamp="sys"> in the SVG, or
   <span class="deck-lamp" data-lamp="sys">SYS</span> in a .deck-hud band.
   sys: green while moving, red while recording, amber paused.
   rec: red while recording, drawn larger and glowing.
   Any other data-lamp: app-driven, .is-on / .is-warn / .is-error. */
.media-deck { --deck-lamp-r:4px; }
[data-lamp] { --_lamp:var(--deck-lamp-off,color-mix(in srgb,var(--muted) 40%,transparent)); --_lamp-glow:0px; }
[data-lamp].is-on { --_lamp:var(--deck-lamp-on,var(--success)); --_lamp-glow:var(--deck-lamp-glow,4px); }
[data-lamp].is-warn { --_lamp:var(--warning); --_lamp-glow:var(--deck-lamp-glow,4px); }
[data-lamp].is-error { --_lamp:var(--danger); --_lamp-glow:var(--deck-lamp-glow,4px); }
.media-deck:is(.is-playing,:has(.transport-keys [value="playing"]:checked)) [data-lamp="sys"] { --_lamp:var(--deck-lamp-on,var(--success)); --_lamp-glow:var(--deck-lamp-glow,4px); }
.media-deck:is(.is-paused,:has(.transport-keys [value="paused"]:checked)) [data-lamp="sys"] { --_lamp:var(--warning); --_lamp-glow:var(--deck-lamp-glow,4px); }
.media-deck:is(.is-recording,:has(.transport-keys [value="recording"]:checked)) :is([data-lamp="sys"],[data-lamp="rec"]) { --_lamp:var(--danger); --_lamp-glow:var(--deck-lamp-glow,4px); }
.media-deck:is(.is-recording,:has(.transport-keys [value="recording"]:checked)) [data-lamp="rec"] { --_lamp-glow:var(--deck-lamp-rec-glow,9px); }
circle[data-lamp] { r:var(--deck-lamp-r,4px); fill:var(--_lamp); filter:drop-shadow(0 0 var(--_lamp-glow) var(--_lamp)); }
circle[data-lamp="rec"] { r:var(--deck-lamp-rec-r,calc(var(--deck-lamp-r,4px) * 1.6)); }
.deck-hud { display:flex; flex-wrap:wrap; align-items:center; gap:.4rem 1.1rem; margin-block-end:.6rem; padding:.4rem .7rem; background:var(--deck-hud-bg,var(--surface-2)); border:1px solid var(--deck-trim,var(--border)); border-radius:var(--radius); font:700 .72rem/1.2 var(--font-mono,monospace); letter-spacing:.14em; text-transform:uppercase; color:var(--deck-hud-fg,var(--text)); }
.deck-hud > .status { margin-inline-start:auto; letter-spacing:normal; text-transform:none; font-family:var(--font); }
.deck-lamp { display:inline-flex; align-items:center; gap:.45em; }
.deck-lamp::before { content:""; flex:none; inline-size:calc(var(--deck-lamp-r,4px) * 2); block-size:calc(var(--deck-lamp-r,4px) * 2); border-radius:50%; background:var(--_lamp); box-shadow:0 0 var(--_lamp-glow) var(--_lamp); }
.deck-lamp[data-lamp="rec"]::before { inline-size:calc(var(--deck-lamp-rec-r,calc(var(--deck-lamp-r,4px) * 1.6)) * 2); block-size:calc(var(--deck-lamp-rec-r,calc(var(--deck-lamp-r,4px) * 1.6)) * 2); }

/* --- .transport-keys — hardware-lamp transport keys on core .key ---------
     <div class="transport-keys" role="radiogroup" aria-label="Transport">
       <label class="key key-rec"><input type="radio" name="t" value="recording"><svg class="icon">…player-record</svg>Rec</label>
       …key-stop (stopped) · key-play (playing) · key-pause (paused)
     </div>
   or plain <button class="key key-play"> with the state class
   (.is-recording/.is-rec, .is-playing/.is-play, .is-paused/.is-pause) on
   the key set or any ancestor (the .media-deck). Unlit at rest; Rec lit red
   while recording; Play lit green while playing and flashing ~2 Hz while
   paused — steady with a dashed edge under reduced motion. */
.transport-keys { display:flex; flex-wrap:wrap; align-items:center; gap:var(--transport-keys-gap,.5rem); }
.transport-keys .key {
  --_tk:var(--muted);
  gap:.45em;
  height:auto;
  min-block-size:max(var(--transport-key-size,2.6rem),var(--tap-min));
  min-inline-size:max(var(--transport-key-width,4.25rem),var(--tap-min));
  padding:0 .8em;
  font-size:var(--transport-key-font-size,.72rem);
  border-width:var(--transport-key-border-width,2px);
}
.transport-keys .key > .icon { --icon-size:1.25em; color:var(--_tk-icon,currentColor); }
/* Lit: the lamp colour is on the edge, glow and icon; the label stays in
   --text so it reads on every theme's tint (WCAG text contrast). */
.transport-keys .key-rec { --_tk:var(--transport-key-rec,var(--danger)); }
.transport-keys .key-play { --_tk:var(--transport-key-play,var(--success)); }
.transport-keys .key-pause { --_tk:var(--transport-key-pause,var(--warning)); }
/* A checked radio is not "lit": keys light from the transport state. The
   pressed key sits down a little so the selection still reads. */
.transport-keys .key,
.transport-keys .key:has(input:checked) { background:var(--key-bg,var(--surface-2)); color:var(--key-fg,var(--muted)); border-color:var(--key-border,var(--border)); box-shadow:var(--key-shadow,inset 0 -2px 0 rgba(0,0,0,.25)); }
.transport-keys .key:has(input:checked),
.transport-keys .key[aria-pressed="true"] { box-shadow:inset 0 2px 4px rgba(0,0,0,.35); color:var(--text); }
:is(:is(.is-recording,.is-rec) .transport-keys,.transport-keys:is(.is-recording,.is-rec,:has([value="recording"]:checked))) .key-rec,
:is(:is(.is-playing,.is-play,.is-paused,.is-pause) .transport-keys,.transport-keys:is(.is-playing,.is-play,.is-paused,.is-pause,:has(:is([value="playing"],[value="paused"]):checked))) .key-play,
:is(:is(.is-paused,.is-pause) .transport-keys,.transport-keys:is(.is-paused,.is-pause,:has([value="paused"]:checked))) .key-pause {
  --_tk-icon:var(--_tk);
  border-color:var(--_tk);
  color:var(--transport-key-lit-fg,var(--text));
  background:color-mix(in srgb,var(--_tk) var(--transport-key-tint,14%),var(--key-bg,var(--surface-2)));
  box-shadow:0 0 var(--transport-key-glow,.8em) color-mix(in srgb,var(--_tk) 55%,transparent);
}
@media (prefers-reduced-motion:no-preference) {
  html:not([data-motion="reduced"],[data-motion="none"]) :is(:is(.is-paused,.is-pause) .transport-keys,.transport-keys:is(.is-paused,.is-pause,:has([value="paused"]:checked))) .key-play { animation:transport-key-flash var(--transport-key-flash,.5s) steps(1,end) infinite; }
}

  html[data-motion="always"]:not([data-motion="reduced"],[data-motion="none"]) :is(:is(.is-paused,.is-pause) .transport-keys,.transport-keys:is(.is-paused,.is-pause,:has([value="paused"]:checked))) .key-play { animation:transport-key-flash var(--transport-key-flash,.5s) steps(1,end) infinite; }

@keyframes transport-key-flash {
  50% { --_tk-icon:currentColor; background:var(--key-bg,var(--surface-2)); color:var(--key-fg,var(--muted)); border-color:var(--key-border,var(--border)); box-shadow:none; }
}
@media (prefers-reduced-motion:reduce) {
  :root:not([data-motion="always"]) :is(:is(.is-paused,.is-pause) .transport-keys,.transport-keys:is(.is-paused,.is-pause,:has([value="paused"]:checked))) .key-play { border-style:dashed; }
}
html:is([data-motion="reduced"],[data-motion="none"]) :is(:is(.is-paused,.is-pause) .transport-keys,.transport-keys:is(.is-paused,.is-pause,:has([value="paused"]:checked))) .key-play { border-style:dashed; }
@media (forced-colors:active) {
  .transport-keys .key:has(input:checked) { border-style:double; border-width:4px; }
}

/* --- .seg7 — seven-segment digits -----------------------------------------
     <span class="seg7" role="img" aria-label="00:12:34">
       <i data-d="0"></i><i data-d="0"></i><b></b><i data-d="1"></i>…
     </span>
   One <i> per digit; data-d is 0–9, "-" or "" (blank). <b> is a colon.
   Each segment is a background layer coloured lit or ghost; a mask cuts the
   seven bevelled shapes. The app updates a digit by changing data-d (and
   the aria-label). */
.seg7 {
  --_on:var(--seg7-lit,var(--accent));
  --_off:var(--seg7-unlit,color-mix(in srgb,var(--_on) var(--seg7-ghost,13%),transparent));
  display:inline-flex;
  align-items:stretch;
  gap:var(--seg7-gap,.14em);
  block-size:1em;
  vertical-align:middle;
  transform:skewX(var(--seg7-skew,-7deg));
  filter:drop-shadow(0 0 var(--seg7-glow,.04em) var(--_on)) brightness(var(--seg7-brightness,1));
  forced-color-adjust:none;
}
.seg7 > i {
  --_a:var(--_off); --_b:var(--_off); --_c:var(--_off); --_d:var(--_off); --_e:var(--_off); --_f:var(--_off); --_g:var(--_off);
  flex:none;
  inline-size:.6em;
  font-style:normal;
  background:
    linear-gradient(var(--_a),var(--_a)) 50% 0 / 66.667% 10%,
    linear-gradient(var(--_b),var(--_b)) 100% 0 / 16.667% 50%,
    linear-gradient(var(--_c),var(--_c)) 100% 100% / 16.667% 50%,
    linear-gradient(var(--_d),var(--_d)) 50% 100% / 66.667% 10%,
    linear-gradient(var(--_e),var(--_e)) 0 100% / 16.667% 50%,
    linear-gradient(var(--_f),var(--_f)) 0 0 / 16.667% 50%,
    linear-gradient(var(--_g),var(--_g)) 50% 50% / 66.667% 10%;
  background-repeat:no-repeat;
  --_mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 20' preserveAspectRatio='none'%3E%3Cpath d='M2.3 1 3.3 0h5.4l1 1-1 1H3.3zM2.3 10l1-1h5.4l1 1-1 1H3.3zM2.3 19l1-1h5.4l1 1-1 1H3.3zM1 1.3l1 1v5.4l-1 1-1-1V2.3zM11 1.3l1 1v5.4l-1 1-1-1V2.3zM1 11.3l1 1v5.4l-1 1-1-1v-5.4zM11 11.3l1 1v5.4l-1 1-1-1v-5.4z'/%3E%3C/svg%3E") 0 0 / 100% 100% no-repeat;
  -webkit-mask:var(--_mask);
  mask:var(--_mask);
}
.seg7 > i:is([data-d="0"],[data-d="2"],[data-d="3"],[data-d="5"],[data-d="6"],[data-d="7"],[data-d="8"],[data-d="9"]) { --_a:var(--_on); }
.seg7 > i:is([data-d="0"],[data-d="1"],[data-d="2"],[data-d="3"],[data-d="4"],[data-d="7"],[data-d="8"],[data-d="9"]) { --_b:var(--_on); }
.seg7 > i:is([data-d="0"],[data-d="1"],[data-d="3"],[data-d="4"],[data-d="5"],[data-d="6"],[data-d="7"],[data-d="8"],[data-d="9"]) { --_c:var(--_on); }
.seg7 > i:is([data-d="0"],[data-d="2"],[data-d="3"],[data-d="5"],[data-d="6"],[data-d="8"],[data-d="9"]) { --_d:var(--_on); }
.seg7 > i:is([data-d="0"],[data-d="2"],[data-d="6"],[data-d="8"]) { --_e:var(--_on); }
.seg7 > i:is([data-d="0"],[data-d="4"],[data-d="5"],[data-d="6"],[data-d="8"],[data-d="9"]) { --_f:var(--_on); }
.seg7 > i:is([data-d="2"],[data-d="3"],[data-d="4"],[data-d="5"],[data-d="6"],[data-d="8"],[data-d="9"],[data-d="-"]) { --_g:var(--_on); }
.seg7 > b {
  --_c:var(--_on);
  flex:none;
  inline-size:.16em;
  background:
    radial-gradient(circle closest-side,var(--_c) 92%,transparent) 50% 28% / .14em .14em no-repeat,
    radial-gradient(circle closest-side,var(--_c) 92%,transparent) 50% 72% / .14em .14em no-repeat;
}
.seg7 > b.is-off { --_c:var(--_off); }
/* Behind VFD glass (.vfd, .is-vfd, or a theme with --meter-style: vfd) the
   digits take the phosphor ink and bloom. */
:is(.vfd,.is-vfd) .seg7,
.seg7.is-vfd {
  --seg7-lit:var(--vfd-ink);
  --seg7-ghost:var(--vfd-ghost,10%);
  --seg7-glow:var(--vfd-glow,.1em);
  --seg7-brightness:var(--vfd-brightness,1.1);
}
@container style(--meter-style: vfd) {
  .seg7:not(.is-themed) {
    --seg7-lit:var(--vfd-ink);
    --seg7-ghost:var(--vfd-ghost,10%);
    --seg7-glow:var(--vfd-glow,.1em);
    --seg7-brightness:var(--vfd-brightness,1.1);
  }
}
/* --- Metering (v5.2): broadcast and studio level meters --------------------
 * .vu         analogue needle meter (VU, .is-ppm), backlit face, peak LED
 * .ledbar     segmented LED bargraph (vertical / .is-horizontal), unlit
 *             segments ghosted in their zone colour, peak-hold, overs cap
 * .ledarc     the arc-shaped segmented loudness meter
 * .is-vfd     vacuum-fluorescent look for any of them, .readout too;
 *             .vfd is the smoked-glass window that holds them
 * .meter-panel  Peak / Overs / Meter-mode radios that redraw the meters
 * .eq-graph   parametric EQ display: log grid, .spectrogram, .rta bars,
 *             the curve and draggable-looking .eq-node band handles
 * Values arrive as inline custom properties: --value and --peak (0–1).
 * Each meter keeps a typed copy (--metering-v) so value changes glide with
 * VU ballistics; nothing here needs JavaScript. See docs/components/metering.md. */

@property --metering-v {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}
@property --rta-l {
  syntax: "<number>";
  inherits: false;
  initial-value: 1;
}

:root {
  --vfd-ink: #3ff5d0;
  --vfd-amber: #ffb547;
  --vfd-blue: #62c8ff;
  --vfd-hot: #ff6a4d;
  --vfd-glass: #050b0a;
}

/* Shared value pipeline. --_target is what the meter shows (the panel's
   mode radios retarget it); --metering-v is the animated, clamped copy. */
:is(.vu-needle, .ledbar, .ledarc) {
  --_target: var(--value, 0);
  --metering-v: clamp(0, var(--_target), 1);
}
@media (prefers-reduced-motion: no-preference) {
  html:not([data-motion="reduced"]) :is(.vu-needle, .ledbar, .ledarc) {
    transition: --metering-v var(--metering-speed, 0.3s) cubic-bezier(0.3, 0.6, 0.4, 1);
  }
  /* Optional demo motion: levels breathe around their --value. */
  html:not([data-motion="reduced"]) .is-demo-live :is(.vu-needle, .ledbar, .ledarc) {
    animation: metering-live var(--metering-live, 1.7s) ease-in-out infinite alternate;
  }
  html:not([data-motion="reduced"]) .is-demo-live :is(.vu-needle, .ledbar, .ledarc):nth-of-type(2n) { --metering-live: 1.3s; animation-delay: -0.4s; }
  html:not([data-motion="reduced"]) .is-demo-live :is(.vu-needle, .ledbar, .ledarc):nth-of-type(3n) { --metering-live: 2.1s; animation-delay: -1.1s; }
  html:not([data-motion="reduced"]) .is-demo-live .rta > li { animation: rta-live calc(0.9s * var(--rta-live-speed, 1)) ease-in-out infinite alternate; }
  html:not([data-motion="reduced"]) .is-demo-live .rta > li:nth-child(2n) { animation-duration: calc(0.7s * var(--rta-live-speed, 1)); animation-delay: -0.3s; }
  html:not([data-motion="reduced"]) .is-demo-live .rta > li:nth-child(3n) { animation-duration: calc(1.15s * var(--rta-live-speed, 1)); animation-delay: -0.6s; }
}

  html[data-motion="always"]:not([data-motion="reduced"]) :is(.vu-needle, .ledbar, .ledarc) {
    transition: --metering-v var(--metering-speed, 0.3s) cubic-bezier(0.3, 0.6, 0.4, 1);
  }
  /* Optional demo motion: levels breathe around their --value. */
  html[data-motion="always"]:not([data-motion="reduced"]) .is-demo-live :is(.vu-needle, .ledbar, .ledarc) {
    animation: metering-live var(--metering-live, 1.7s) ease-in-out infinite alternate;
  }
  html[data-motion="always"]:not([data-motion="reduced"]) .is-demo-live :is(.vu-needle, .ledbar, .ledarc):nth-of-type(2n) { --metering-live: 1.3s; animation-delay: -0.4s; }
  html[data-motion="always"]:not([data-motion="reduced"]) .is-demo-live :is(.vu-needle, .ledbar, .ledarc):nth-of-type(3n) { --metering-live: 2.1s; animation-delay: -1.1s; }
  html[data-motion="always"]:not([data-motion="reduced"]) .is-demo-live .rta > li { animation: rta-live calc(0.9s * var(--rta-live-speed, 1)) ease-in-out infinite alternate; }
  html[data-motion="always"]:not([data-motion="reduced"]) .is-demo-live .rta > li:nth-child(2n) { animation-duration: calc(0.7s * var(--rta-live-speed, 1)); animation-delay: -0.3s; }
  html[data-motion="always"]:not([data-motion="reduced"]) .is-demo-live .rta > li:nth-child(3n) { animation-duration: calc(1.15s * var(--rta-live-speed, 1)); animation-delay: -0.6s; }

@keyframes metering-live {
  0% { --metering-v: clamp(0, var(--_target) * 0.8, 1); }
  30% { --metering-v: clamp(0, var(--_target) * 1.05, 1); }
  55% { --metering-v: clamp(0, var(--_target) * 0.9, 1); }
  100% { --metering-v: clamp(0, var(--_target), 1); }
}
@keyframes rta-live { from { --rta-l: 0.82; } to { --rta-l: 1.06; } }

/* --- .vu — analogue needle meter ----------------------------------------
 *   <div class="vu" role="meter" aria-label="Left" aria-valuenow="-3"
 *        aria-valuemin="-20" aria-valuemax="3" aria-valuetext="−3 VU">
 *     <ol class="vu-scale" aria-hidden="true"><li style="--at:0">20</li>…</ol>
 *     <span class="vu-legend">VU</span>
 *     <span class="vu-needle" style="--value:.46"></span>
 *     <span class="vu-peak">Peak</span>
 *   </div>
 * Geometry is in cqi of the face: the needle pivots just below the window
 * (--vu-pivot-y), the printed arc has radius --vu-radius. The face is
 * cream "paper" in light-scheme themes and theme-tinted in dark ones
 * (light-dark()); .is-backlit / .is-themed force either. */
.vu {
  --_sweep: var(--vu-sweep, 96deg);
  --_r: var(--vu-radius, 50cqi);
  --_py: var(--vu-pivot-y, 66cqi);
  --_zone-at: var(--vu-zone-at, 0.6857);
  --_face: var(--vu-face, light-dark(#efe3bf, color-mix(in oklab, var(--accent) 9%, var(--surface-2))));
  --_ink: var(--vu-ink, light-dark(#24201a, var(--text)));
  --_zone: var(--vu-zone, light-dark(#c42a1f, var(--danger)));
  --_zone-ink: var(--vu-zone-ink, light-dark(#b0241a, color-mix(in oklab, var(--danger) 70%, var(--text))));
  --_needle: var(--vu-needle, light-dark(#1d1a16, var(--accent)));
  --_glow: var(--vu-glow, light-dark(rgb(255 238 190 / 0.95), color-mix(in oklab, var(--accent) 20%, transparent)));
  container-type: inline-size;
  position: relative;
  isolation: isolate;
  overflow: hidden;
  box-sizing: content-box;
  display: inline-block;
  vertical-align: middle;
  inline-size: var(--vu-size, 18rem);
  max-inline-size: calc(100% - 2 * var(--vu-bezel-width, 0.55rem));
  aspect-ratio: var(--vu-aspect, 16 / 10);
  background:
    var(--_mesh, linear-gradient(transparent, transparent)),
    radial-gradient(130% 95% at 50% 108%, var(--_glow), transparent 72%),
    var(--_face);
  color: var(--_ink);
  border: var(--vu-bezel-width, 0.55rem) solid var(--vu-bezel, var(--surface-2));
  border-radius: var(--vu-corner, var(--radius));
  box-shadow: inset 0 0.6rem 1.1rem -0.5rem rgb(0 0 0 / 0.55), var(--vu-shadow, 0 0 0 1px var(--border));
  font-family: var(--vu-font, var(--font));
  line-height: 1;
}
/* The printed arc (thin, full sweep) plus the zone band from --vu-zone-at. */
.vu::before {
  content: "";
  position: absolute;
  inset-inline-start: calc(50% - var(--_r));
  inset-block-start: calc(var(--_py) - var(--_r));
  inline-size: calc(2 * var(--_r));
  aspect-ratio: 1;
  border-radius: 50%;
  background: conic-gradient(from calc(var(--_sweep) / -2),
    var(--_ink) 0 calc(var(--_zone-at) * var(--_sweep)),
    var(--_zone) 0 var(--_sweep), transparent 0);
  mask:
    radial-gradient(farthest-side, transparent calc(100% - 0.6cqi), #000 calc(100% - 0.55cqi)),
    radial-gradient(farthest-side, transparent calc(100% - 2.6cqi), #000 calc(100% - 2.55cqi)),
    conic-gradient(from calc(var(--_sweep) / -2), transparent 0 calc(var(--_zone-at) * var(--_sweep)), #000 0 var(--_sweep), transparent 0);
  mask-composite: add, intersect;
  filter: var(--_ink-glow, none);
}
/* The pivot shroud over the needle's foot. */
.vu::after {
  content: "";
  position: absolute;
  z-index: calc(var(--z-raised) + 1);
  inset-inline-start: 50%;
  inset-block-start: var(--_py);
  inline-size: 22cqi;
  block-size: 14cqi;
  translate: -50% -50%;
  border-radius: 50%;
  background: radial-gradient(60% 60% at 50% 30%, color-mix(in oklab, var(--vu-bezel, var(--surface-2)) 70%, #fff), var(--vu-bezel, var(--surface-2)) 70%);
  box-shadow: 0 -1px 3px rgb(0 0 0 / 0.4);
}
.vu-scale { position: absolute; inset: 0; margin: 0; padding: 0; list-style: none; }
.vu-scale > li {
  --_a: calc(var(--_sweep) * (var(--at, 0) - 0.5));
  --_lr: calc(var(--_r) + 7cqi);
  position: absolute;
  inset-inline-start: calc(50% + sin(var(--_a)) * var(--_lr));
  inset-block-start: calc(var(--_py) - cos(var(--_a)) * var(--_lr));
  translate: -50% -50%;
  font-size: var(--vu-label-size, 6.5cqi);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  text-shadow: var(--_ink-shadow, none);
}
/* Each mark draws its own tick, turned to its angle; empty marks are minor. */
.vu-scale > li::before {
  content: "";
  position: absolute;
  inset-inline-start: 50%;
  inset-block-start: 50%;
  inline-size: 0.8cqi;
  block-size: 4.4cqi;
  margin: -2.2cqi 0 0 -0.4cqi;
  background: currentColor;
  transform: rotate(var(--_a)) translateY(calc(var(--_lr) - var(--_r) - 2.2cqi));
  filter: var(--_ink-glow, none);
}
.vu-scale > li:empty::before { block-size: 2.6cqi; margin-block-start: -1.3cqi; inline-size: 0.55cqi; transform: rotate(var(--_a)) translateY(calc(var(--_lr) - var(--_r) - 1.3cqi)); }
.vu-scale > li.is-over { color: var(--_zone-ink); }
.vu-scale > li.is-over::before { background: var(--_zone); }
.vu-legend {
  position: absolute;
  inset-block-start: 36cqi;
  inset-inline: 0;
  text-align: center;
  font-size: var(--vu-legend-size, 11cqi);
  font-weight: 500;
  letter-spacing: 0.04em;
  text-shadow: var(--_ink-shadow, none);
}
.vu-needle {
  position: absolute;
  z-index: var(--z-raised);
  inset-inline-start: calc(50% - var(--vu-needle-width, 0.7cqi) / 2);
  inset-block-start: calc(var(--_py) - var(--_r) - 4cqi);
  inline-size: var(--vu-needle-width, 0.7cqi);
  block-size: calc(var(--_r) + 4cqi);
  transform-origin: 50% 100%;
  rotate: calc(var(--_sweep) * (var(--metering-v) - 0.5));
  background: var(--_needle);
  border-radius: 1cqi 1cqi 0 0;
  box-shadow: 0.6cqi 0.8cqi 0.8cqi rgb(0 0 0 / 0.25);
  filter: var(--_ink-glow, none);
}
/* Dual-needle stereo face: the second needle takes the zone colour. */
.vu.is-pair .vu-needle + .vu-needle { --_needle: var(--vu-needle-r, var(--_zone)); }
.vu-peak {
  position: absolute;
  inset-inline-end: 4cqi;
  inset-block-start: 40cqi;
  display: grid;
  justify-items: center;
  gap: 1cqi;
  font-size: 4.2cqi;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.vu-peak::before {
  content: "";
  inline-size: 5cqi;
  aspect-ratio: 1;
  border-radius: 50%;
  background: color-mix(in oklab, var(--_zone) 35%, #200);
  box-shadow: inset 0 -0.5cqi 1cqi rgb(0 0 0 / 0.5);
}
.vu.is-peak .vu-peak::before {
  background: radial-gradient(circle at 40% 35%, #fff 0 12%, var(--_zone) 45%);
  box-shadow: 0 0 2.5cqi var(--_zone), 0 0 6cqi color-mix(in oklab, var(--_zone) 60%, transparent);
}
/* Faces: .is-backlit is the lamp-lit cream face in every theme, .is-themed
   always the theme's own surface (the light-dark() branches above). */
.vu.is-backlit { color-scheme: light; }
.vu.is-themed { color-scheme: dark; }
/* PPM: evenly spaced 1–7, white on black, wider swing, no red zone. */
.vu.is-ppm {
  --vu-face: #141414;
  --vu-ink: #f4f4f0;
  --vu-zone: #f4f4f0;
  --vu-zone-ink: #f4f4f0;
  --vu-needle: #f4f4f0;
  --vu-glow: transparent;
  --vu-sweep: 100deg;
  --vu-zone-at: 1;
}

/* --- .ledbar — segmented LED bargraph -------------------------------------
 *   <div class="ledbar" role="meter" aria-label="Left" aria-valuenow="-12"
 *        aria-valuemin="-40" aria-valuemax="0" style="--value:.7;--peak:.82"></div>
 * Zones: green to --zone-warn, yellow to --zone-over, red above (fractions
 * of the travel); .is-reference adds the red reference band between
 * --zone-ref-from and --zone-ref-to. Segments, unlit ghosts, peak-hold and
 * the overs cap are gradients, masks and clip-paths: no per-segment DOM. */
:is(.ledbar, .ledarc) {
  --_n: var(--ledbar-segments, 30);
  --_pk: clamp(0, var(--mode-peak, var(--peak, 0)), 1);
  --_warn: var(--zone-warn, 0.6);
  --_over: var(--zone-over, 0.85);
  --_rf: var(--zone-ref-from, var(--_over));
  --_rt: var(--zone-ref-to, var(--_over));
  --_bg: var(--ledbar-bg, var(--meter-track, var(--bg)));
  --_c-lo: var(--ledbar-low, var(--meter-low, var(--success)));
  --_c-mid: var(--ledbar-mid, var(--meter-mid, var(--warning)));
  --_c-hi: var(--ledbar-high, var(--meter-high, var(--danger)));
  --_c-ref: var(--ledbar-ref, var(--_c-hi));
  --_lit-v: round(down, var(--metering-v), calc(1 / var(--_n)));
  --_lit-pk: round(up, var(--_pk), calc(1 / var(--_n)));
}
:is(.ledbar, .ledarc).is-reference {
  --_rf: var(--zone-ref-from, 0.5);
  --_rt: var(--zone-ref-to, 0.56);
}
/* Unlit: each zone's colour, dimmed into the housing. */
:is(.ledbar, .ledarc) {
  --_lo: color-mix(in oklab, var(--_c-lo) var(--ledbar-unlit, 24%), var(--_bg));
  --_mid: color-mix(in oklab, var(--_c-mid) var(--ledbar-unlit, 24%), var(--_bg));
  --_hi: color-mix(in oklab, var(--_c-hi) var(--ledbar-unlit, 24%), var(--_bg));
  --_ref: color-mix(in oklab, var(--_c-ref) var(--ledbar-unlit, 24%), var(--_bg));
}
.ledbar::before, .ledbar::after, .ledarc::after {
  --_lo: var(--_c-lo);
  --_mid: var(--_c-mid);
  --_hi: var(--_c-hi);
  --_ref: var(--_c-ref);
}
.ledbar, .ledbar::before, .ledbar::after {
  --_zones: linear-gradient(var(--_dir, to top),
    var(--_lo) 0 calc(var(--_warn) * 100%), var(--_mid) 0 calc(var(--_rf) * 100%),
    var(--_ref) 0 calc(var(--_rt) * 100%), var(--_mid) 0 calc(var(--_over) * 100%), var(--_hi) 0);
  --_segs: repeating-linear-gradient(var(--_dir, to top),
    #000 0 calc(100% / var(--_n) - var(--ledbar-gap, 2px)), transparent 0 calc(100% / var(--_n)));
}
.ledbar {
  --_over-lit: 0.22;
  position: relative;
  box-sizing: border-box;
  flex: none;
  inline-size: var(--ledbar-width, 0.9rem);
  block-size: var(--ledbar-height, 12rem);
  background:
    repeating-linear-gradient(var(--_dir, to top), transparent 0 calc(100% / var(--_n) - var(--ledbar-gap, 2px)), var(--_bg) 0 calc(100% / var(--_n))),
    var(--_zones), var(--_bg);
  border: 0 solid var(--_bg);
  border-block-start: var(--ledbar-cap, 0.45rem) solid color-mix(in oklab, var(--ledbar-over, var(--danger)) calc(var(--_over-lit) * 100%), var(--_bg));
  border-radius: var(--ledbar-radius, 2px);
  box-shadow: var(--ledbar-shadow, 0 0 0 1px var(--hairline));
}
.ledbar.is-over { --_over-lit: 1; }
.ledbar::before, .ledbar::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--_zones);
  mask: var(--_segs);
  filter: var(--_lit-glow, none);
  pointer-events: none;
}
.ledbar::before { clip-path: inset(calc((1 - var(--_lit-v)) * 100%) 0 0 0); }
.ledbar::after { clip-path: inset(calc((1 - var(--_lit-pk)) * 100%) 0 calc((var(--_lit-pk) - 1 / var(--_n)) * 100%) 0); }
.ledbar.is-horizontal {
  --_dir: to right;
  inline-size: var(--ledbar-length, 100%);
  block-size: var(--ledbar-width, 0.9rem);
  border-block-start-width: 0;
  border-inline-end: var(--ledbar-cap, 0.45rem) solid color-mix(in oklab, var(--ledbar-over, var(--danger)) calc(var(--_over-lit) * 100%), var(--_bg));
}
.ledbar.is-horizontal::before { clip-path: inset(0 calc((1 - var(--_lit-v)) * 100%) 0 0); }
.ledbar.is-horizontal::after { clip-path: inset(0 calc((1 - var(--_lit-pk)) * 100%) 0 calc((var(--_lit-pk) - 1 / var(--_n)) * 100%)); }

/* Stereo pair with printed scales: any run of .ledbar and .ledbar-scale
   columns (scale | L | scale | R | scale), all the same height. */
.ledbar-pair {
  display: inline-grid;
  grid-auto-flow: column;
  justify-content: start;
  gap: var(--ledbar-pair-gap, 0.25rem);
  block-size: var(--ledbar-height, 12rem);
  max-inline-size: 100%;
}
.ledbar-pair:not(.is-horizontal) > .ledbar { block-size: auto; }
.ledbar-scale {
  position: relative;
  inline-size: var(--ledbar-scale-width, 1.6rem);
  margin: 0;
  padding: 0;
  list-style: none;
  font-family: var(--ledbar-scale-font, var(--font-mono));
  font-size: var(--ledbar-scale-size, 0.6rem);
  font-variant-numeric: tabular-nums;
  line-height: 1;
  color: var(--ledbar-scale-fg, var(--muted));
}
.ledbar-scale > li {
  position: absolute;
  inset-inline: 0;
  inset-block-end: calc((var(--at, 0) - 0.5 / var(--ledbar-segments, 30)) * (100% - var(--ledbar-cap, 0.45rem)));
  translate: 0 50%;
  text-align: center;
  white-space: nowrap;
}
.ledbar-scale > li.is-ref { color: var(--ledbar-scale-ref, var(--danger-text, var(--danger))); font-weight: 700; }
.ledbar-pair.is-horizontal {
  grid-auto-flow: row;
  justify-content: stretch;
  block-size: auto;
  inline-size: var(--ledbar-length, 100%);
}
.ledbar-pair.is-horizontal > .ledbar { inline-size: auto; }
.ledbar-pair.is-horizontal > .ledbar-scale { inline-size: auto; block-size: var(--ledbar-scale-height, 0.9rem); }
.ledbar-pair.is-horizontal > .ledbar-scale > li {
  inset-inline: auto;
  inset-block: 0;
  inset-inline-start: calc((var(--at, 0) - 0.5 / var(--ledbar-segments, 30)) * (100% - var(--ledbar-cap, 0.45rem)));
  translate: -50% 0;
  align-content: center;
}

/* --- .ledarc — the arc-shaped segmented loudness meter --------------------
 *   <div class="ledarc" role="meter" … style="--value:.6;--peak:.7">
 *     <ol class="ledarc-scale" aria-hidden="true"><li style="--at:0">-40</li>…</ol>
 *     <ol class="ledarc-scale is-lower" aria-hidden="true">…</ol>
 *     <span class="ledarc-legend">Loudness</span>
 *   </div>
 * A wide, shallow arc of a large circle (--ledarc-sweep of a circle of
 * radius --ledarc-radius); segments by angle, same zones as .ledbar. */
.ledarc {
  --_n: var(--ledarc-segments, 40);
  --_sweep: var(--ledarc-sweep, 64deg);
  --_r: var(--ledarc-radius, 80cqi);
  --_t: var(--ledarc-thickness, 9cqi);
  --_cy: calc(var(--_r) + 10cqi);
  --_from: calc(var(--_sweep) / -2);
  container-type: inline-size;
  position: relative;
  isolation: isolate;
  overflow: hidden;
  box-sizing: content-box;
  display: block;
  inline-size: var(--ledarc-size, 100%);
  max-inline-size: 100%;
  aspect-ratio: var(--ledarc-aspect, 100 / 40);
  background: var(--_mesh, linear-gradient(transparent, transparent)), var(--ledarc-face, transparent);
  color: var(--ledarc-ink, var(--text));
  font-family: var(--ledarc-font, var(--font));
  line-height: 1;
}
.ledarc::before, .ledarc::after {
  --_zones: conic-gradient(from var(--_from),
    var(--_lo) 0 calc(var(--_warn) * var(--_sweep)), var(--_mid) 0 calc(var(--_rf) * var(--_sweep)),
    var(--_ref) 0 calc(var(--_rt) * var(--_sweep)), var(--_mid) 0 calc(var(--_over) * var(--_sweep)),
    var(--_hi) 0 var(--_sweep), transparent 0);
  content: "";
  position: absolute;
  inset-inline-start: calc(50% - var(--_r));
  inset-block-start: calc(var(--_cy) - var(--_r));
  inline-size: calc(2 * var(--_r));
  aspect-ratio: 1;
  border-radius: 50%;
  background: var(--_zones);
  pointer-events: none;
  mask:
    repeating-conic-gradient(from var(--_from), #000 0 calc(var(--_sweep) / var(--_n) - var(--ledarc-gap, 0.45deg)), transparent 0 calc(var(--_sweep) / var(--_n))),
    radial-gradient(farthest-side, transparent calc(100% - var(--_t)), #000 calc(100% - var(--_t) + 0.5px));
  mask-composite: intersect;
}
.ledarc::after {
  mask:
    repeating-conic-gradient(from var(--_from), #000 0 calc(var(--_sweep) / var(--_n) - var(--ledarc-gap, 0.45deg)), transparent 0 calc(var(--_sweep) / var(--_n))),
    radial-gradient(farthest-side, transparent calc(100% - var(--_t)), #000 calc(100% - var(--_t) + 0.5px)),
    conic-gradient(from var(--_from),
      #000 0 calc(var(--_lit-v) * var(--_sweep)),
      transparent 0 calc((var(--_lit-pk) - 1 / var(--_n)) * var(--_sweep)),
      #000 0 calc(var(--_lit-pk) * var(--_sweep)), transparent 0);
  mask-composite: intersect, intersect;
  filter: var(--_lit-glow, none);
}
.ledarc-scale { position: absolute; inset: 0; margin: 0; padding: 0; list-style: none; }
.ledarc-scale > li {
  --_a: calc(var(--_sweep) * (var(--at, 0) - 0.5));
  --_lr: calc(var(--_r) + 3.4cqi);
  position: absolute;
  inset-inline-start: calc(50% + sin(var(--_a)) * var(--_lr));
  inset-block-start: calc(var(--_cy) - cos(var(--_a)) * var(--_lr));
  translate: -50% -50%;
  rotate: var(--_a);
  font-size: var(--ledarc-label-size, 3cqi);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  text-shadow: var(--_ink-shadow, none);
}
.ledarc-scale.is-lower > li { --_lr: calc(var(--_r) - var(--_t) - 3.4cqi); }
.ledarc-scale > li.is-ref { color: var(--ledarc-ref-fg, var(--danger-text, var(--danger))); font-weight: 700; }
.ledarc-legend {
  position: absolute;
  inset-inline: 0;
  inset-block-end: 3cqi;
  text-align: center;
  font-size: var(--ledarc-legend-size, 3.8cqi);
  letter-spacing: 0.04em;
  text-shadow: var(--_ink-shadow, none);
}

/* --- .meter-panel — Peak / Overs / Meter-mode radios ----------------------
 * Wrap the meters and their controls; radios carry data-peak, data-overs or
 * data-mode. A meter gives each mode its own reading (--value-2/--peak-2,
 * --value-3/--peak-3) and the checked radio picks it, CSS only. */
.meter-panel:has([data-mode="2"]:checked) :is(.vu-needle, .ledbar, .ledarc) { --_target: var(--value-2, var(--value, 0)); --mode-peak: var(--peak-2, var(--peak, 0)); }
.meter-panel:has([data-mode="3"]:checked) :is(.vu-needle, .ledbar, .ledarc) { --_target: var(--value-3, var(--value, 0)); --mode-peak: var(--peak-3, var(--peak, 0)); }
.meter-panel:has([data-peak="off"]:checked) :is(.ledbar, .ledarc) { --mode-peak: 0; }
.meter-panel:has([data-overs="off"]:checked) .ledbar { --_over-lit: 0.22; }
.meter-panel:has([data-overs="off"]:checked) .is-overs-lamp { --lamp-on: var(--lamp-off, var(--surface-2)); background: var(--lamp-off, var(--surface-2)); border-color: var(--lamp-border, var(--hairline)); box-shadow: none; }
.meter-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: start;
  gap: var(--space-s, 0.75rem) var(--space-l, 1.5rem);
}
.meter-controls fieldset { display: grid; gap: 0.35rem; margin: 0; padding: 0; border: 0; min-inline-size: 0; }
.meter-controls legend {
  padding: 0 0 0.35rem;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}
.meter-controls .keys { display: flex; flex-wrap: wrap; gap: 0.5rem; }
:is(.meter-controls, .eq-band) .key { min-block-size: var(--tap-min); min-inline-size: var(--tap-min); }

/* --- VFD: .is-vfd on .vu / .ledbar / .ledarc / .readout, .vfd window ------
 * Phosphor ink (--vfd-ink; .is-amber / .is-blue), glow, ghosted unlit
 * segments, a fine grid mesh and slightly soft edges. */
.vfd {
  position: relative;
  isolation: isolate;
  padding: var(--vfd-padding, 0.9rem 1rem);
  background:
    linear-gradient(170deg, rgb(255 255 255 / 0.07), transparent 38%),
    radial-gradient(120% 140% at 50% 120%, color-mix(in oklab, var(--vfd-ink) 10%, transparent), transparent 60%),
    var(--vfd-glass);
  color: var(--vfd-ink);
  border: 1px solid #000;
  border-radius: var(--vfd-radius, calc(var(--radius) * 0.6));
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.06), inset 0 0.5rem 1.4rem rgb(0 0 0 / 0.7), var(--vfd-shadow, 0 0 0 1px var(--border));
  text-shadow: 0 0 var(--vfd-glow, 0.35em) color-mix(in oklab, var(--vfd-ink) 70%, transparent);
  filter: brightness(var(--vfd-brightness, 1));
  color-scheme: dark;
  /* Meters behind the glass print their scales in phosphor. */
  --ledarc-ink: var(--vfd-ink);
  --ledbar-scale-fg: var(--vfd-ink);
}
.vfd::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: calc(var(--z-raised) + 2);
  border-radius: inherit;
  pointer-events: none;
  background:
    repeating-linear-gradient(0deg, rgb(0 0 0 / 0.32) 0 1px, transparent 1px 3px),
    repeating-linear-gradient(90deg, rgb(0 0 0 / 0.32) 0 1px, transparent 1px 3px);
}
:is(.vfd, .is-vfd).is-amber { --vfd-ink: var(--vfd-amber); }
:is(.vfd, .is-vfd).is-blue { --vfd-ink: var(--vfd-blue); }
.vfd-label {
  font-size: 0.68rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  opacity: 0.85;
}
.vfd-label.is-dim { opacity: 0.16; text-shadow: none; }
:is(.vu, .ledbar, .ledarc).is-vfd {
  --_mesh:
    repeating-linear-gradient(0deg, rgb(0 0 0 / 0.3) 0 1px, transparent 1px 3px),
    repeating-linear-gradient(90deg, rgb(0 0 0 / 0.3) 0 1px, transparent 1px 3px);
  --_ink-glow: drop-shadow(0 0 0.15rem color-mix(in oklab, var(--vfd-ink) 80%, transparent));
  --_ink-shadow: 0 0 0.3em color-mix(in oklab, var(--vfd-ink) 75%, transparent);
  --_lit-glow: blur(0.3px) drop-shadow(0 0 0.18rem color-mix(in oklab, var(--vfd-ink) 70%, transparent));
}
.vu.is-vfd {
  --vu-face: var(--vfd-glass);
  --vu-ink: var(--vfd-ink);
  --vu-zone: var(--vfd-hot);
  --vu-zone-ink: var(--vfd-hot);
  --vu-needle: var(--vfd-ink);
  --vu-glow: color-mix(in oklab, var(--vfd-ink) 14%, transparent);
  --vu-bezel: #0c0e0e;
  --vu-needle-width: 0.9cqi;
}
.vu.is-vfd .vu-needle { box-shadow: none; }
:is(.ledbar, .ledarc).is-vfd {
  --ledbar-bg: var(--vfd-glass);
  --ledbar-low: var(--vfd-ink);
  --ledbar-mid: var(--vfd-ink);
  --ledbar-high: var(--vfd-hot);
  --ledbar-ref: var(--vfd-hot);
  --ledbar-unlit: 11%;
  --ledbar-over: var(--vfd-hot);
  --ledarc-ink: var(--vfd-ink);
  --ledbar-scale-fg: var(--vfd-ink);
}
.ledbar.is-vfd {
  --ledbar-gap: calc(48% / var(--_n));
  --ledbar-shadow: 0 0 0 1px #000;
  background:
    var(--_mesh),
    repeating-linear-gradient(var(--_dir, to top), transparent 0 calc(100% / var(--_n) - var(--ledbar-gap)), var(--_bg) 0 calc(100% / var(--_n))),
    var(--_zones), var(--_bg);
}
.ledarc.is-vfd { --ledarc-gap: 0.7deg; --ledarc-face: var(--vfd-glass); }
.readout.is-vfd {
  --readout-bg: var(--vfd-glass);
  --readout-padding: 0.15em 0.45em;
  --readout-fg: var(--vfd-ink);
  --readout-unit-fg: color-mix(in oklab, var(--vfd-ink) 75%, transparent);
  --readout-glow: 0 0 0.2em color-mix(in oklab, var(--vfd-ink) 80%, transparent), 0 0 0.6em color-mix(in oklab, var(--vfd-ink) 40%, transparent);
  position: relative;
}
.vfd .readout.is-vfd { --readout-bg: transparent; }
/* data-ghost="88:88" prints the unlit segments behind the value. */
.readout.is-vfd[data-ghost]::before {
  content: attr(data-ghost);
  position: absolute;
  inset-block-start: var(--readout-ghost-top, 0.1em);
  inset-inline-start: var(--readout-ghost-start, 0.35em);
  opacity: var(--vfd-ghost-opacity, 0.12);
  text-shadow: none;
  pointer-events: none;
}

/* Themes can make every meter a VFD: html[data-theme="x"] { --meter-style: vfd } */
@container style(--meter-style: vfd) {
  :is(.vu, .ledbar, .ledarc):not(.is-backlit, .is-themed) {
    --_mesh:
      repeating-linear-gradient(0deg, rgb(0 0 0 / 0.3) 0 1px, transparent 1px 3px),
      repeating-linear-gradient(90deg, rgb(0 0 0 / 0.3) 0 1px, transparent 1px 3px);
    --_ink-glow: drop-shadow(0 0 0.15rem color-mix(in oklab, var(--vfd-ink) 80%, transparent));
    --_ink-shadow: 0 0 0.3em color-mix(in oklab, var(--vfd-ink) 75%, transparent);
    --_lit-glow: blur(0.3px) drop-shadow(0 0 0.18rem color-mix(in oklab, var(--vfd-ink) 70%, transparent));
    --vu-face: var(--vfd-glass);
    --vu-ink: var(--vfd-ink);
    --vu-zone: var(--vfd-hot);
    --vu-zone-ink: var(--vfd-hot);
    --vu-needle: var(--vfd-ink);
    --vu-glow: color-mix(in oklab, var(--vfd-ink) 14%, transparent);
    --ledbar-bg: var(--vfd-glass);
    --ledbar-low: var(--vfd-ink);
    --ledbar-mid: var(--vfd-ink);
    --ledbar-high: var(--vfd-hot);
    --ledbar-ref: var(--vfd-hot);
    --ledbar-unlit: 11%;
    --ledarc-ink: var(--vfd-ink);
    --ledarc-face: var(--vfd-glass);
  }
}

/* --- .eq-graph — parametric EQ with RTA and spectrogram overlay ------------
 *   <div class="eq-editor">            toggles + graph + band panels
 *     <div class="eq-graph">
 *       <div class="spectrogram">…cells <i style="--v:.4"> or a <canvas>…</div>
 *       <ol class="rta"><li style="--v:.6;--peak:.7"></li>×31</ol>
 *       <svg class="eq-graph-curve" viewBox="0 0 1000 400" preserveAspectRatio="none"><path d="…"/></svg>
 *       <ol class="eq-freqs"><li style="--f:.233">100</li>…</ol>
 *       <ol class="eq-gains"><li style="--g:.5">+6</li>…</ol>
 *       <label class="eq-node" style="--f:.42;--g:.3" data-series="2"><input type="radio" name="band">2</label>
 *     </div>
 *     <div class="eq-bands"><section class="eq-band">…</section>…</div>
 *   </div>
 * --f is the log-frequency position 0–1 (20 Hz–20 kHz: log10(f/20)/3),
 * --g the gain −1…1 of the displayed range. */
.eq-graph {
  container-type: inline-size;
  position: relative;
  isolation: isolate;
  overflow: hidden;
  inline-size: 100%;
  aspect-ratio: var(--eq-graph-aspect, 5 / 2);
  min-block-size: 11rem;
  background: var(--eq-bg, var(--bg));
  border: 1px solid var(--eq-border, var(--hairline));
  border-radius: var(--eq-radius, var(--radius));
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}
/* Grid: a decade of log lines (1, 2…9) repeated three times from 20 Hz,
   dB lines every 1/--eq-gain-lines, the 0 dB line stronger. */
.eq-graph::before {
  --_dec: calc(100cqi / 3);
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    linear-gradient(to top, transparent calc(50% - 0.5px), var(--eq-zero, var(--border)) 0 calc(50% + 0.5px), transparent 0),
    repeating-linear-gradient(to bottom, transparent 0 calc(100% / var(--eq-gain-lines, 6) - 1px), var(--eq-grid, var(--hairline)) 0 calc(100% / var(--eq-gain-lines, 6))),
    linear-gradient(to right,
      var(--eq-grid-major, var(--border)) 0 1px, transparent 0 30.1%,
      var(--eq-grid, var(--hairline)) 0 calc(30.1% + 1px), transparent 0 47.7%,
      var(--eq-grid, var(--hairline)) 0 calc(47.7% + 1px), transparent 0 60.2%,
      var(--eq-grid, var(--hairline)) 0 calc(60.2% + 1px), transparent 0 69.9%,
      var(--eq-grid, var(--hairline)) 0 calc(69.9% + 1px), transparent 0 77.8%,
      var(--eq-grid, var(--hairline)) 0 calc(77.8% + 1px), transparent 0 84.5%,
      var(--eq-grid, var(--hairline)) 0 calc(84.5% + 1px), transparent 0 90.3%,
      var(--eq-grid, var(--hairline)) 0 calc(90.3% + 1px), transparent 0 95.4%,
      var(--eq-grid, var(--hairline)) 0 calc(95.4% + 1px), transparent 0)
      calc(var(--_dec) * -0.30103) 0 / var(--_dec) 100% repeat-x;
}
.eq-graph > :is(.spectrogram, .rta, .eq-graph-curve) { position: absolute; inset: 0; margin: 0; padding: 0; }
/* Spectrogram colour scale, quiet to loud: floor (no signal), signal
   detected, low, medium, high, peak. Each theme's default is built from its
   own palette (accent, then success → warning → danger); a theme sets
   --spectro-* to choose its own, and data-spectro radios in the
   .eq-editor pick a preset (theme / scale / mono / thermal). */
.eq-graph {
  --_s-floor: var(--spectro-floor, transparent);
  --_s-signal: var(--spectro-signal, var(--accent));
  --_s-low: var(--spectro-low, var(--success));
  --_s-mid: var(--spectro-mid, color-mix(in oklab, var(--warning) 55%, var(--success)));
  --_s-high: var(--spectro-high, var(--warning));
  --_s-peak: var(--spectro-peak, var(--danger));
}
.eq-editor:has([data-spectro="scale"]:checked) .eq-graph {
  --_s-signal: var(--chart-seq-2); --_s-low: var(--chart-seq-4); --_s-mid: var(--chart-seq-6);
  --_s-high: var(--chart-seq-7); --_s-peak: var(--chart-seq-8);
}
.eq-editor:has([data-spectro="mono"]:checked) .eq-graph {
  --_s-signal: color-mix(in oklab, var(--accent) 30%, transparent); --_s-low: color-mix(in oklab, var(--accent) 50%, transparent);
  --_s-mid: color-mix(in oklab, var(--accent) 72%, transparent); --_s-high: var(--accent);
  --_s-peak: color-mix(in oklab, var(--accent) 45%, #fff);
}
.eq-editor:has([data-spectro="thermal"]:checked) .eq-graph {
  --_s-signal: #2a0a5e; --_s-low: #8a1f8c; --_s-mid: #e0333c; --_s-high: #ff9a1f; --_s-peak: #fff3b0;
}
.spectrogram {
  display: grid;
  grid-template-columns: repeat(var(--spectrogram-cols, 31), 1fr);
  grid-auto-rows: 1fr;
  opacity: var(--spectrogram-opacity, 0.45);
  pointer-events: none;
}
/* Each cell shows the slice of a tall scale gradient at its --v (0–1), so
   any five colours map without per-colour rules. */
.spectrogram > :not(canvas, img, video) {
  background: linear-gradient(to bottom,
      var(--_s-floor) 0%, var(--_s-floor) 18%, var(--_s-signal) 30%, var(--_s-low) 48%,
      var(--_s-mid) 66%, var(--_s-high) 84%, var(--_s-peak) 100%)
    0 calc(var(--v, 0) * 100%) / 100% 4000% no-repeat;
}
.spectrogram > :is(canvas, img, video) { position: absolute; inset: 0; inline-size: 100%; block-size: 100%; object-fit: fill; }

/* RTA: one bar per band. Each <li> carries --v (RMS), --avg and --peak, all
   0–1 over a 60 dB window (1 = 0 dBFS). The detector, range and decay come
   from the .eq-editor's controls (below) or the --rta-* tokens. */
@property --rta-h { syntax: "<number>"; inherits: false; initial-value: 0; }
@property --rta-pk { syntax: "<number>"; inherits: false; initial-value: 0; }
.rta {
  --_rta: var(--rta-color, var(--chart-series-2, var(--accent)));
  display: flex;
  gap: var(--rta-gap, 1px);
  list-style: none;
  pointer-events: none;
  opacity: var(--rta-opacity, 0.75);
}
.rta.is-pre { --_rta: var(--rta-pre-color, var(--chart-series-3, var(--muted))); }
.rta > li {
  --_src: var(--v, 0);
  --rta-h: clamp(0, 1 - (1 - var(--_src) * var(--rta-l)) * var(--rta-k, 1), 1);
  --rta-pk: clamp(0, 1 - (1 - var(--peak, 0)) * var(--rta-k, 1), 1);
  --_cap: var(--rta-peak, color-mix(in oklab, var(--_rta) 55%, var(--text)));
  flex: 1;
  background:
    linear-gradient(var(--_cap), var(--_cap)) 0 calc((1 - var(--rta-pk)) * 100%) / 100% var(--_cap-size, 2px) no-repeat,
    linear-gradient(var(--_rta), var(--_rta)) 0 calc((1 - var(--rta-h)) * 100%) / 100% 2px no-repeat,
    linear-gradient(to top, color-mix(in oklab, var(--_rta) var(--rta-fill, 30%), transparent), color-mix(in oklab, var(--_rta) var(--rta-fill-top, 60%), transparent)) 0 100% / 100% calc(var(--rta-h) * 100%) no-repeat;
}
@media (prefers-reduced-motion: no-preference) {
  html:not([data-motion="reduced"]) .rta > li { transition: --rta-h var(--rta-decay, 0.45s) ease-out, --rta-pk var(--rta-decay, 0.45s) ease-out; }
}

  html[data-motion="always"]:not([data-motion="reduced"]) .rta > li { transition: --rta-h var(--rta-decay, 0.45s) ease-out, --rta-pk var(--rta-decay, 0.45s) ease-out; }

/* .is-line: just the band tops, a stepped trace. */
.rta.is-line > li { --rta-fill: 0%; --rta-fill-top: 0%; }
/* Right-hand RTA level scale: each li prints data-r60 or, at 30 dB, data-r30. */
.eq-graph > .eq-levels {
  position: absolute; inset: 0; margin: 0; padding: 0; list-style: none; pointer-events: none;
  font-size: var(--eq-label-size, 0.62rem); line-height: 1; color: var(--eq-label-fg, var(--muted));
}
.eq-levels > li { position: absolute; inset-inline-end: 0.4rem; inset-block-start: calc((1 - var(--l, 0)) * 100%); translate: 0 calc((1 - var(--l, 0)) * -100%); }
.eq-levels > li:last-child { margin-block-start: -1.1rem; }
.eq-levels > li::before { content: attr(data-r60); }
.eq-editor:has([data-rta-range="30"]:checked) .eq-levels > li::before { content: attr(data-r30); }

.eq-graph-curve { inline-size: 100%; block-size: 100%; pointer-events: none; overflow: visible; }
.eq-graph-curve path {
  fill: var(--eq-fill, color-mix(in srgb, var(--eq-line, var(--accent)) 20%, transparent));
  stroke: var(--eq-line, var(--accent));
  stroke-width: var(--eq-graph-line-width, 2.5px);
  vector-effect: non-scaling-stroke;
  stroke-linejoin: round;
}
.eq-graph-curve path.is-band { fill: none; stroke: var(--series, var(--muted)); stroke-width: 1.25px; stroke-dasharray: 4 3; opacity: 0.8; }
.eq-graph > :is(.eq-freqs, .eq-gains) {
  position: absolute;
  inset: 0;
  margin: 0;
  padding: 0;
  list-style: none;
  pointer-events: none;
  font-size: var(--eq-label-size, 0.62rem);
  line-height: 1;
  color: var(--eq-label-fg, var(--muted));
}
.eq-graph > .eq-nodes { position: absolute; inset: 0; z-index: calc(var(--z-raised) + 1); margin: 0; padding: 0; border: 0; min-inline-size: 0; }
.eq-freqs > li, .eq-gains > li { white-space: nowrap; }
.eq-freqs > li { position: absolute; inset-block-end: 0.4rem; inset-inline-start: calc(var(--f, 0) * 100%); translate: -50% 0; }
.eq-freqs > li:first-child { translate: 0.35rem 0; }
.eq-freqs > li:last-child { translate: calc(-100% - 0.35rem) 0; }
.eq-gains > li { position: absolute; inset-inline-start: 0.4rem; inset-block-start: calc((1 - var(--g, 0)) * 50%); translate: 0 -50%; }
.eq-node {
  position: absolute;
  z-index: calc(var(--z-raised) + 1);
  inset-inline-start: calc(var(--f, 0.5) * 100%);
  inset-block-start: calc((1 - var(--g, 0)) * 50%);
  translate: -50% -50%;
  display: grid;
  place-items: center;
  inline-size: max(var(--tap-min), 1.9rem);
  block-size: max(var(--tap-min), 1.9rem);
  font: 700 0.72rem/1 var(--font);
  color: var(--text);
}
.eq-node::before {
  content: "";
  position: absolute;
  inset: calc(50% - 0.8rem);
  z-index: -1;
  border-radius: 50%;
  background: var(--eq-node-bg, var(--surface));
  border: 2px solid var(--series, var(--accent));
  box-shadow: 0 1px 3px rgb(0 0 0 / 0.35);
}
.eq-node input { position: absolute; inset: 0; margin: 0; opacity: 0; }
.eq-node:has(input:checked) { color: var(--eq-node-active-fg, var(--bg)); }
.eq-node:has(input:checked)::before {
  background: var(--eq-node-active, var(--text));
  box-shadow: 0 0 0 0.3rem color-mix(in oklab, var(--series, var(--accent)) 45%, transparent), 0 0 0.9rem var(--series, var(--accent));
}
.eq-node:has(input:focus-visible)::before { outline: 2px solid var(--focus); outline-offset: 3px; }
.eq-node.is-off::before { border-style: dashed; opacity: 0.6; }
/* Analyser controls, all radios or checkboxes inside the .eq-editor:
   - data-rta-view="pre|both|post": a linked three-way selector;
   - data-rta-range="30|60": the RTA window (60 dB default);
   - data-rta-decay="slow|med|fast": how fast bars fall;
   - data-rta-mode="peak|rms|avg|peak-rms|peak-avg": the detector — bars
     from --peak, --v or --avg, with or without peak-hold caps;
   - data-spectro-src (a switch): spectrogram from the pre signal when on;
   - data-eq-show="rta|spectrogram": on/off checkboxes. */
.eq-editor:has([data-rta-view="pre"]:checked) .rta:not(.is-pre),
.eq-editor:has([data-rta-view="post"]:checked) .rta.is-pre,
.eq-editor:has([data-eq-show="rta"]:not(:checked)) .rta:not(.is-pre),
.eq-editor:has([data-eq-show="spectrogram"]:not(:checked)) .spectrogram,
.eq-editor:has([data-spectro-src]:checked) .spectrogram:not(.is-pre):has(~ .spectrogram.is-pre),
.eq-editor:not(:has([data-spectro-src]:checked)) .spectrogram.is-pre { display: none; }
.eq-editor:has([data-rta-range="30"]:checked) { --rta-k: 2; }
.eq-editor:has([data-rta-decay="slow"]:checked) { --rta-decay: 1.2s; --rta-live-speed: 2; }
.eq-editor:has([data-rta-decay="fast"]:checked) { --rta-decay: 0.12s; --rta-live-speed: 0.45; }
.eq-editor:has([data-rta-mode="rms"]:checked) .rta > li { --_cap-size: 0px; }
.eq-editor:has([data-rta-mode="avg"]:checked) .rta > li { --_src: var(--avg, var(--v, 0)); --_cap-size: 0px; }
.eq-editor:has([data-rta-mode="peak"]:checked) .rta > li { --_src: var(--peak, 0); --_cap-size: 0px; }
.eq-editor:has([data-rta-mode="peak-avg"]:checked) .rta > li { --_src: var(--avg, var(--v, 0)); }
/* Band panels: the Nth .eq-band shows while the Nth .eq-node is checked. */
.eq-bands > .eq-band { display: none; }
.eq-editor:has(.eq-node:nth-of-type(1) input:checked) .eq-band:nth-of-type(1),
.eq-editor:has(.eq-node:nth-of-type(2) input:checked) .eq-band:nth-of-type(2),
.eq-editor:has(.eq-node:nth-of-type(3) input:checked) .eq-band:nth-of-type(3),
.eq-editor:has(.eq-node:nth-of-type(4) input:checked) .eq-band:nth-of-type(4),
.eq-editor:has(.eq-node:nth-of-type(5) input:checked) .eq-band:nth-of-type(5),
.eq-editor:has(.eq-node:nth-of-type(6) input:checked) .eq-band:nth-of-type(6),
.eq-editor:has(.eq-node:nth-of-type(7) input:checked) .eq-band:nth-of-type(7),
.eq-editor:has(.eq-node:nth-of-type(8) input:checked) .eq-band:nth-of-type(8) { display: flex; }
.eq-band {
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem 1rem;
  border-inline-start: 3px solid var(--series, var(--accent));
  padding-inline-start: 0.75rem;
}

@media (forced-colors: active) {
  :is(.vu, .ledbar, .ledarc, .rta, .spectrogram) { forced-color-adjust: none; }
}
/* ftl-themes v5: navigation and structure (PLAN.md §9 #37, #39–#46) and
 * the keyboard layer (§20). Built into every bundle after core.css, so it
 * reuses .tabs/.tab, .btn-close, .nav-item, the nested-ul menu rules and
 * the --z-* scale from there. Docs: docs/components/navigation.md. */

/* --- Closable tabs (#37) ---------------------------------------------------
 *   <div class="tabs is-scroll" role="tablist">
 *     <div class="tab is-active"><button role="tab" aria-selected="true">a.css</button>
 *       <button class="btn-close" tabindex="-1" aria-label="Close a.css"></button></div>
 *     <button class="tab-add" aria-label="New tab"></button>
 *   </div>
 * A <button> can't hold a second button, so a closable .tab is a wrapper:
 * the first child is the tab itself, the .btn-close sits beside it. */
.tabs.is-scroll {
  /* max-inline-size: a theme may make .tabs inline-flex (a capsule), which
     would otherwise grow with its tabs and never scroll. */
  max-inline-size: 100%;
  flex-wrap: nowrap;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: thin;
  scrollbar-color: var(--scrollbar-thumb, var(--border)) transparent;
}
.tabs.is-scroll > * { flex-shrink: 0; }
.tab:has(> .btn-close) {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
  padding: 0;
  padding-inline-end: var(--space-3xs);
}
.tab:has(> .btn-close) > :not(.btn-close) {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  align-self: stretch;
  min-block-size: var(--tap-min);
  max-inline-size: var(--tab-max, 14rem);
  padding-block: 0.4em;
  padding-inline: 0.9em 0.2em;
  color: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  touch-action: manipulation;
}
.tab > .btn-close { width: 1.5em; height: 1.5em; font-size: 0.95em; }
/* On a filled tab the close glyph follows the tab's own text colour. */
.tab > .btn-close:not(:hover) { color: var(--tab-close-fg, inherit); }
/* Unsaved changes: a dot before the name (a shape, not just a colour). */
.tab.is-dirty > :not(.btn-close)::before {
  content: var(--tab-dirty-glyph, "\25CF");
  content: var(--tab-dirty-glyph, "\25CF") / "";
  font-size: 0.75em;
  color: var(--tab-dirty-fg, currentColor);
}
/* The add-tab button stays in view at the end of a scrolling strip. */
.tab-add {
  position: sticky;
  inset-inline-end: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  min-inline-size: var(--tap-min);
  min-block-size: var(--tap-min);
  padding: 0 0.6em;
  border-radius: var(--tab-radius, var(--radius) var(--radius) 0 0);
  background: var(--tab-add-bg, var(--surface));
  color: var(--tab-add-fg, var(--muted));
  touch-action: manipulation;
}
.tab-add::before { content: var(--tab-add-glyph, "+"); font-size: 1.25em; line-height: 1; }
.tab-add:has(.icon)::before { content: none; }
.tab-add:focus-visible { color: var(--text); outline-offset: -2px; }
@media (hover: hover) {
  .tab-add:hover { color: var(--text); background: var(--tab-add-bg-hover, var(--surface-2)); }
}

/* --- Menu bar (#39) ---------------------------------------------------------
 * The same nested-ul markup as .context-menu (core's "v5 nested menus"
 * shows and hides every .has-submenu > ul and draws the separators):
 *   <ul class="menubar" role="menubar">
 *     <li class="has-submenu"><button role="menuitem" aria-haspopup="menu">File</button>
 *       <ul role="menu"><li><button role="menuitem">New <kbd>Ctrl+N</kbd></button></li>…</ul></li>
 *   </ul>
 * Top-level items open their panel on hover, on focus (tap on touch), or
 * when the app sets aria-expanded="true". */
.menubar {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: var(--menubar-gap, 0);
  padding: var(--menubar-pad, var(--space-3xs));
  background: var(--menubar-bg, var(--surface));
  color: var(--menubar-fg, var(--text));
  border-bottom: var(--menubar-rule-width, 1px) solid var(--menubar-rule, var(--border));
}
/* In the bar, items take the bar's text colour as .nav-item does (core.css). */
.app-bar .menubar { --menubar-bg: transparent; --menubar-rule-width: 0; --menubar-fg: var(--app-bar-text); }
.menubar li { position: relative; }
.menubar > li > :is(button, a) {
  display: flex;
  align-items: center;
  justify-content: center;
  block-size: 100%;
  min-block-size: var(--tap-min);
  min-inline-size: var(--tap-min);
  padding: 0.3em 0.75em;
  border-radius: var(--menubar-item-radius, var(--radius));
  color: inherit;
  font-size: var(--menubar-font-size, 0.9em);
  white-space: nowrap;
  touch-action: manipulation;
}
.menubar > li > :is(button, a):is(:focus-visible, [aria-expanded="true"]),
.menubar > li:focus-within > :is(button, a) {
  background: var(--menubar-item-bg-active, var(--accent));
  color: var(--menubar-item-fg-active, var(--on-accent));
}
@media (hover: hover) {
  .menubar > li:hover > :is(button, a) {
    background: var(--menubar-item-bg-active, var(--accent));
    color: var(--menubar-item-fg-active, var(--on-accent));
  }
}
.menubar > li > :is(button, a):focus-visible { outline-offset: -2px; }
/* Top-level items open downwards, so they carry no side arrow. */
.menubar > .has-submenu > :is(button, a)::after { content: none; }
:is(.context-menu, .menubar) .has-submenu > [aria-expanded="true"] + ul { display: block; }
/* The dropdown panels: the context menu's surface, under the item. Two
   classes so it outranks core's touch-tier inline-submenu rule. */
.menubar > .has-submenu > ul {
  position: absolute;
  top: 100%;
  inset-inline-start: 0;
  z-index: var(--z-dropdown);
  min-inline-size: var(--menubar-panel-min, 12rem);
  inline-size: max-content;
  max-inline-size: min(20rem, calc(100vw - 2rem));
  margin: 0;
  padding: 0.35rem;
  background: var(--context-menu-bg, var(--dropdown-bg, var(--surface-2)));
  border: var(--context-menu-border-width, 1px) solid var(--context-menu-border, var(--dropdown-border, var(--border)));
  border-radius: var(--context-menu-radius, var(--dropdown-radius, var(--radius)));
  box-shadow: var(--context-menu-shadow, var(--panel-shadow, 0 2px 12px rgba(0, 0, 0, 0.3)));
}
.menubar > li.is-end > ul { inset-inline-start: auto; inset-inline-end: 0; }
/* Nested-menu items reach the tap minimum (core sizes .context-menu-item,
   not yet the ul form's buttons). */
:is(.menubar, .context-menu) li > :is(button, a) { min-block-size: var(--tap-min); touch-action: manipulation; }
/* Deeper levels fly out to the side on desktop, like the context menu. */
@media (min-width: 901px) and (hover: hover) {
  .menubar ul .has-submenu > ul {
    position: absolute;
    top: calc(-0.35rem - 1px);
    inset-inline-start: calc(100% + 0.35rem);
    z-index: var(--z-dropdown);
    min-inline-size: 10rem;
    inline-size: max-content;
    max-inline-size: 20rem;
    padding: 0.35rem;
    background: var(--context-menu-bg, var(--dropdown-bg, var(--surface-2)));
    border: var(--context-menu-border-width, 1px) solid var(--context-menu-border, var(--dropdown-border, var(--border)));
    border-radius: var(--context-menu-radius, var(--dropdown-radius, var(--radius)));
    box-shadow: var(--context-menu-shadow, var(--panel-shadow, 0 2px 12px rgba(0, 0, 0, 0.3)));
  }
  .menubar ul .has-submenu > ul::before {
    content: "";
    position: absolute;
    inset-block: 0;
    inset-inline-start: -0.5rem;
    inline-size: 0.5rem;
  }
}

/* Menu item states shared by .menubar and the ul .context-menu: disabled,
   and checkable items (role="menuitemcheckbox"/"menuitemradio" with
   aria-checked) that show a tick, not just a colour. */
:is(.context-menu, .menubar) li > :is(button, a):is(:disabled, [aria-disabled="true"]) {
  opacity: 0.5;
  cursor: var(--cursor-not-allowed, not-allowed);
}
:is(.context-menu, .menubar) li > :is([role="menuitemcheckbox"], [role="menuitemradio"])::before {
  content: "";
  inline-size: 1em;
  margin-inline-end: -0.5rem;
  flex: none;
}
:is(.context-menu, .menubar) li > :is([role="menuitemcheckbox"], [role="menuitemradio"])[aria-checked="true"]::before {
  content: var(--menu-check-glyph, "\2713");
}
:is(.context-menu, .menubar) li > :is([role="menuitemcheckbox"], [role="menuitemradio"]) { justify-content: flex-start; }
:is(.context-menu, .menubar) li > :is([role="menuitemcheckbox"], [role="menuitemradio"]) > kbd { margin-inline-start: auto; }

/* --- Tree view (#40) ----------------------------------------------------------
 * Nested lists with <details>, so expand and collapse need no JS:
 *   <ul class="tree">
 *     <li><details open><summary>src</summary>
 *       <ul><li><a href="#" aria-current="page">app.js</a></li></ul></details></li>
 *   </ul>
 * Rows are the <summary> and any a / button / span directly in an <li>.
 * Selected: aria-current, .is-selected, or aria-selected="true" on a
 * role="treeitem" <li> (apps that add arrow-key navigation). */
.tree {
  font-size: var(--tree-font-size, 0.9em);
  --tree-marker: 1em;
}
.tree ul {
  margin-inline-start: calc(0.5em + var(--tree-marker) / 2);
  padding-inline-start: var(--tree-indent, 0.5em);
  border-inline-start: 1px solid var(--tree-guide, var(--hairline));
}
.tree :is(summary, li > a, li > button, li > span) {
  display: flex;
  align-items: center;
  gap: 0.4em;
  inline-size: 100%;
  min-block-size: var(--tap-min);
  padding: 0.25em 0.5em;
  padding-inline-start: calc(0.9em + var(--tree-marker));
  border-radius: var(--tree-item-radius, var(--radius));
  color: var(--tree-item-fg, var(--text));
  text-align: start;
  list-style: none;
  overflow-wrap: anywhere;
  touch-action: manipulation;
}
.tree summary { padding-inline-start: 0.5em; }
.tree summary::-webkit-details-marker { display: none; }
.tree summary::before {
  content: var(--tree-glyph, "\25B8");
  flex: none;
  inline-size: var(--tree-marker);
  text-align: center;
  color: var(--tree-glyph-fg, var(--muted));
}
.tree details[open] > summary::before { transform: rotate(90deg); }
.tree .icon { color: var(--tree-icon-fg, var(--muted)); }
:is(.tree :is(summary, li > a, li > button, li > span):is(.is-selected, [aria-current]:not([aria-current="false"])),
    .tree [role="treeitem"]:is([aria-selected="true"], :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))) > :is(a, button, span, details > summary)) {
  background: var(--tree-item-bg-selected, var(--row-selected-bg, rgba(128, 128, 128, 0.25)));
  color: var(--tree-item-fg-selected, var(--row-selected-fg, inherit));
  box-shadow: var(--tree-selected-marker, inset 0.2rem 0 0 var(--accent));
  font-weight: 600;
}
:is(.tree :is(summary, li > a, li > button, li > span):is(.is-selected, [aria-current]:not([aria-current="false"])),
    .tree [role="treeitem"]:is([aria-selected="true"], :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))) > :is(a, button, span, details > summary)) .icon { color: inherit; }
@media (hover: hover) {
  .tree :is(summary, li > a, li > button):hover { background: var(--row-hover-bg, rgba(128, 128, 128, 0.12)); }
}
@media (prefers-reduced-motion: no-preference) {
  .tree summary::before { transition: transform 0.15s ease; }
}

  :root[data-motion="always"] .tree summary::before { transition: transform 0.15s ease; }


/* --- Bottom tab bar (#41) -----------------------------------------------------
 *   <nav class="tabbar" aria-label="Primary">
 *     <a href="/" aria-current="page"><svg class="icon">…</svg><span>Home</span></a>…
 *   </nav>
 * The mobile primary nav. In the shell it can BE the status row:
 * <nav class="app-status tabbar">. Clears the home indicator through
 * env(safe-area-inset-bottom) (needs viewport-fit=cover to be non-zero). */
.tabbar {
  display: flex;
  align-items: stretch;
  padding: 0 env(safe-area-inset-right, 0px) env(safe-area-inset-bottom, 0px) env(safe-area-inset-left, 0px);
  background: var(--tabbar-bg, var(--surface));
  color: var(--tabbar-fg, var(--muted));
  border-top: var(--tabbar-rule-width, 1px) solid var(--tabbar-rule, var(--border));
}
.app-status.tabbar {
  gap: 0;
  padding: 0 env(safe-area-inset-right, 0px) env(safe-area-inset-bottom, 0px) env(safe-area-inset-left, 0px);
  font-size: inherit;
}
.tabbar > :is(a, button) {
  position: relative;
  flex: 1 1 0;
  min-inline-size: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-3xs);
  min-block-size: max(var(--tap-min), var(--tabbar-height, 3.25rem));
  padding: var(--space-2xs) var(--space-3xs);
  border-top: var(--tabbar-indicator-width, 2px) solid transparent;
  color: inherit;
  font-size: var(--tabbar-label-size, 0.7rem);
  letter-spacing: var(--label-tracking, normal);
  touch-action: manipulation;
}
.tabbar > * > .icon { --icon-size: var(--tabbar-icon-size, 1.5rem); }
.tabbar > * > :not(.icon, .badge) {
  max-inline-size: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tabbar > :is([aria-current]:not([aria-current="false"]), :is([aria-selected="true"], :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))), :is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked)))) {
  color: var(--tabbar-fg-active, var(--accent-text, var(--accent)));
  border-top-color: var(--tabbar-indicator, currentColor);
  font-weight: 600;
}
.tabbar > * > .badge {
  position: absolute;
  top: var(--space-3xs);
  inset-inline-start: calc(50% + 0.35rem);
  font-size: 0.65rem;
}
.tabbar > :focus-visible { outline-offset: -2px; }
@media (hover: hover) {
  .tabbar > :hover { color: var(--tabbar-fg-hover, var(--text)); }
}

/* --- Bottom sheet (#42) ---------------------------------------------------------
 *   <dialog class="sheet" aria-labelledby="t">
 *     <div class="sheet-header"><h2 id="t">Share</h2><button class="btn-close" commandfor=… command="close" aria-label="Close"></button></div>…
 *   </dialog>
 * The mobile counterpart of .modal and .dropdown: anchored to the bottom
 * edge, full width up to --sheet-max, with a drag-handle mark on top (a
 * visual; dragging is the app's). Also works as <div class="sheet" popover>. */
.sheet {
  --surface-pad: var(--sheet-pad, var(--space-m));
  position: relative;
  inline-size: min(100%, var(--sheet-max, 40rem));
  max-inline-size: 100%;
  max-block-size: var(--sheet-max-height, 85dvh);
  margin: 0 auto;
  padding: var(--surface-pad);
  padding-block: calc(var(--surface-pad) + 0.75rem) calc(var(--surface-pad) + env(safe-area-inset-bottom, 0px));
  overflow: auto;
  overscroll-behavior: contain;
  background: var(--sheet-bg, var(--modal-bg, var(--panel-bg, var(--surface))));
  color: var(--on-surface);
  border: var(--panel-border-width, 1px) solid var(--sheet-border, var(--modal-border, var(--border)));
  border-block-end: 0;
  border-radius: var(--sheet-radius, calc(var(--radius) * 3)) var(--sheet-radius, calc(var(--radius) * 3)) 0 0;
  box-shadow: var(--sheet-shadow, 0 -4px 24px rgba(0, 0, 0, 0.3));
  overflow-wrap: anywhere;
}
.sheet::before {
  content: "";
  position: absolute;
  top: 0.5rem;
  left: 50%;
  translate: -50% 0;
  inline-size: var(--sheet-handle-width, 2.25rem);
  block-size: 0.3rem;
  border-radius: 999px;
  background: var(--sheet-handle, var(--border));
}
dialog.sheet:modal,
.sheet[popover]:popover-open {
  position: fixed;
  inset: auto 0 0 0;
  margin: 0 auto;
  color: var(--on-surface);
}
.sheet[popover]:not(:popover-open) { display: none; }
.sheet::backdrop {
  background: var(--overlay-bg, rgba(0, 0, 0, 0.6));
  backdrop-filter: var(--overlay-blur, none);
}
.sheet-header {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  margin-block-end: var(--space-s);
  color: var(--sheet-header-fg, inherit);
  font-weight: 600;
}
.sheet-header > .btn-close { margin-inline-start: auto; }
.sheet .sheet-header > :is(h1, h2, h3, h4, h5, h6) { margin: 0; font: inherit; font-size: 1.05em; color: inherit; }
@media (prefers-reduced-motion: no-preference) {
  dialog.sheet,
  .sheet[popover] { transition: translate 0.25s ease, display 0.25s allow-discrete, overlay 0.25s allow-discrete; }
  @starting-style {
    dialog.sheet[open],
    .sheet[popover]:popover-open { translate: 0 100%; }
  }
}

  :root[data-motion="always"] dialog.sheet,:root[data-motion="always"] .sheet[popover] { transition: translate 0.25s ease, display 0.25s allow-discrete, overlay 0.25s allow-discrete; }
  @starting-style {
    :root[data-motion="always"] dialog.sheet[open],:root[data-motion="always"] .sheet[popover]:popover-open { translate: 0 100%; }
  }


/* --- Collapsible nav rail (#43) ------------------------------------------------
 * .app-rail stays decorative (themes paint it, LCARS even writes labels in
 * it), so navigation gets its own element built from .nav-item:
 *   <nav class="nav-rail" aria-label="Sections">
 *     <a class="nav-item" href="#" aria-current="page"><svg class="icon">…</svg><span>Inbox</span></a>…
 *   </nav>
 * Full on desktop and XL; icon-only up to 900px (labels stay as the
 * accessible name). .is-collapsed forces icon-only, .is-expanded forces
 * labels, for an app's own toggle. */
.nav-rail {
  display: flex;
  flex-direction: column;
  gap: var(--space-3xs);
  flex: none;
  inline-size: var(--nav-rail-width, 14rem);
  max-inline-size: 100%;
  padding: var(--nav-rail-pad, var(--space-xs));
  overflow: hidden auto;
  background: var(--nav-rail-bg, var(--surface));
  border-inline-end: var(--nav-rail-rule-width, 1px) solid var(--nav-rail-rule, var(--border));
}
.nav-rail > .nav-item {
  position: relative;
  display: flex;
  align-items: center;
  gap: 0.6em;
  min-inline-size: 0;
  border-inline-start: 3px solid transparent;
  white-space: nowrap;
}
.nav-rail > .nav-item > :not(.icon, .badge) { min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; }
.nav-rail > .nav-item > .badge { margin-inline-start: auto; }
.nav-rail > .nav-item:is(:is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))), [aria-current]:not([aria-current="false"])) {
  background: var(--nav-item-bg-active, var(--surface-2));
  color: var(--nav-item-fg-active, var(--text));
  border-inline-start-color: var(--nav-rail-marker, var(--accent));
}
.nav-rail > .nav-item:focus-visible { outline-offset: -2px; }
.nav-rail > :is(h2, h3, h4, .nav-rail-heading) {
  margin: var(--space-xs) 0 0;
  padding-inline: 0.7em;
  color: var(--muted);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: var(--label-tracking, normal);
  text-transform: var(--label-transform, none);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.nav-rail.is-collapsed {
  inline-size: var(--nav-rail-collapsed-width, calc(max(var(--tap-min), 2.75rem) + 2 * var(--space-xs) + 1px));
  padding-inline: var(--space-xs);
}
.nav-rail.is-collapsed > .nav-item { justify-content: center; padding-inline: 0; min-inline-size: max(var(--tap-min), 2.75rem); }
.nav-rail.is-collapsed > .nav-item > :not(.icon, .badge),
.nav-rail.is-collapsed > :is(h2, h3, h4, .nav-rail-heading) {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.nav-rail.is-collapsed > .nav-item > .badge { position: absolute; top: 0; inset-inline-end: 0; margin: 0; font-size: 0.6rem; }
@media (max-width: 900px) {
  .nav-rail:not(.is-expanded) {
    inline-size: var(--nav-rail-collapsed-width, calc(max(var(--tap-min), 2.75rem) + 2 * var(--space-xs) + 1px));
    padding-inline: var(--space-xs);
  }
  .nav-rail:not(.is-expanded) > .nav-item { justify-content: center; padding-inline: 0; min-inline-size: max(var(--tap-min), 2.75rem); }
  .nav-rail:not(.is-expanded) > .nav-item > :not(.icon, .badge),
  .nav-rail:not(.is-expanded) > :is(h2, h3, h4, .nav-rail-heading) {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  .nav-rail:not(.is-expanded) > .nav-item > .badge { position: absolute; top: 0; inset-inline-end: 0; margin: 0; font-size: 0.6rem; }
}

/* --- Stepper / wizard progress (#44) -------------------------------------------
 *   <ol class="steps">
 *     <li class="is-done">Account</li>
 *     <li aria-current="step">Billing <small>Card or invoice</small></li>
 *     <li class="is-error">Shipping</li>
 *     <li>Review</li>
 *   </ol>
 * Upcoming is the default. Each marker carries its state as a glyph
 * (number, tick, "!") as well as colour. Horizontal; vertical with
 * .is-vertical, and always in a box or screen 480px wide or less. */
.steps {
  display: flex;
  gap: var(--steps-gap, var(--space-xs));
  counter-reset: step;
  --steps-marker: var(--steps-marker-size, 1.75rem);
}
.steps > li {
  position: relative;
  flex: 1 1 0;
  min-inline-size: 0;
  counter-increment: step;
  color: var(--steps-fg, var(--muted));
  font-size: 0.85em;
  text-align: center;
  overflow-wrap: anywhere;
}
.steps > li::before {
  content: counter(step);
  display: grid;
  place-items: center;
  box-sizing: border-box;
  inline-size: var(--steps-marker);
  block-size: var(--steps-marker);
  margin: 0 auto var(--space-2xs);
  border: 2px solid var(--steps-marker-border, var(--muted));
  border-radius: var(--steps-marker-radius, 50%);
  background: var(--steps-marker-bg, var(--surface));
  color: inherit;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
/* Connector from this marker to the next one. */
.steps > li:not(:last-child)::after {
  content: "";
  position: absolute;
  top: calc(var(--steps-marker) / 2 - 1px);
  inset-inline-start: calc(50% + var(--steps-marker) / 2 + 0.25rem);
  inset-inline-end: calc(-50% - var(--steps-gap, var(--space-xs)) + var(--steps-marker) / 2 + 0.25rem);
  block-size: 2px;
  background: var(--steps-line, var(--border));
}
.steps small { display: block; color: var(--muted); font-size: 0.85em; }
.steps > li > a { position: relative; display: inline-block; color: inherit; touch-action: manipulation; }
.steps > li > a::before {
  content: "";
  position: absolute;
  left: -0.5rem;
  right: -0.5rem;
  top: calc((1em - var(--tap-min)) / 2);
  bottom: calc((1em - var(--tap-min)) / 2);
}
.steps > li.is-done { color: var(--steps-fg-done, var(--text)); }
.steps > li.is-done::before {
  content: var(--steps-done-glyph, "\2713");
  background: var(--steps-done-bg, var(--accent));
  border-color: var(--steps-done-bg, var(--accent));
  color: var(--steps-done-fg, var(--on-accent));
}
.steps > li.is-done::after { background: var(--steps-line-done, var(--accent)); }
.steps > li[aria-current="step"] { color: var(--steps-fg-current, var(--text)); font-weight: 600; }
.steps > li[aria-current="step"]::before {
  border-color: var(--steps-current, var(--accent));
  color: var(--steps-current-fg, var(--accent-text, var(--accent)));
  box-shadow: 0 0 0 3px var(--steps-current-halo, transparent);
}
.steps > li.is-error { color: var(--danger-text, var(--danger)); }
.steps > li.is-error::before {
  content: var(--steps-error-glyph, "!");
  background: var(--danger);
  border-color: var(--danger);
  color: var(--on-danger);
}
/* Vertical: markers down the inline-start edge, the label beside them. */
.steps.is-vertical { flex-direction: column; gap: 0; }
.steps.is-vertical > li {
  flex: none;
  min-block-size: calc(var(--steps-marker) + var(--space-m));
  padding-inline-start: calc(var(--steps-marker) + var(--space-s));
  padding-block: 0.3em var(--space-m);
  text-align: start;
}
.steps.is-vertical > li::before { position: absolute; top: 0; inset-inline-start: 0; margin: 0; }
.steps.is-vertical > li:not(:last-child)::after {
  top: calc(var(--steps-marker) + 2px);
  bottom: 2px;
  inset-inline: calc(var(--steps-marker) / 2 - 1px) auto;
  inline-size: 2px;
  block-size: auto;
}
@container (max-width: 480px) {
  .steps { flex-direction: column; gap: 0; }
  .steps > li { flex: none; min-block-size: calc(var(--steps-marker) + var(--space-m)); padding-inline-start: calc(var(--steps-marker) + var(--space-s)); padding-block: 0.3em var(--space-m); text-align: start; }
  .steps > li::before { position: absolute; top: 0; inset-inline-start: 0; margin: 0; }
  .steps > li:not(:last-child)::after { top: calc(var(--steps-marker) + 2px); bottom: 2px; inset-inline: calc(var(--steps-marker) / 2 - 1px) auto; inline-size: 2px; block-size: auto; }
}
@media (max-width: 480px) {
  .steps { flex-direction: column; gap: 0; }
  .steps > li { flex: none; min-block-size: calc(var(--steps-marker) + var(--space-m)); padding-inline-start: calc(var(--steps-marker) + var(--space-s)); padding-block: 0.3em var(--space-m); text-align: start; }
  .steps > li::before { position: absolute; top: 0; inset-inline-start: 0; margin: 0; }
  .steps > li:not(:last-child)::after { top: calc(var(--steps-marker) + 2px); bottom: 2px; inset-inline: calc(var(--steps-marker) / 2 - 1px) auto; inline-size: 2px; block-size: auto; }
}

/* --- Command palette (#45) -------------------------------------------------------
 *   <dialog class="command" aria-label="Command palette">
 *     <div class="command-search"><svg class="icon">…</svg>
 *       <input class="input" type="search" role="combobox" aria-controls="r" aria-expanded="true"
 *              aria-activedescendant="o1" placeholder="Type a command"></div>
 *     <div class="command-list" role="listbox" id="r">
 *       <ul role="group" aria-labelledby="g1"><li role="presentation" id="g1">Files</li>
 *         <li role="option" id="o1" aria-selected="true">Open file… <kbd>Ctrl+O</kbd></li></ul>
 *     </div>
 *     <div class="command-footer"><span><kbd>↑</kbd><kbd>↓</kbd> move</span>…</div>
 *   </dialog>
 * The app filters and moves aria-selected; <mark> highlights the match. */
.command {
  inline-size: min(var(--command-width, 40rem), calc(100vw - 2rem));
  max-inline-size: calc(100vw - 2rem);
  padding: 0;
  overflow: hidden;
  background: var(--command-bg, var(--modal-bg, var(--panel-bg, var(--surface))));
  color: var(--on-surface);
  border: var(--panel-border-width, 1px) solid var(--command-border, var(--modal-border, var(--border)));
  border-radius: var(--command-radius, var(--modal-radius, var(--panel-radius, var(--radius))));
  box-shadow: var(--command-shadow, var(--modal-shadow, 0 12px 40px rgba(0, 0, 0, 0.45)));
}
dialog.command:modal,
.command[popover]:popover-open {
  position: fixed;
  inset: 0;
  margin: var(--command-top, 12dvh) auto auto;
  block-size: fit-content;
  max-block-size: calc(100dvh - 2 * var(--command-top, 12dvh));
}
.command[popover]:not(:popover-open) { display: none; }
.command::backdrop {
  background: var(--overlay-bg, rgba(0, 0, 0, 0.6));
  backdrop-filter: var(--overlay-blur, none);
}
@media (max-width: 480px) {
  .command { --command-top: var(--space-s); }
}
.command-search {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  padding: var(--space-2xs) var(--space-m);
  border-block-end: 1px solid var(--command-rule, var(--hairline));
  color: var(--muted);
  --input-bg: transparent;
  --input-border: transparent;
  --input-shadow: none;
}
.command-search > .input {
  flex: 1;
  min-inline-size: 0;
  padding-inline: 0;
  color: var(--text);
  font-size: 1.05em;
}
.command-search > .input:focus-visible { outline-offset: -2px; }
.command-list {
  max-block-size: var(--command-list-max, min(22rem, 55dvh));
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--space-2xs);
}
.command-list [role="group"] + [role="group"] {
  margin-block-start: var(--space-2xs);
  padding-block-start: var(--space-2xs);
  border-block-start: 1px solid var(--command-rule, var(--hairline));
}
.command-list [role="group"] > [role="presentation"] {
  padding: 0.4em 0.75em 0.2em;
  color: var(--muted);
  font-size: 0.75em;
  font-weight: 600;
  letter-spacing: var(--label-tracking, normal);
  text-transform: var(--label-transform, none);
}
.command-list [role="option"] {
  display: flex;
  align-items: center;
  gap: 0.6em;
  min-block-size: var(--tap-min);
  padding: 0.35em 0.75em;
  border-inline-start: 3px solid transparent;
  border-radius: var(--command-item-radius, var(--radius));
  color: var(--text);
  touch-action: manipulation;
}
.command-list [role="option"] > :is(kbd, .command-hint) { margin-inline-start: auto; }
.command-list [role="option"] > .command-hint { color: var(--muted); font-size: 0.8em; white-space: nowrap; }
.command-list [role="option"] > .command-hint + kbd { margin-inline-start: 0; }
.command-list [role="option"]:is([aria-selected="true"], :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))) {
  background: var(--command-item-bg-active, var(--dropdown-item-bg-hover, var(--accent)));
  color: var(--command-item-fg-active, var(--dropdown-item-fg-hover, var(--on-accent)));
  border-inline-start-color: currentColor;
}
.command-list [role="option"]:is([aria-selected="true"], :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))) > .command-hint { color: inherit; }
.command-list [role="option"]:is([aria-disabled="true"]) { opacity: 0.5; }
@media (hover: hover) {
  .command-list [role="option"]:not(:is([aria-selected="true"], :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked)))):hover { background: var(--row-hover-bg, rgba(128, 128, 128, 0.12)); }
}
.command mark { background: none; color: inherit; font-weight: 700; text-decoration: underline; text-underline-offset: 0.15em; }
.command-footer {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3xs) var(--space-m);
  padding: var(--space-xs) var(--space-m);
  border-block-start: 1px solid var(--command-rule, var(--hairline));
  color: var(--muted);
  font-size: 0.75em;
}
.command-footer kbd { margin-inline-end: 0.25em; }

/* --- Split pane (#46) -----------------------------------------------------------
 *   <div class="split"><section>…</section><section>…</section></div>
 * The first pane resizes with the browser's own grip (CSS resize, bottom
 * corner); its start size is --split-size. For a full-height drag bar the
 * app drives, put <div class="split-handle" role="separator" tabindex="0"
 * aria-orientation="vertical" aria-valuenow="40" aria-label="Resize">
 * between the panes and set --split-size from JS. .is-vertical stacks the
 * panes top and bottom. Side-by-side panes stack in 480px or less. */
.split {
  display: flex;
  min-block-size: var(--split-min-height, 12rem);
  border: var(--split-border-width, 1px) solid var(--split-border, var(--border));
  border-radius: var(--split-radius, var(--radius));
}
.split > :not(.split-handle) { min-inline-size: 0; overflow: auto; overflow-wrap: anywhere; padding: var(--split-pad, var(--space-s)); }
.split > :first-child {
  flex: none;
  inline-size: var(--split-size, 50%);
  min-inline-size: var(--split-min, 6rem);
  max-inline-size: calc(100% - var(--split-min, 6rem));
  resize: horizontal;
  border-inline-end: var(--split-rule-width, 1px) solid var(--split-rule, var(--border));
}
.split > :last-child { flex: 1 1 0; }
.split:has(> .split-handle) > :first-child { resize: none; border-inline-end: 0; }
.split-handle {
  position: relative;
  flex: none;
  inline-size: var(--split-handle-size, 0.5rem);
  background: var(--split-handle-bg, var(--surface-2));
  border-inline: 1px solid var(--split-rule, var(--border));
  cursor: var(--cursor-resize-x, col-resize);
  touch-action: none;
}
/* The grip mark, and a hit area at least --tap-min wide. */
.split-handle::after {
  content: "";
  position: absolute;
  inset: 50% auto auto 50%;
  translate: -50% -50%;
  inline-size: 2px;
  block-size: 1.5rem;
  border-radius: 1px;
  background: var(--split-grip, var(--muted));
}
.split-handle::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc((100% - var(--tap-min)) / 2);
  right: calc((100% - var(--tap-min)) / 2);
  z-index: var(--z-raised);
}
.split-handle:is(:focus-visible, .is-dragging) { background: var(--split-handle-bg-active, var(--accent)); }
.split-handle:is(:focus-visible, .is-dragging)::after { background: var(--split-grip-active, var(--on-accent)); }
@media (hover: hover) {
  .split-handle:hover { background: var(--split-handle-bg-active, var(--accent)); }
  .split-handle:hover::after { background: var(--split-grip-active, var(--on-accent)); }
}
.split.is-vertical { flex-direction: column; block-size: var(--split-height, 24rem); }
.split.is-vertical > :first-child {
  inline-size: auto;
  max-inline-size: none;
  block-size: var(--split-size, 50%);
  min-block-size: var(--split-min, 4rem);
  max-block-size: calc(100% - var(--split-min, 4rem));
  resize: vertical;
  border-inline-end: 0;
  border-block-end: var(--split-rule-width, 1px) solid var(--split-rule, var(--border));
}
.split.is-vertical:has(> .split-handle) > :first-child { border-block-end: 0; }
.split.is-vertical > .split-handle {
  inline-size: auto;
  block-size: var(--split-handle-size, 0.5rem);
  border-inline: 0;
  border-block: 1px solid var(--split-rule, var(--border));
  cursor: var(--cursor-resize-y, row-resize);
}
.split.is-vertical > .split-handle::after { inline-size: 1.5rem; block-size: 2px; }
.split.is-vertical > .split-handle::before {
  left: 0;
  right: 0;
  top: calc((100% - var(--tap-min)) / 2);
  bottom: calc((100% - var(--tap-min)) / 2);
}
@container (max-width: 480px) {
  .split:not(.is-vertical) { flex-direction: column; }
  .split:not(.is-vertical) > :first-child { inline-size: auto; max-inline-size: none; resize: none; border-inline-end: 0; border-block-end: var(--split-rule-width, 1px) solid var(--split-rule, var(--border)); }
  .split:not(.is-vertical) > .split-handle { display: none; }
  .split:not(.is-vertical) > :last-child { flex: 1 0 auto; }
}
@media (max-width: 480px) {
  .split:not(.is-vertical) { flex-direction: column; }
  .split:not(.is-vertical) > :first-child { inline-size: auto; max-inline-size: none; resize: none; border-inline-end: 0; border-block-end: var(--split-rule-width, 1px) solid var(--split-rule, var(--border)); }
  .split:not(.is-vertical) > .split-handle { display: none; }
  .split:not(.is-vertical) > :last-child { flex: 1 0 auto; }
}

/* --- Keyboard layer (§20) ----------------------------------------------------- */

/* A key combination: <kbd><kbd>Ctrl</kbd>+<kbd>S</kbd></kbd>. The outer
   kbd is the whole input; only the keys are boxed. */
kbd:has(> kbd) {
  padding: 0;
  border: 0;
  background: none;
  box-shadow: none;
  color: inherit;
  font-family: inherit;
  font-size: inherit;
}
kbd kbd { font-size: 0.9em; }
/* Shortcut hints inside menu items read as plain text at the item's end,
   the way desktop menus print them; they follow the item's own colour on
   hover and focus. */
:is(.context-menu, .menubar, .dropdown) kbd {
  margin-inline-start: auto;
  padding: 0;
  padding-inline-start: 1.5rem;
  border: 0;
  background: none;
  box-shadow: none;
  color: var(--menu-kbd-fg, inherit);
  font-size: 0.85em;
}
/* Inside a button or a command result: a quiet outlined key that follows
   the control's text colour. */
:is(.btn, .tab, .command-list, .command-search) kbd {
  padding: 0.1em 0.35em;
  border-color: var(--kbd-hint-border, currentColor);
  background: var(--kbd-hint-bg, transparent);
  box-shadow: none;
  color: inherit;
  font-size: 0.75em;
  line-height: 1.5;
}
.command-footer kbd { font-size: 0.95em; }
/* Attribute tooltips can't hold markup, so a shortcut rides in its own
   attribute: <button data-tooltip="Save" data-shortcut="Ctrl+S">. The app
   picks the platform's symbols (⌘S vs Ctrl+S). */
[data-tooltip][data-shortcut]::after { content: attr(data-tooltip) "  " attr(data-shortcut); }

/* .shortcuts — the "?" overlay listing the app's shortcuts. Put it on the
   dialog itself (<dialog class="modal modal-lg shortcuts">) or any box;
   groups are <section>s of a <dl> (name in <dt>, keys in <dd>). */
.shortcuts {
  --shortcuts-col: 16rem;
}
.shortcuts-groups {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, var(--shortcuts-col)), 1fr));
  gap: var(--space-m) var(--space-l);
}
.shortcuts :is(h2, h3, h4):not(.modal-header > *) {
  margin: 0 0 var(--space-2xs);
  color: var(--muted);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: var(--label-tracking, normal);
  text-transform: var(--label-transform, none);
}
.shortcuts dl {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  margin: 0;
  font-size: 0.9em;
}
.shortcuts :is(dt, dd) {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.3em;
  margin: 0;
  padding-block: var(--space-2xs);
  border-block-end: 1px solid var(--shortcuts-rule, var(--hairline));
}
.shortcuts dd { justify-content: flex-end; padding-inline-start: var(--space-s); white-space: nowrap; }

/* Roving focus (§20): composite widgets move focus with the arrow keys
   (one tabindex="0" item, the rest -1). Inside boxes that clip or scroll,
   the ring is drawn inset so it is never cut off. */
:is(.list, .tree, .menubar, .context-menu, .dropdown, .command-list, .nav-rail, .tabbar, .tabs, .segmented,
    .toolbar, .btn-group, .table-wrap, [role="grid"], [role="listbox"], [role="toolbar"], [role="tablist"]) :focus-visible {
  outline-offset: -2px;
}
/* A list row that takes roving focus is a target like a button row. */
.list-item[tabindex] { min-block-size: var(--tap-min); }
.table :is(td, th):focus-visible,
[role="row"]:focus-visible { outline-offset: -2px; }
/* --- v5 operations kit: status pins, sketch maps, routes, feeds, barcodes ---
 * Small additions the operations pages (smart home, inventory, fleet) need
 * on top of instruments.css and surfaces.css:
 *   [data-status]          ok | warn | error | idle | info → --series, so a
 *                          map pin, legend item or chart series takes a
 *                          status colour instead of a chart colour
 *   .map-pin[data-shape]   circle | square | diamond | triangle markers, so
 *                          status reads by shape as well as colour
 *   .map .map-land …       token-filled hooks for a schematic map drawn in
 *                          inline SVG (water, park, block, road, major road)
 *   .map-route             a route line in a map SVG (.is-planned, .is-done)
 *   .camera-feed                  a camera/video feed frame: placeholder scene,
 *                          scanlines, a corner badge and a caption strip
 *   .barcode               a CSS-drawn 1D barcode with its human-readable text
 * Everything reads tokens with core fallbacks. See docs/components/ops.md. */

/* --- Status colours ------------------------------------------------------- */
[data-status="ok"] { --series: var(--status-ok, var(--success)); }
[data-status="warn"] { --series: var(--status-warn, var(--warning)); }
[data-status="error"] { --series: var(--status-error, var(--danger)); }
[data-status="idle"] { --series: var(--status-idle, var(--muted)); }
[data-status="info"] { --series: var(--status-info, var(--accent)); }

/* --- Shaped map pins -------------------------------------------------------
 * A shaped pin is a marker centred on its point (not a teardrop above it).
 * The triangle draws its outline with ::after under the fill (clip-path
 * would cut a border off). Legend items take the same shapes. */
.map-pin[data-shape] { translate: -50% -50%; place-items: center; }
.map-pin[data-shape]::before { margin: 0; rotate: none; grid-area: 1 / 1; }
.map-pin[data-shape="circle"]::before { border-radius: 50%; }
.map-pin[data-shape="square"]::before { border-radius: var(--map-pin-square-radius, 3px); }
.map-pin[data-shape="diamond"]::before { border-radius: var(--map-pin-square-radius, 3px); rotate: 45deg; scale: 0.85; }
.map-pin[data-shape="triangle"]::before,
.map-pin[data-shape="triangle"]::after {
  clip-path: polygon(50% 4%, 100% 92%, 0 92%);
  border: 0;
  border-radius: 0;
}
.map-pin[data-shape="triangle"]::before {
  z-index: var(--z-raised);
  scale: 0.72;
  translate: 0 8%;
  background: var(--series, var(--map-pin-bg, var(--accent)));
}
.map-pin[data-shape="triangle"]::after {
  content: "";
  grid-area: 1 / 1;
  inline-size: calc(var(--map-pin-size, 1.4rem) * 1.25);
  block-size: calc(var(--map-pin-size, 1.4rem) * 1.25);
  background: var(--map-pin-border, var(--surface));
}
:is(.legend, .chart-legend) > li[data-shape="circle"]::before { border-radius: 50%; }
:is(.legend, .chart-legend) > li[data-shape="diamond"]::before { rotate: 45deg; scale: 0.8; }
:is(.legend, .chart-legend) > li[data-shape="triangle"]::before { border-radius: 0; clip-path: polygon(50% 0, 100% 100%, 0 100%); }

/* --- Sketch map hooks ------------------------------------------------------
 * For a schematic map drawn as the .map's inline <svg> (no tiles). */
.map .map-land { fill: var(--map-land, var(--surface-2)); }
.map .map-water { fill: var(--map-water, color-mix(in oklab, var(--chart-series-1, var(--accent)) 28%, var(--surface-2))); }
.map .map-park { fill: var(--map-park, color-mix(in oklab, var(--success) 22%, var(--surface-2))); }
.map .map-block { fill: var(--map-block, color-mix(in oklab, var(--text) 9%, var(--surface-2))); }
.map .map-road {
  fill: none;
  stroke: var(--map-road, color-mix(in oklab, var(--muted) 40%, var(--surface-2)));
  stroke-width: var(--map-road-width, 6px);
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}
.map .map-road.is-major {
  stroke: var(--map-road-major, color-mix(in oklab, var(--warning) 45%, var(--surface-2)));
  stroke-width: var(--map-road-major-width, 10px);
}
.map .map-label { fill: var(--map-label-fg, var(--muted)); font-size: var(--map-label-size, 11px); font-family: var(--font); }

/* --- Route lines ----------------------------------------------------------- */
.map-route {
  fill: none;
  stroke: var(--series, var(--map-route, var(--accent)));
  stroke-width: var(--map-route-width, 4px);
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}
.map-route.is-planned { stroke-dasharray: var(--map-route-dash, 2px 7px); }
.map-route.is-done { stroke-opacity: var(--map-route-done-opacity, 0.45); }

/* --- Feed ------------------------------------------------------------------
 *   <div class="card-media camera-feed" role="img" aria-label="Front door camera, live">
 *     <svg …>silhouettes in currentColor</svg>
 *     <span class="badge badge-danger camera-feed-badge">Live</span>
 *     <span class="camera-feed-caption">Front door <time>14:22:07</time></span>
 *   </div> */
.camera-feed {
  position: relative;
  overflow: hidden;
  aspect-ratio: var(--camera-feed-ratio, 16 / 9);
  color: var(--camera-feed-fg, color-mix(in oklab, var(--text) 30%, var(--surface-2)));
  background: var(--camera-feed-bg,
    linear-gradient(color-mix(in oklab, var(--muted) 30%, var(--surface-2)) 0 58%,
                    color-mix(in oklab, var(--text) 14%, var(--surface-2)) 58%));
}
.camera-feed > svg { position: absolute; inset: 0; inline-size: 100%; block-size: 100%; }
.camera-feed::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    repeating-linear-gradient(transparent 0 2px, var(--camera-feed-scanline, rgb(0 0 0 / 0.07)) 2px 3px),
    radial-gradient(ellipse at center, transparent 55%, var(--camera-feed-vignette, rgb(0 0 0 / 0.35)));
}
.camera-feed.is-offline { filter: grayscale(1); }
.camera-feed.is-offline > svg { opacity: 0.25; }
.camera-feed-badge {
  position: absolute;
  z-index: var(--z-raised);
  inset-block-start: var(--space-xs);
  inset-inline-start: var(--space-xs);
}
.camera-feed-caption {
  position: absolute;
  z-index: var(--z-raised);
  inset-inline: 0;
  inset-block-end: 0;
  display: flex;
  justify-content: space-between;
  gap: var(--space-xs);
  padding: var(--space-2xs) var(--space-xs);
  background: var(--camera-feed-caption-bg, rgb(0 0 0 / 0.6));
  color: var(--camera-feed-caption-fg, #fff);
  font-family: var(--font-mono);
  font-size: 0.75rem;
}
@media (prefers-reduced-motion: no-preference) {
  .camera-feed-badge.is-live::before {
    content: "";
    display: inline-block;
    inline-size: 0.5em;
    block-size: 0.5em;
    margin-inline-end: 0.35em;
    border-radius: 50%;
    background: currentColor;
    animation: camera-feed-rec 1.4s steps(2, jump-none) infinite;
  }
}

  :root[data-motion="always"] .camera-feed-badge.is-live::before {
    content: "";
    display: inline-block;
    inline-size: 0.5em;
    block-size: 0.5em;
    margin-inline-end: 0.35em;
    border-radius: 50%;
    background: currentColor;
    animation: camera-feed-rec 1.4s steps(2, jump-none) infinite;
  }

html[data-motion="reduced"] .camera-feed-badge.is-live::before { animation: none; }
@keyframes camera-feed-rec { 50% { opacity: 0.15; } }

/* --- Barcode ---------------------------------------------------------------
 *   <span class="barcode" role="img" aria-label="Barcode INV-00421">INV-00421</span>
 * The stripes are decorative (ponytail: one fixed pattern, not an encoding;
 * render real codes as an SVG from a generator and keep this frame). */
.barcode {
  display: inline-grid;
  justify-items: center;
  gap: var(--space-2xs);
  padding: var(--space-xs) var(--space-m);
  background: var(--barcode-bg, var(--surface));
  color: var(--barcode-fg, var(--text));
  border: 1px solid var(--barcode-border, var(--border));
  border-radius: var(--barcode-radius, var(--radius));
  font-family: var(--font-mono);
  font-size: 0.8rem;
  letter-spacing: 0.2em;
}
.barcode::before {
  content: "";
  inline-size: var(--barcode-width, 12rem);
  max-inline-size: 100%;
  block-size: var(--barcode-height, 3.5rem);
  background: repeating-linear-gradient(90deg,
    currentColor 0 2px, transparent 0 4px, currentColor 0 5px, transparent 0 8px,
    currentColor 0 11px, transparent 0 12px, currentColor 0 13px, transparent 0 16px,
    currentColor 0 18px, transparent 0 19px, currentColor 0 22px, transparent 0 24px,
    currentColor 0 25px, transparent 0 28px, currentColor 0 29px, transparent 0 30px,
    currentColor 0 33px, transparent 0 35px, currentColor 0 36px, transparent 0 39px,
    currentColor 0 41px, transparent 0 43px, currentColor 0 44px, transparent 0 47px);
}

@media (forced-colors: active) {
  .map-route { stroke: CanvasText; }
  .barcode { forced-color-adjust: none; background: Canvas; color: CanvasText; }
}
/* --- v5 productivity kit (PLAN.md §22 "Productivity kit") -------------------
 * Kanban board, planner calendar (month, week and day views, agenda),
 * Gantt timeline, and the schedule list (extends core's .schedule badge).
 * Builds on core's .calendar frame (forms.css), .tabset view switch, .badge,
 * .avatar, .btn and the data-series colours (instruments.css). Apps move
 * cards, lay out events and draw connectors; this file styles every state.
 * Positions come in through documented inline APIs: --day/--span/--lane
 * (month bars), --start/--end/--col/--cols (timed events, Gantt bars),
 * --now, --at, --progress, --duration.
 * Docs: docs/components/planner.md. Example: planner.html.
 *
 * Private custom properties (--_*) are internal plumbing, not theme tokens. */

/* ===========================================================================
 * Kanban board
 * ======================================================================== */
.kanban {
  display: flex;
  align-items: flex-start;
  gap: var(--kanban-gap, var(--space-s));
  max-inline-size: 100%;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  scroll-padding-inline: var(--space-2xs);
  padding-block-end: var(--space-2xs);
  container-type: inline-size;
}
/* One column per screen on phones; the next one peeks in. */
@media (max-width: 480px) { .kanban { scroll-snap-type: x mandatory; } }

.kanban-column {
  flex: 0 0 var(--kanban-column-width, 17.5rem);
  min-inline-size: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  max-block-size: var(--kanban-column-max, none);
  padding: var(--kanban-column-pad, var(--space-s));
  background: var(--kanban-column-bg, var(--surface-2));
  color: var(--kanban-column-fg, var(--text));
  border: var(--kanban-column-border-width, 1px) solid var(--kanban-column-border, var(--hairline));
  border-block-start: var(--kanban-column-accent-width, 1px) solid var(--kanban-column-accent, var(--kanban-column-border, var(--hairline)));
  border-radius: var(--kanban-column-radius, var(--panel-radius, var(--radius)));
  scroll-snap-align: start;
}
@container (max-width: 480px) {
  .kanban-column { flex-basis: calc(100% - var(--space-xl)); }
}
.kanban-column-header {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  min-block-size: var(--tap-min);
}
.kanban-column-title {
  flex: 1;
  min-inline-size: 0;
  margin: 0;
  font-size: 1em;
  font-weight: 600;
  text-transform: var(--label-transform, none);
  overflow-wrap: anywhere;
}
.kanban-count { flex: none; font-variant-numeric: tabular-nums; }
.kanban-limit { color: var(--muted); font-weight: 400; }
.kanban-limit::before { content: "\2009/\2009"; }

/* Over the WIP limit: danger stripe, danger count and a "!" before it,
   so the state is not carried by colour alone. */
.kanban-column.is-over-limit {
  --kanban-column-accent: var(--kanban-over-limit, var(--danger));
  --kanban-column-accent-width: 3px;
}
.kanban-column.is-over-limit .kanban-count {
  --badge-bg: color-mix(in srgb, var(--kanban-over-limit, var(--danger)) 22%, var(--surface));
  --badge-fg: var(--danger-text, var(--text));
  --badge-border: var(--kanban-over-limit, var(--danger));
}
.kanban-column.is-over-limit .kanban-count::before { content: "!\2009"; font-weight: 800; }
.kanban-column.is-over-limit .kanban-limit { color: inherit; }

/* Drop target: the column the dragged card is over. */
.kanban-column.is-drop-target {
  background: var(--kanban-drop-bg, color-mix(in srgb, var(--accent) 12%, var(--kanban-column-bg, var(--surface-2))));
  outline: 2px dashed var(--kanban-drop-ring, var(--accent));
  outline-offset: -2px;
}

.kanban-cards {
  display: flex;
  flex-direction: column;
  gap: var(--kanban-card-gap, var(--space-xs));
  min-block-size: var(--kanban-empty-size, 3.5rem);
  margin: 0;
  padding: 0;
  list-style: none;
  overflow-y: auto;
  overscroll-behavior-y: contain;
}
/* An empty column still reads as a place to drop. */
.kanban-cards:empty {
  border: 1px dashed var(--kanban-column-border, var(--border));
  border-radius: var(--kanban-card-radius, var(--radius));
}

.kanban-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  min-inline-size: 0;
  padding: var(--kanban-card-pad, var(--space-s));
  background: var(--kanban-card-bg, var(--surface));
  color: var(--kanban-card-fg, var(--text));
  border: var(--kanban-card-border-width, 1px) solid var(--kanban-card-border, var(--border));
  border-radius: var(--kanban-card-radius, var(--radius));
  box-shadow: var(--kanban-card-shadow, none);
  overflow-wrap: anywhere;
  touch-action: manipulation;
}
.kanban-card[draggable="true"] { cursor: var(--cursor-grab, grab); }
.kanban-card:focus-within { border-color: var(--kanban-card-border-focus, var(--focus)); }
@media (hover: hover) {
  .kanban-card:hover { border-color: var(--kanban-card-border-hover, var(--accent)); }
}
.kanban-card-title { margin: 0; font-size: 1em; font-weight: 600; line-height: 1.35; }
.kanban-card-title a { color: inherit; }
.kanban-card > p { margin: 0; color: var(--muted); font-size: 0.875em; }

/* The card being dragged: lifted, tilted and accent-ringed. The app keeps
   it in the DOM (or uses it as the drag image) and adds .is-dragging. */
.kanban-card.is-dragging {
  cursor: var(--cursor-grabbing, grabbing);
  border-color: var(--kanban-drag-ring, var(--accent));
  box-shadow: var(--kanban-drag-shadow, 0 0.6rem 1.4rem color-mix(in srgb, #000 35%, transparent));
  rotate: var(--kanban-drag-tilt, 2deg);
  opacity: var(--kanban-drag-opacity, 1);
}
/* The slot a dropped card will land in. --kanban-placeholder-size can be
   set inline to the dragged card's height. */
.kanban-placeholder {
  position: relative;
  flex: none;
  block-size: var(--kanban-placeholder-size, 4.5rem);
  background: var(--kanban-placeholder-bg, color-mix(in srgb, var(--accent) 10%, transparent));
  border: 2px dashed var(--kanban-placeholder-border, var(--accent));
  border-radius: var(--kanban-card-radius, var(--radius));
}

.kanban-labels { display: flex; flex-wrap: wrap; gap: var(--space-3xs, 0.2rem); margin: 0; padding: 0; list-style: none; }
.kanban-labels > li { display: contents; }
.kanban-labels .badge[data-series] {
  --badge-bg: color-mix(in srgb, var(--series) 24%, var(--kanban-card-bg, var(--surface)));
  --badge-fg: var(--kanban-card-fg, var(--text));
  --badge-border: color-mix(in srgb, var(--series) 70%, transparent);
}

.kanban-card-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2xs) var(--space-xs);
  font-size: 0.8em;
  color: var(--muted);
}
.kanban-card-meta > :last-child:is(.kanban-assignees) { margin-inline-start: auto; }

.kanban-due {
  display: inline-flex;
  align-items: center;
  gap: 0.3em;
  font-variant-numeric: tabular-nums;
}
.kanban-due .icon { inline-size: 1em; block-size: 1em; flex: none; }
.kanban-due.is-soon { color: var(--kanban-due-soon, var(--warning-text, var(--text))); font-weight: 600; }
.kanban-due.is-overdue {
  padding: max(var(--space-2xs), 0.4em) 0.5em;
  background: var(--kanban-overdue-bg, color-mix(in srgb, var(--danger) 20%, var(--kanban-card-bg, var(--surface))));
  color: var(--kanban-overdue-fg, var(--danger-text, var(--text)));
  border: 1px solid var(--kanban-overdue-border, var(--danger));
  border-radius: var(--radius);
  font-weight: 700;
}

/* Priority: a glyph plus the word, so colour is never the only signal. */
.kanban-priority { display: inline-flex; align-items: center; gap: 0.3em; }
.kanban-priority::before {
  content: var(--_glyph, "\25C6");
  color: var(--_tone, var(--muted));
  font-size: 0.9em;
  line-height: 1;
}
.kanban-priority[data-priority="urgent"] { --_glyph: var(--kanban-priority-urgent-glyph, "\25B2\25B2"); --_tone: var(--danger); color: var(--danger-text, var(--text)); font-weight: 700; }
.kanban-priority[data-priority="high"] { --_glyph: var(--kanban-priority-high-glyph, "\25B2"); --_tone: var(--warning); }
.kanban-priority[data-priority="medium"] { --_glyph: var(--kanban-priority-medium-glyph, "\25C6"); --_tone: var(--accent); }
.kanban-priority[data-priority="low"] { --_glyph: var(--kanban-priority-low-glyph, "\25BC"); --_tone: var(--muted); }

.kanban-assignees { display: inline-flex; margin: 0; padding: 0; list-style: none; }
.kanban-assignees > li { display: contents; }
.kanban-assignees .avatar {
  --avatar-size: var(--kanban-avatar-size, 1.6rem);
  font-size: 0.75em;
  box-shadow: 0 0 0 2px var(--kanban-card-bg, var(--surface));
}
.kanban-assignees .avatar + .avatar,
.kanban-assignees > li + li > .avatar { margin-inline-start: -0.4rem; }

/* ===========================================================================
 * Planner calendar: month, week and day views
 * Reuses forms.css .calendar (frame, header, title, today/outside tokens).
 * ======================================================================== */
.calendar.is-planner {
  display: block;
  inline-size: auto;
  font-size: 1em;
  container-type: inline-size;
  --_line: var(--calendar-grid-line, var(--hairline));
}

/* Event chip, shared by the month bars and the timed grid. */
.calendar-event {
  --_ev: var(--series, var(--calendar-event-color, var(--accent)));
  display: block;
  min-inline-size: 0;
  padding: 0.3em 0.45em;
  background: var(--calendar-event-bg, color-mix(in srgb, var(--_ev) 24%, var(--calendar-bg, var(--surface))));
  color: var(--calendar-event-fg, var(--text));
  border: 0;
  border-inline-start: var(--calendar-event-bar, 3px) solid var(--_ev);
  border-radius: var(--calendar-event-radius, calc(var(--radius) * 0.75));
  font-size: 0.8em;
  line-height: 1.25;
  text-decoration: none;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  touch-action: manipulation;
}
.calendar-event time { font-variant-numeric: tabular-nums; font-weight: 600; }
@media (hover: hover) {
  .calendar-event:is(a, button):hover { background: var(--calendar-event-bg-hover, color-mix(in srgb, var(--_ev) 36%, var(--calendar-bg, var(--surface)))); }
}
.calendar-event:is(a, button):focus-visible { outline-offset: 1px; z-index: var(--z-raised); }
/* Tentative / cancelled: a pattern and a strike, not just a tint. */
.calendar-event.is-tentative {
  background: repeating-linear-gradient(135deg, color-mix(in srgb, var(--_ev) 24%, var(--calendar-bg, var(--surface))) 0 0.4em, var(--calendar-bg, var(--surface)) 0.4em 0.8em);
}
.calendar-event.is-cancelled { text-decoration: line-through; color: var(--calendar-event-cancelled-fg, inherit); }

/* --- Month view --------------------------------------------------------- */
.calendar-weekdays {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  color: var(--calendar-weekday-fg, var(--muted));
  font-size: 0.8em;
  font-weight: 600;
  text-align: center;
  text-transform: var(--label-transform, none);
}
.calendar-weekdays > * { padding-block: var(--space-2xs); }
.calendar-month { border-block-end: 1px solid var(--_line); }

/* A week row: seven day cells spanning every row, event bars laid over
   them in lanes. Row 1 holds the date, rows 2…lanes+1 the bars, the last
   row the "+n more" button. */
.calendar-week {
  --_lanes: var(--lanes, var(--calendar-lanes, 3));
  --_lane: max(var(--calendar-lane, 1.75em), var(--tap-min));
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  grid-template-rows: var(--calendar-date-row, 2.4em) repeat(var(--_lanes), var(--_lane)) minmax(var(--_lane), 1fr);
  row-gap: 2px;
  min-block-size: var(--calendar-week-min, 7.5em);
  padding-block-end: 2px;
  border-block-start: 1px solid var(--_line);
}
.calendar-cell {
  grid-row: 1 / -1;
  min-inline-size: 0;
  padding: var(--space-2xs);
  border-inline-start: 1px solid var(--_line);
}
.calendar-week > .calendar-cell:nth-child(1) { grid-column: 1; border-inline-start: 0; }
.calendar-week > .calendar-cell:nth-child(2) { grid-column: 2; }
.calendar-week > .calendar-cell:nth-child(3) { grid-column: 3; }
.calendar-week > .calendar-cell:nth-child(4) { grid-column: 4; }
.calendar-week > .calendar-cell:nth-child(5) { grid-column: 5; }
.calendar-week > .calendar-cell:nth-child(6) { grid-column: 6; }
.calendar-week > .calendar-cell:nth-child(7) { grid-column: 7; }
.calendar-cell.is-weekend { background: var(--calendar-weekend-bg, color-mix(in srgb, var(--surface-2) 45%, transparent)); }
.calendar-cell.is-outside {
  background: var(--calendar-outside-bg, color-mix(in srgb, var(--surface-2) 70%, transparent));
  color: var(--calendar-outside-fg, var(--muted));
}
.calendar-cell:is(.is-selected, :is([aria-selected="true"], :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked)))) {
  box-shadow: inset 0 0 0 2px var(--calendar-selected-bg, var(--accent));
}

/* The day number. Today: a filled disc (shape plus colour). */
.calendar-date {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: 2.1em;
  block-size: 2.1em;
  padding-inline: 0.3em;
  border-radius: var(--calendar-date-radius, 999px);
  font-size: 0.85em;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}
.calendar-date-month { margin-inline-start: 0.25em; white-space: nowrap; }
:is(.calendar-cell, .calendar-column-head)[aria-current="date"] .calendar-date {
  background: var(--calendar-today-bg, var(--calendar-today-ring, var(--accent)));
  color: var(--calendar-today-fg, var(--on-accent));
}

/* Bars: --day (1–7) and --span place them, --lane picks the row. A bar
   that runs on from the previous or into the next week squares that end. */
.calendar-week > .calendar-event {
  grid-column: var(--day, 1) / span var(--span, 1);
  grid-row: calc(var(--lane, 1) + 1);
  align-self: stretch;
  margin-inline: 3px;
  padding-block: 0;
  display: flex;
  align-items: center;
  gap: 0.35em;
}
.calendar-week > .calendar-event.is-continued-before { margin-inline-start: 0; border-start-start-radius: 0; border-end-start-radius: 0; border-inline-start-style: dashed; }
.calendar-week > .calendar-event.is-continued-after { margin-inline-end: 0; border-start-end-radius: 0; border-end-end-radius: 0; }
.calendar-week > .calendar-event.is-continued-after::after { content: "\2192"; margin-inline-start: auto; }
.calendar-week > .calendar-event.is-continued-after:dir(rtl)::after { content: "\2190"; }
/* A timed (not all-day) event in the month grid: a dot, no fill. */
.calendar-week > .calendar-event.is-timed {
  background: none;
  border-inline-start: 0;
}
.calendar-week > .calendar-event.is-timed::before {
  content: "";
  flex: none;
  inline-size: 0.55em;
  block-size: 0.55em;
  border-radius: 50%;
  background: var(--_ev);
}
.calendar-more {
  grid-column: var(--day, 1);
  grid-row: -2;
  align-self: stretch;
  justify-self: start;
  margin-inline: 3px;
  padding: 0 0.45em;
  background: none;
  border: 0;
  border-radius: var(--radius);
  color: var(--calendar-more-fg, var(--accent-text, var(--text)));
  font: inherit;
  font-size: 0.8em;
  font-weight: 600;
  white-space: nowrap;
  text-decoration: underline;
  touch-action: manipulation;
}
@media (hover: hover) {
  .calendar-more:hover { background: var(--calendar-day-bg-hover, var(--surface-2)); }
}

/* Narrow (phone) month: a compact grid with an event dot per busy day
   (data-events on the cell), and the agenda list below takes over. */
.calendar-agenda { display: none; }
@container (max-width: 480px) {
  .calendar-week {
    grid-template-rows: auto;
    min-block-size: 0;
    padding-block-end: 0;
  }
  .calendar-week > :is(.calendar-event, .calendar-more) { display: none; }
  .calendar-cell {
    display: grid;
    justify-items: center;
    align-content: start;
    gap: 2px;
    min-block-size: var(--tap-min);
    padding-inline: 0;
  }
  .calendar-cell[data-events]::after {
    content: "";
    inline-size: 0.4em;
    block-size: 0.4em;
    border-radius: 50%;
    background: var(--calendar-event-color, var(--accent));
  }
  .calendar-agenda { display: block; margin-block-start: var(--space-s); }
  /* "28 Sep" → "28": the month name stays in the accessibility tree. */
  .calendar-date-month { margin: 0; font-size: 0; }
}

/* --- Week and day views: the time grid -------------------------------- */
.calendar-timegrid {
  --_hour: var(--calendar-hour, max(3.5rem, calc(var(--tap-min) * 2)));
  --_from: var(--hour-start, 0);
  --_to: var(--hour-end, 24);
  display: grid;
  grid-template-columns: var(--calendar-gutter, 3.5rem) repeat(var(--days, 7), minmax(var(--calendar-column-min, 6rem), 1fr));
  max-block-size: var(--calendar-timegrid-max, none);
  overflow: auto;
  overscroll-behavior: contain;
  border-block: 1px solid var(--_line, var(--hairline));
}
.calendar-corner,
.calendar-column-head {
  grid-row: 1;
  position: sticky;
  inset-block-start: 0;
  z-index: var(--z-sticky);
  background: var(--calendar-bg, var(--surface));
  border-block-end: 1px solid var(--_line, var(--hairline));
}
.calendar-corner {
  grid-column: 1;
  inset-inline-start: 0;
  z-index: calc(var(--z-sticky) + 1);
  display: flex;
  align-items: end;
  padding: var(--space-2xs);
  color: var(--muted);
  font-size: 0.7em;
}
.calendar-column-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.3em;
  padding: var(--space-2xs);
  color: var(--calendar-weekday-fg, var(--muted));
  font-size: 0.85em;
  font-weight: 600;
  text-transform: var(--label-transform, none);
}
.calendar-column-head[aria-current="date"] { color: var(--calendar-today-text, var(--accent-text, var(--text))); }
.calendar-column-head .calendar-date { font-size: 1em; }

.calendar-hours {
  grid-row: 2;
  grid-column: 1;
  position: sticky;
  inset-inline-start: 0;
  z-index: var(--z-sticky);
  margin: 0;
  padding: 0;
  list-style: none;
  background: var(--calendar-bg, var(--surface));
  color: var(--muted);
  font-size: 0.72em;
  font-variant-numeric: tabular-nums;
  text-align: end;
}
.calendar-hours > li {
  block-size: var(--_hour);
  box-sizing: border-box;
  padding: 0.45em 0.5em 0;
  border-block-start: 1px solid var(--_line, var(--hairline));
}
.calendar-hours > li:first-child { border-block-start-color: transparent; }

.calendar-column {
  grid-row: 2;
  position: relative;
  min-inline-size: 0;
  block-size: calc((var(--_to) - var(--_from)) * var(--_hour));
  border-inline-start: 1px solid var(--_line, var(--hairline));
  background-image:
    repeating-linear-gradient(to bottom, var(--_line, var(--hairline)) 0 1px, transparent 1px var(--_hour)),
    repeating-linear-gradient(to bottom, transparent 0 calc(var(--_hour) / 2), var(--calendar-half-hour-line, color-mix(in srgb, var(--_line, var(--hairline)) 45%, transparent)) calc(var(--_hour) / 2) calc(var(--_hour) / 2 + 1px), transparent calc(var(--_hour) / 2 + 1px) var(--_hour));
}
.calendar-column.is-weekend { background-color: var(--calendar-weekend-bg, color-mix(in srgb, var(--surface-2) 45%, transparent)); }
.calendar-column[aria-current="date"] { background-color: var(--calendar-today-column-bg, color-mix(in srgb, var(--accent) 6%, transparent)); }

/* Timed events: --start/--end in hours (9.5 = 09:30), overlapping events
   side by side with --col (1-based) of --cols. */
.calendar-column > .calendar-event {
  position: absolute;
  inset-block-start: calc((var(--start, 0) - var(--_from)) * var(--_hour) + 1px);
  block-size: calc((var(--end, 1) - var(--start, 0)) * var(--_hour) - 2px);
  inset-inline-start: calc((var(--col, 1) - 1) * 100% / var(--cols, 1) + 1px);
  inline-size: calc(100% / var(--cols, 1) - 3px);
  box-sizing: border-box;
  white-space: normal;
  overflow-wrap: normal;
  box-shadow: 0 0 0 1px var(--calendar-bg, var(--surface));
}
.calendar-column > .calendar-event > time { margin-inline-start: 0.35em; white-space: nowrap; font-weight: 400; color: var(--muted); }

/* The current-time line at --now (hours), in today's column. */
.calendar-now {
  position: absolute;
  inset-inline: 0;
  inset-block-start: calc((var(--now, 0) - var(--_from)) * var(--_hour));
  z-index: var(--z-raised);
  block-size: 0;
  border-block-start: var(--calendar-now-width, 2px) solid var(--calendar-now, var(--danger));
  pointer-events: none;
}
.calendar-now::before {
  content: "";
  position: absolute;
  inset-inline-start: -0.35rem;
  inset-block-start: calc(-0.35rem - var(--calendar-now-width, 2px) / 2);
  inline-size: 0.7rem;
  block-size: 0.7rem;
  border-radius: 50%;
  background: var(--calendar-now, var(--danger));
}

/* ===========================================================================
 * Gantt timeline
 * ======================================================================== */
.gantt {
  --_unit: var(--gantt-unit, max(var(--tap-min), 2.25rem));
  --_row: var(--gantt-row, var(--_unit));
  --_units: var(--units, 14);
  --_line: var(--gantt-grid-line, var(--hairline));
  max-inline-size: 100%;
  max-block-size: var(--gantt-max, none);
  overflow: auto;
  overscroll-behavior-x: contain;
  background: var(--gantt-bg, var(--surface));
  color: var(--gantt-fg, var(--text));
  border: var(--gantt-border-width, 1px) solid var(--gantt-border, var(--border));
  border-radius: var(--gantt-radius, var(--panel-radius, var(--radius)));
  container-type: inline-size;
}
.gantt-header,
.gantt-body { --_list: var(--gantt-list-width, 13rem); }
@container (max-width: 480px) {
  .gantt-header,
  .gantt-body { --_list: var(--gantt-list-width-narrow, 8.5rem); }
}
.gantt-header,
.gantt-row {
  display: grid;
  grid-template-columns: var(--_list) calc(var(--_units) * var(--_unit));
  inline-size: max-content;
}
.gantt-header {
  position: sticky;
  inset-block-start: 0;
  z-index: calc(var(--z-sticky) + 1);
  background: var(--gantt-header-bg, var(--surface-2));
  box-shadow: inset 0 -1px 0 var(--gantt-border, var(--border));
}
.gantt-corner,
.gantt-task {
  position: sticky;
  inset-inline-start: 0;
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  min-inline-size: 0;
  padding-inline: var(--space-s);
  box-shadow: inset -1px 0 0 var(--gantt-border, var(--border));
}
.gantt-corner:dir(rtl),
.gantt-task:dir(rtl) { box-shadow: inset 1px 0 0 var(--gantt-border, var(--border)); }
.gantt-corner {
  z-index: var(--z-raised);
  background: var(--gantt-header-bg, var(--surface-2));
  color: var(--muted);
  font-size: 0.8em;
  font-weight: 600;
  text-transform: var(--label-transform, none);
}
.gantt-scale {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: var(--_unit);
  margin: 0;
  padding: 0;
  list-style: none;
  color: var(--muted);
  font-size: 0.72em;
  font-variant-numeric: tabular-nums;
}
.gantt-scale > li {
  grid-column: span var(--span, 1);
  display: grid;
  place-items: center;
  min-inline-size: 0;
  padding-block: var(--space-xs);
  box-shadow: inset 1px 0 0 var(--_line);
  line-height: 1.15;
  text-align: center;
  white-space: nowrap;
}
.gantt-scale > li.is-off { background: var(--gantt-off-bg, color-mix(in srgb, var(--surface) 55%, transparent)); }
.gantt-scale > li[aria-current="date"] {
  background: var(--gantt-today, var(--danger));
  color: var(--gantt-today-fg, var(--on-danger, #fff));
  font-weight: 700;
}

.gantt-body { position: relative; inline-size: max-content; }
.gantt-rows { margin: 0; padding: 0; list-style: none; }
.gantt-row { block-size: var(--_row); }
.gantt-task {
  z-index: var(--z-sticky);
  background: var(--gantt-bg, var(--surface));
  box-shadow: inset -1px 0 0 var(--gantt-border, var(--border)), inset 0 -1px 0 var(--_line);
  font-size: 0.875em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  padding-inline-start: calc(var(--space-s) + (var(--level, 1) - 1) * 1.1em);
}
.gantt-task:dir(rtl) { box-shadow: inset 1px 0 0 var(--gantt-border, var(--border)), inset 0 -1px 0 var(--_line); }
.gantt-task > :is(span, a) { overflow: hidden; text-overflow: ellipsis; }
.gantt-row.is-group > .gantt-task { font-weight: 700; }
.gantt-row:is([aria-selected="true"], :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))) > .gantt-task,
.gantt-row.is-selected > .gantt-task { background: var(--gantt-selected-bg, color-mix(in srgb, var(--accent) 16%, var(--gantt-bg, var(--surface)))); }

.gantt-track {
  position: relative;
  background-image: repeating-linear-gradient(to right, var(--_line) 0 1px, transparent 1px var(--_unit));
  box-shadow: inset 0 -1px 0 var(--_line);
}

/* Bars: --start and --end in time units from the left of the axis
   (fractions allowed), --progress 0–1 as a fill strip along the bottom. */
.gantt-bar {
  --_bar: var(--series, var(--gantt-bar, var(--accent)));
  position: absolute;
  inset-block-start: 50%;
  translate: 0 -50%;
  inset-inline-start: calc(var(--start, 0) * var(--_unit));
  inline-size: calc((var(--end, 1) - var(--start, 0)) * var(--_unit));
  min-inline-size: 0.5rem;
  block-size: var(--gantt-bar-size, max(66%, calc(1.45em + 14px)));
  box-sizing: border-box;
  display: flex;
  align-items: center;
  padding: 0 0.5em var(--gantt-progress-size, 0.25em);
  background:
    linear-gradient(var(--_bar), var(--_bar)) left bottom / calc(var(--progress, 0) * 100%) var(--gantt-progress-size, 0.25em) no-repeat,
    var(--gantt-bar-bg, color-mix(in srgb, var(--_bar) 26%, var(--gantt-bg, var(--surface))));
  color: var(--gantt-bar-fg, var(--text));
  border: 1px solid var(--_bar);
  border-radius: var(--gantt-bar-radius, var(--radius));
  font-size: 0.75em;
  line-height: 1.2;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  text-decoration: none;
}
.gantt-bar:dir(rtl) { background-position: right bottom, 0 0; }
/* As a link or button the bar's hit area is the full row height. */
.gantt-bar:is(a, button) { overflow: visible; font: inherit; font-size: 0.75em; touch-action: manipulation; }
.gantt-bar:is(a, button)::before { content: ""; position: absolute; inset-inline: 0; inset-block: calc((100% - var(--_row)) / 2); }
.gantt-bar:is(a, button) > span { overflow: hidden; text-overflow: ellipsis; }
@media (hover: hover) {
  .gantt-bar:is(a, button):hover { border-width: 2px; }
}
.gantt-bar.is-done { --gantt-bar-bg: color-mix(in srgb, var(--_bar) 14%, var(--gantt-bg, var(--surface))); color: var(--muted); }
.gantt-bar.is-critical { border-width: 2px; border-color: var(--gantt-critical, var(--danger)); }
.gantt-bar.is-late { border-style: dashed; border-color: var(--gantt-critical, var(--danger)); }
.gantt-bar.is-summary {
  block-size: 0.6rem;
  padding: 0;
  border-radius: 0;
  background: var(--_bar);
  clip-path: polygon(0 0, 100% 0, 100% 100%, calc(100% - 0.4rem) 55%, 0.4rem 55%, 0 100%);
}

/* Milestone: a diamond centred on --at. */
.gantt-milestone {
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: calc(var(--at, 0) * var(--_unit));
  inline-size: var(--gantt-milestone-size, 0.95rem);
  block-size: var(--gantt-milestone-size, 0.95rem);
  translate: -50% -50%;
  rotate: 45deg;
  background: var(--series, var(--gantt-milestone, var(--accent)));
  border: 2px solid var(--gantt-bg, var(--surface));
  outline: 1px solid var(--series, var(--gantt-milestone, var(--accent)));
}
.gantt-milestone:dir(rtl) { translate: 50% -50%; }
.gantt-milestone.is-done { background: var(--gantt-bg, var(--surface)); }
.gantt-milestone-label {
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: calc(var(--at, 0) * var(--_unit) + var(--gantt-milestone-size, 0.95rem));
  translate: 0 -50%;
  font-size: 0.75em;
  white-space: nowrap;
  color: var(--muted);
}

/* Today marker over the rows, at --at units. */
.gantt-today {
  position: absolute;
  inset-block: 0;
  inset-inline-start: calc(var(--_list) + var(--at, 0) * var(--_unit) - var(--gantt-today-width, 2px) / 2);
  z-index: var(--z-raised);
  inline-size: var(--gantt-today-width, 2px);
  background: var(--gantt-today, var(--danger));
  pointer-events: none;
}

/* Dependency connectors: an SVG over the time axis whose user units are
   the grid (x = time unit, y = row), so the app draws in data
   coordinates: viewBox="0 0 <units> <rows>" preserveAspectRatio="none".
   Strokes don't scale; cells are square by default (--gantt-row =
   --gantt-unit), so arrowheads keep their shape. */
.gantt-links {
  position: absolute;
  inset-block: 0;
  inset-inline-start: var(--_list);
  inline-size: calc(var(--_units) * var(--_unit));
  block-size: 100%;
  overflow: visible;
  pointer-events: none;
  color: var(--gantt-link, var(--muted));
}
.gantt-links:dir(rtl) { scale: -1 1; }
.gantt-link {
  fill: none;
  stroke: currentColor;
  stroke-width: var(--gantt-link-width, 1.5px);
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}
.gantt-link.is-critical { stroke: var(--gantt-critical, var(--danger)); stroke-width: calc(var(--gantt-link-width, 1.5px) + 1px); }
.gantt-link.is-blocked { stroke-dasharray: 4 3; }
.gantt-link-head { fill: currentColor; }
.gantt-link-head.is-critical { fill: var(--gantt-critical, var(--danger)); }

/* ===========================================================================
 * Schedule list: time-blocked agenda rows, built on core's .schedule badge
 * ======================================================================== */
.schedule-heading {
  margin: var(--space-s) 0 var(--space-2xs);
  color: var(--schedule-heading-fg, var(--muted));
  font-size: 0.8em;
  font-weight: 700;
  text-transform: var(--label-transform, none);
  letter-spacing: 0.02em;
}
.schedule-heading:first-child { margin-block-start: 0; }
.schedule-heading[aria-current="date"] { color: var(--accent-text, var(--text)); }
.schedule-list {
  display: flex;
  flex-direction: column;
  gap: var(--schedule-list-gap, var(--space-2xs));
  margin: 0;
  padding: 0;
  list-style: none;
}
/* --duration (hours) makes the block that tall (--schedule-hour per hour). */
.schedule-item {
  position: relative;
  --_ev: var(--series, var(--schedule-color, var(--accent-2, var(--accent))));
  display: grid;
  grid-template-columns: var(--schedule-time-width, 4.25em) minmax(0, 1fr) auto;
  align-items: start;
  gap: var(--space-2xs) var(--space-s);
  min-block-size: calc(var(--duration, 0) * var(--schedule-hour, 3rem));
  box-sizing: border-box;
  padding: var(--schedule-item-pad, var(--space-xs) var(--space-s));
  background: var(--schedule-item-bg, color-mix(in srgb, var(--_ev) 12%, var(--surface)));
  color: var(--text);
  border-inline-start: var(--schedule-bar-width, 3px) solid var(--_ev);
  border-radius: var(--schedule-item-radius, var(--radius));
  overflow-wrap: anywhere;
}
/* A row is a touch target only when it holds something to press. Keep one
   text node out of the gap between <time class="schedule"> and
   .schedule-body: a stray character becomes its own grid item. */
.schedule-item:has(a, button, .stretched-link, input) { min-block-size: max(var(--tap-min), calc(var(--duration, 0) * var(--schedule-hour, 3rem))); }
.schedule-item > .schedule {
  --schedule-font-size: 0.85em;
  flex-direction: column;
  align-items: flex-start;
  gap: 0;
  padding-block-start: 0.15em;
  color: var(--schedule-time-fg, var(--text));
  font-weight: 600;
}
.schedule-item > .schedule > .schedule-end { display: block; color: var(--muted); font-weight: 400; }
.schedule-body { min-inline-size: 0; }
.schedule-title { display: block; font-weight: 600; }
.schedule-title a { color: inherit; }
.schedule-meta { display: block; color: var(--muted); font-size: 0.85em; }
.schedule-item > :last-child:not(.schedule-body, .schedule) { align-self: center; }
.schedule-item.is-past { --schedule-item-bg: transparent; color: var(--muted); }
.schedule-item.is-past .schedule-title { font-weight: 400; }
.schedule-item[aria-current="time"],
.schedule-item.is-current {
  box-shadow: inset 0 0 0 2px var(--schedule-current-ring, var(--accent));
}
.schedule-item.is-free {
  --schedule-item-bg: transparent;
  border-inline-start-style: dashed;
  border-inline-start-color: var(--border);
  color: var(--muted);
}
.schedule-item.is-free .schedule-title { font-weight: 400; font-style: italic; }
.schedule-item.is-conflict { box-shadow: inset 0 0 0 1px var(--danger); }

/* --- Motion ---------------------------------------------------------------- */
@media (prefers-reduced-motion: no-preference) {
  html:not([data-motion="reduced"]) :is(.kanban-card, .calendar-event, .calendar-more, .gantt-bar) {
    transition: background-color 0.12s ease, border-color 0.12s ease, box-shadow 0.15s ease, rotate 0.15s ease;
  }
}

  html[data-motion="always"]:not([data-motion="reduced"]) :is(.kanban-card, .calendar-event, .calendar-more, .gantt-bar) {
    transition: background-color 0.12s ease, border-color 0.12s ease, box-shadow 0.15s ease, rotate 0.15s ease;
  }


/* --- Forced colours ---------------------------------------------------------- */
@media (forced-colors: active) {
  .calendar-event, .gantt-bar, .schedule-item, .kanban-placeholder { border-color: CanvasText; }
  .calendar-now, .calendar-now::before, .gantt-today, .gantt-milestone { background: Highlight; forced-color-adjust: none; }
  :is(.calendar-cell, .calendar-column-head)[aria-current="date"] .calendar-date { forced-color-adjust: none; background: Highlight; color: HighlightText; }
}
/* --- v5 prose, code and page templates (PLAN.md §22) -----------------------
 * Built into every bundle after core.css. Three parts:
 *   1. Code: a per-theme code palette for the class names Prism, highlight.js
 *      and Shiki emit (no library bundled), line numbers, highlighted and
 *      diff lines, and a .code-block with a copy button.
 *   2. .prose: raw Markdown/CMS HTML with no classes inside, plus GitHub-style
 *      callouts (.markdown-alert-*).
 *   3. The pieces the page templates (auth, settings, master-detail, inbox,
 *      onboarding, pricing) need that core doesn't have: .master-detail, .otp,
 *      radio-driven .steps wizards, billing-period price swaps, .card.is-featured
 *      and .panel.is-danger.
 * Docs: docs/components/prose.md and docs/templates.md. */

/* === 1. Code ============================================================== */

/* The palette. Each colour reads its public token, falling back to base
 * tokens: semantic text colours (checked at 4.5:1 on --surface by check.py),
 * or a hue mixed towards --text, which only raises its contrast. The private
 * --_code-* copies give the Prism, highlight.js and Shiki mappings below one
 * source each. Set on every pre/code so a theme or an app can also set the
 * public tokens on any container. */
:where(pre, code) {
  --_code-keyword: var(--code-keyword, var(--accent-text, var(--accent)));
  --_code-string: var(--code-string, var(--success-text, var(--success)));
  --_code-comment: var(--code-comment, var(--muted));
  --_code-number: var(--code-number, var(--warning-text, var(--warning)));
  --_code-function: var(--code-function, color-mix(in oklab, var(--accent-2) 35%, var(--text)));
  --_code-type: var(--code-type, color-mix(in oklab, var(--accent-text, var(--accent)), var(--danger-text, var(--danger))));
  --_code-punctuation: var(--code-punctuation, color-mix(in oklab, var(--muted) 55%, var(--text)));
  --_code-tag: var(--code-tag, var(--danger-text, var(--danger)));
  --_code-attr: var(--code-attr, color-mix(in oklab, var(--accent-text, var(--accent)) 50%, var(--text)));
  --_code-variable: var(--code-variable, currentColor);
  --_code-inserted: var(--code-inserted, var(--success-text, var(--success)));
  --_code-deleted: var(--code-deleted, var(--danger-text, var(--danger)));
  --_code-pad: var(--code-pad, calc(var(--space-m) * var(--density)));
}
/* Shiki's css-variables theme writes style="color: var(--shiki-token-…)". */
:where(pre.shiki, .shiki) {
  --shiki-foreground: var(--code-block-fg, var(--text));
  --shiki-background: var(--code-block-bg, var(--surface));
  --shiki-token-keyword: var(--_code-keyword);
  --shiki-token-string: var(--_code-string);
  --shiki-token-string-expression: var(--_code-string);
  --shiki-token-comment: var(--_code-comment);
  --shiki-token-constant: var(--_code-number);
  --shiki-token-function: var(--_code-function);
  --shiki-token-parameter: var(--_code-variable);
  --shiki-token-punctuation: var(--_code-punctuation);
  --shiki-token-link: var(--_code-string);
}
pre { padding: var(--_code-pad); color: var(--code-block-fg, inherit); tab-size: var(--code-tab-size, 2); }
pre > code { font-size: var(--code-block-font-size, 0.875em); line-height: var(--code-leading, 1.6); }
/* Libraries' own root classes paint a background; the pre already has one. */
pre > code:is(.hljs, [class*="language-"]) { background: none; color: inherit; }

/* Token classes, in this order: later rules win where a span carries two
 * (highlight.js's .hljs-title.class_ is a type, not a function). */
:where(pre, code) :is(.token.comment, .token.prolog, .token.doctype, .token.cdata, .hljs-comment, .hljs-quote) {
  color: var(--_code-comment);
  font-style: var(--code-comment-style, italic);
}
:where(pre, code) .token.prefix:is(.inserted, .deleted) { color: inherit; }
:where(pre, code) :is(.token.keyword, .token.atrule, .token.important, .token.rule, .hljs-keyword, .hljs-doctag, .hljs-meta) {
  color: var(--_code-keyword);
}
:where(pre, code) :is(.token.string, .token.char, .token.attr-value, .token.regex, .token.url, .hljs-string, .hljs-regexp, .hljs-char, .hljs-link, .hljs-meta .hljs-string) {
  color: var(--_code-string);
}
:where(pre, code) :is(.token.number, .token.boolean, .token.constant, .token.symbol, .token.entity, .hljs-number, .hljs-literal, .hljs-symbol, .hljs-bullet) {
  color: var(--_code-number);
}
:where(pre, code) :is(.token.function, .hljs-title, .hljs-section) { color: var(--_code-function); }
:where(pre, code) :is(.token.class-name, .token.builtin, .hljs-type, .hljs-built_in, .hljs-title.class_) {
  color: var(--_code-type);
}
:where(pre, code) :is(.token.punctuation, .token.operator, .hljs-punctuation, .hljs-operator, .hljs-tag) {
  color: var(--_code-punctuation);
}
/* Prism's .token.tag shares its name with the .tag chip (forms.css): undo
 * the chip inside code. */
:where(pre, code) .token.tag {
  display: inline;
  max-inline-size: none;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  box-shadow: none;
  font-size: inherit;
  line-height: inherit;
  overflow-wrap: normal;
}
:where(pre, code) :is(.token.tag, .token.selector, .hljs-name, .hljs-selector-tag) { color: var(--_code-tag); }
:where(pre, code) :is(.token.attr-name, .token.property, .hljs-attr, .hljs-attribute, .hljs-property,
  .hljs-selector-class, .hljs-selector-id, .hljs-selector-attr, .hljs-selector-pseudo) {
  color: var(--_code-attr);
}
:where(pre, code) :is(.token.variable, .token.parameter, .hljs-variable, .hljs-template-variable, .hljs-params, .hljs-subst) {
  color: var(--_code-variable);
}
:where(pre, code) :is(.token.bold, .token.important, .hljs-strong, .hljs-section) { font-weight: 700; }
:where(pre, code) :is(.token.italic, .hljs-emphasis) { font-style: italic; }
:where(pre, code) :is(.token.inserted:not(.prefix), .hljs-addition) {
  color: var(--_code-inserted);
  background: var(--code-inserted-bg, color-mix(in srgb, var(--success) 14%, transparent));
}
:where(pre, code) :is(.token.deleted:not(.prefix), .hljs-deletion) {
  color: var(--_code-deleted);
  background: var(--code-deleted-bg, color-mix(in srgb, var(--danger) 14%, transparent));
}

/* Lines. Shiki (.line) and rehype-pretty-code ([data-line]) wrap each line in
 * a span; diff lines from highlight.js and Prism are spans too. Each becomes
 * a full-width strip that bleeds into the pre's padding, so a tint reaches
 * both edges. */
pre :is(.line, [data-line], .hljs-addition, .hljs-deletion),
pre:is(.language-diff, .diff-highlight) :is(.token.inserted, .token.deleted):not(.prefix),
pre > code.language-diff :is(.token.inserted, .token.deleted):not(.prefix) {
  position: relative;
  display: inline-block;
  box-sizing: border-box;
  min-inline-size: calc(100% + 2 * var(--_code-pad));
  margin-inline: calc(-1 * var(--_code-pad));
  padding-inline: var(--_code-pad);
}
/* Highlighted lines: a tint and a start-edge bar (not colour alone). */
pre :is(.line.highlighted, .line.is-highlighted, [data-highlighted-line]) {
  background: var(--code-highlight-bg, color-mix(in srgb, var(--accent) 14%, transparent));
  box-shadow: inset var(--code-highlight-bar-width, 3px) 0 0 var(--code-highlight-bar, var(--accent));
}
/* Prism's line-highlight plugin positions its own .line-highlight boxes. */
pre:has(> .line-highlight) { position: relative; }
.line-highlight {
  position: absolute;
  inset-inline: 0;
  background: var(--code-highlight-bg, color-mix(in srgb, var(--accent) 14%, transparent));
  box-shadow: inset var(--code-highlight-bar-width, 3px) 0 0 var(--code-highlight-bar, var(--accent));
  pointer-events: none;
}
/* Shiki's diff notation drops the +/- from the text, so the sign is drawn
 * in the padding band, along with the tint. */
pre .line.diff:is(.add, .remove) { background: var(--code-inserted-bg, color-mix(in srgb, var(--success) 14%, transparent)); }
pre .line.diff.remove { background: var(--code-deleted-bg, color-mix(in srgb, var(--danger) 14%, transparent)); }
pre .line.diff:is(.add, .remove)::after {
  position: absolute;
  inset-inline-start: calc(var(--_code-pad) / 2 - 0.5ch);
  content: "+";
  content: "+" / "";
  color: var(--_code-inserted);
}
pre .line.diff.remove::after { content: "\2212"; content: "\2212" / ""; color: var(--_code-deleted); }
/* Shiki's focus notation dims everything but the focused lines. */
pre.has-focused .line:not(.focused) { opacity: 0.55; }

/* Line numbers: .line-numbers on the pre (Prism's class) or
 * data-line-numbers on the code (rehype-pretty-code), over line spans. */
:is(pre.line-numbers, code[data-line-numbers]) { counter-reset: code-line; }
:is(pre.line-numbers, code[data-line-numbers]) :is(.line, [data-line])::before {
  counter-increment: code-line;
  content: counter(code-line);
  display: inline-block;
  inline-size: var(--code-gutter, 3ch);
  margin-inline-end: 2ch;
  color: var(--code-line-number, var(--muted));
  text-align: end;
  user-select: none;
}
/* Prism's own line-numbers plugin markup (.line-numbers-rows). */
pre.line-numbers:has(.line-numbers-rows) { position: relative; padding-inline-start: calc(var(--_code-pad) + var(--code-gutter, 3ch) + 2ch); }
pre.line-numbers > code:has(> .line-numbers-rows) { position: relative; }
.line-numbers-rows {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: calc(-1 * (var(--code-gutter, 3ch) + 2ch));
  inline-size: calc(var(--code-gutter, 3ch) + 1ch);
  border-inline-end: 1px solid var(--code-gutter-rule, var(--hairline));
  pointer-events: none;
  user-select: none;
  counter-reset: code-line;
}
.line-numbers-rows > span { display: block; counter-increment: code-line; }
.line-numbers-rows > span::before {
  content: counter(code-line);
  display: block;
  padding-inline-end: 1ch;
  color: var(--code-line-number, var(--muted));
  text-align: end;
}

/* .code-block — a code block with a header (file name, language, copy):
 *   <figure class="code-block">
 *     <figcaption class="code-block-header"><span>app.js</span>
 *       <button class="btn btn-sm btn-clear code-copy" type="button">
 *         <span>Copy</span><span>Copied</span></button></figcaption>
 *     <pre><code class="language-js">…</code></pre>
 *   </figure>
 * rehype-pretty-code's figure and title get the same look. The app copies
 * the text and adds .is-copied (or data-copied) for a moment; the two labels
 * share one cell, so the button keeps its width. */
:is(.code-block, [data-rehype-pretty-code-figure]) {
  margin: 0;
  min-inline-size: 0;
  border: var(--code-block-border-width, 1px) solid var(--code-block-border, var(--border));
  border-radius: var(--radius);
  background: var(--code-block-bg, var(--surface));
  overflow: hidden;
}
:is(.code-block, [data-rehype-pretty-code-figure]) > pre { margin: 0; border: 0; border-radius: 0; }
:is(.code-block-header, [data-rehype-pretty-code-title]) {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  min-inline-size: 0;
  padding: var(--space-3xs) var(--space-3xs) var(--space-3xs) var(--_code-pad, var(--space-m));
  border-block-end: 1px solid var(--code-block-border, var(--border));
  background: var(--code-block-header-bg, var(--surface-2));
  color: var(--code-block-header-fg, var(--muted));
  font-family: var(--font-mono);
  font-size: 0.8em;
}
.code-block-header { --_code-pad: var(--code-pad, calc(var(--space-m) * var(--density))); }
.code-block-header > :first-child { min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.code-block-header > .code-copy { margin-inline-start: auto; }
.code-copy { flex: none; }
.code-copy:has(> :not(.icon) + :not(.icon)) { display: inline-grid; justify-items: center; }
.code-copy > .icon { grid-area: 1 / 1; }
.code-copy > :not(.icon) { grid-area: 1 / 2; }
.code-copy:not(:has(> .icon)) > * { grid-column: 1; }
.code-copy > :not(.icon) + :not(.icon),
.code-copy:is(.is-copied, [data-copied]) > :not(.icon):first-of-type { visibility: hidden; }
.code-copy:is(.is-copied, [data-copied]) > :not(.icon) + :not(.icon) { visibility: visible; color: var(--success-text, var(--success)); }

/* === 2. Prose ============================================================= */

/*   <article class="prose">…HTML from Markdown…</article>
 * Text holds a ~70ch measure; code, tables, figures and lone images may run
 * to the container width. Spacing comes from the --space-* scale, so the
 * rhythm tightens on phones and opens up on XL. */
.prose {
  color: var(--prose-fg, inherit);
  font-size: var(--prose-font-size, 1rem);
  line-height: var(--prose-leading, 1.65);
  overflow-wrap: break-word;
  min-inline-size: 0;
}
.prose > * { max-inline-size: var(--prose-measure, 70ch); margin-block: 0; }
.prose.is-centered > * { margin-inline: auto; }
.prose > :is(pre, figure, .code-block, .table-wrap, video, iframe, p:has(> img:only-child)) {
  max-inline-size: var(--prose-wide, none);
}
/* Vertical rhythm. */
.prose > * + * { margin-block-start: var(--prose-flow, var(--space-m)); }
.prose > :is(h1, h2) { margin-block-start: var(--prose-h2-space, var(--space-2xl)); }
.prose > :is(h3, h4) { margin-block-start: var(--prose-h3-space, var(--space-xl)); }
.prose > :is(h5, h6) { margin-block-start: var(--space-l); }
.prose > :is(h1, h2, h3, h4, h5, h6) + * { margin-block-start: var(--prose-heading-gap, var(--space-xs)); }
.prose > hr,
.prose > hr + * { margin-block-start: var(--space-xl); }
.prose > :first-child { margin-block-start: 0; }

/* Headings. Sizes beat a theme's global heading rule (a theme that styles
 * every h2 as a small label) so the hierarchy holds; a theme styles prose
 * headings with html[data-theme="x"] .prose h2, or with the size tokens. */
.prose :is(h1, h2, h3, h4, h5, h6) {
  line-height: var(--prose-heading-leading, 1.25);
  text-wrap: balance;
  scroll-margin-block-start: var(--space-xl);
}
:root .prose h1 { font-size: var(--prose-h1-size, 2em); }
:root .prose h2 { font-size: var(--prose-h2-size, 1.5em); }
:root .prose h3 { font-size: var(--prose-h3-size, 1.25em); }
:root .prose h4 { font-size: var(--prose-h4-size, 1.1em); }
:root .prose h5 { font-size: var(--prose-h5-size, 1em); }
:root .prose h6 { font-size: var(--prose-h6-size, 0.9em); }
.prose h6 { color: var(--muted); }

.prose :is(a:not([class]), a.footnote-back, a.data-footnote-backref, [role="doc-backlink"]) {
  color: var(--link, var(--accent));
  text-decoration: underline;
  text-decoration-thickness: from-font;
  text-underline-offset: 0.15em;
}
@media (hover: hover) {
  .prose a:not([class]):hover { text-decoration-thickness: 2px; }
}
.prose :is(strong, b) { font-weight: 700; }
.prose :not(pre) > code { font-size: 0.875em; overflow-wrap: anywhere; }
.prose :is(sup, sub) { line-height: 0; }
.prose mark {
  padding: 0.25em 0.35em;
  border-radius: calc(var(--radius) * 0.5);
  background: var(--mark-bg, color-mix(in srgb, var(--warning) 35%, transparent));
  color: var(--mark-fg, inherit);
}
.prose abbr[title] {
  text-decoration: underline dotted;
  text-underline-offset: 0.2em;
  cursor: var(--cursor-help, help);
}
.prose :is(del, s) { color: var(--muted); }
.prose ins { text-decoration: underline; text-decoration-color: var(--success); }

/* Lists, nested to three levels. */
.prose :is(ul, ol) { padding-inline-start: var(--prose-list-indent, 1.6em); }
.prose ul { list-style: disc; }
.prose ul ul { list-style: circle; }
.prose ul ul ul { list-style: square; }
.prose ol { list-style: decimal; }
.prose ol ol { list-style: lower-alpha; }
.prose ol ol ol { list-style: lower-roman; }
.prose li::marker { color: var(--prose-marker, var(--muted)); }
.prose li + li,
.prose li > * + * { margin-block-start: var(--prose-list-gap, var(--space-2xs)); }
.prose li > p { margin: 0; }
/* GitHub task lists. */
.prose :is(ul.contains-task-list, ul:has(> li > input[type="checkbox"]:first-child)) { list-style: none; padding-inline-start: 0.4em; }
.prose li > input[type="checkbox"]:first-child { margin: 0 0.5em 0 0; vertical-align: -0.1em; accent-color: var(--accent); }

.prose blockquote {
  padding-inline-start: var(--space-m);
  border-inline-start: var(--prose-quote-rule-width, 3px) solid var(--prose-quote-rule, var(--border));
  color: var(--prose-quote-fg, var(--muted));
}
.prose blockquote > * { margin: 0; }
.prose blockquote > * + * { margin-block-start: var(--space-s); }

.prose dt { font-weight: 600; }
.prose dd { margin: var(--space-3xs) 0 0; padding-inline-start: var(--space-l); }
.prose dd + dt { margin-block-start: var(--space-s); }

.prose hr {
  block-size: 0;
  border: 0;
  border-block-start: 1px solid var(--prose-rule, var(--hairline));
}

/* Media. Markdown wraps a lone image in a <p>; it gets the wide measure. */
.prose :is(img, video) { display: inline-block; max-inline-size: 100%; block-size: auto; vertical-align: middle; }
.prose :is(p > img:only-child, figure img, figure video, figure > svg) {
  display: block;
  border-radius: var(--prose-media-radius, var(--radius));
}
.prose figure { margin-inline: 0; }
.prose figcaption { margin-block-start: var(--space-xs); color: var(--muted); font-size: 0.875em; }
.prose figure:has(> :is(img, picture, video, svg)) > figcaption { text-align: center; }

/* Tables need no wrapper: the table itself scrolls sideways when its cells
 * can't wrap to fit. display: block keeps table semantics in Chromium and
 * Firefox; WebKit has historically dropped them, so where the renderer can
 * add one, a .table-wrap around the table is the safe form (the table then
 * stays display: table). */
.prose table {
  display: block;
  inline-size: max-content;
  max-inline-size: 100%;
  overflow-x: auto;
  border-collapse: collapse;
  font-size: 0.9em;
  font-variant-numeric: tabular-nums;
  scrollbar-width: thin;
  scrollbar-color: var(--scrollbar-thumb, var(--border)) transparent;
}
.prose .table-wrap > table { display: table; inline-size: 100%; max-inline-size: none; overflow: visible; }
.prose :is(th, td) {
  padding: calc(0.5em * var(--density)) calc(0.75em * var(--density));
  border-block-end: 1px solid var(--table-rule, var(--hairline));
  text-align: start;
  vertical-align: top;
}
.prose th {
  background: var(--table-head-bg, transparent);
  color: var(--table-head-fg, var(--muted));
  border-block-end: var(--table-head-rule-width, 1px) solid var(--table-head-rule, var(--border));
  font-size: 0.85em;
  font-weight: 600;
  letter-spacing: var(--table-head-tracking, 0.06em);
  text-transform: var(--table-head-transform, uppercase);
  white-space: nowrap;
}
.prose :is(th, td)[align="center"] { text-align: center; }
.prose :is(th, td)[align="right"] { text-align: end; }

.prose details {
  padding-inline: var(--space-s);
  border: 1px solid var(--prose-details-border, var(--border));
  border-radius: var(--radius);
  background: var(--prose-details-bg, transparent);
}
.prose details[open] { padding-block-end: var(--space-s); }
.prose summary {
  display: flex;
  align-items: center;
  gap: 0.5em;
  min-block-size: max(var(--tap-min), 2.5rem);
  padding-block: var(--space-2xs);
  font-weight: 600;
  list-style: none;
  touch-action: manipulation;
}
.prose summary::-webkit-details-marker { display: none; }
.prose summary::before {
  content: var(--prose-details-glyph, "\25B8");
  content: var(--prose-details-glyph, "\25B8") / "";
  flex: none;
  color: var(--muted);
}
.prose details[open] > summary::before { rotate: 90deg; }
.prose details > :not(summary) { margin: 0; }
.prose details > :not(summary) + * { margin-block-start: var(--space-xs); }

/* Footnotes, as GitHub, markdown-it-footnote and Pandoc write them. */
.prose :is(.footnotes, [data-footnotes], [role="doc-endnotes"]) {
  margin-block-start: var(--space-2xl);
  padding-block-start: var(--space-m);
  border-block-start: 1px solid var(--prose-rule, var(--hairline));
  color: var(--muted);
  font-size: 0.875em;
}
.prose :is(.footnotes, [data-footnotes], [role="doc-endnotes"]) > hr,
.prose > hr.footnotes-sep { display: none; }
.prose :is(.footnotes, [data-footnotes], [role="doc-endnotes"]) li > p { display: inline; }
.prose :is(sup > a, a[role="doc-noteref"], [data-footnote-ref]) { text-decoration: none; font-size: 0.8em; padding-inline: 0.1em; }
.prose :is(.footnotes, [data-footnotes], [role="doc-endnotes"]) li:target,
.prose :is(sup, a[role="doc-noteref"]):target {
  background: var(--prose-target-bg, color-mix(in srgb, var(--accent) 16%, transparent));
  outline: 1px dashed var(--accent);
  outline-offset: 2px;
}
.prose :is(.footnotes, [data-footnotes]) [id] { scroll-margin-block-start: var(--space-xl); }
/* GitHub hides the "Footnotes" heading with .sr-only. */
.prose .sr-only {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* --- Callouts: > [!NOTE], [!TIP], [!IMPORTANT], [!WARNING], [!CAUTION] ------
 * GitHub, remark-github-blockquote-alert and markdown-it-github-alerts write
 *   <div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title">Note</p>…</div>
 * Obsidian-style renderers write <blockquote data-callout="note">. Both get
 * the alert colours; the title word carries the meaning, not the colour. */
:is(.markdown-alert, [data-callout]) {
  --_callout: var(--callout-note, var(--accent));
  --_callout-fg: var(--callout-note-text, var(--accent-text, var(--accent)));
  margin-inline: 0;
  padding: var(--space-s) var(--space-m);
  border: 0;
  border-inline-start: var(--callout-rule-width, 4px) solid var(--_callout);
  border-radius: var(--callout-radius, 0 var(--radius) var(--radius) 0);
  background: var(--callout-bg, color-mix(in srgb, var(--_callout) 9%, transparent));
  color: var(--callout-fg, var(--text));
}
:is(.markdown-alert, [data-callout]) > * { margin: 0; }
:is(.markdown-alert, [data-callout]) > * + * { margin-block-start: var(--space-xs); }
:is(.markdown-alert-title, .callout-title) {
  display: flex;
  align-items: center;
  gap: 0.5em;
  color: var(--_callout-fg);
  font-weight: 600;
}
:is(.markdown-alert-title, .callout-title) > svg { flex: none; inline-size: 1em; block-size: 1em; fill: currentColor; }
:is(.markdown-alert-tip, [data-callout="tip"], [data-callout="success"]) {
  --_callout: var(--callout-tip, var(--success));
  --_callout-fg: var(--callout-tip-text, var(--success-text, var(--success)));
}
:is(.markdown-alert-important, [data-callout="important"]) {
  --_callout: var(--callout-important, var(--accent-2));
  --_callout-fg: var(--callout-important-text, color-mix(in oklab, var(--accent-2) 55%, var(--text)));
}
:is(.markdown-alert-warning, [data-callout="warning"]) {
  --_callout: var(--callout-warning, var(--warning));
  --_callout-fg: var(--callout-warning-text, var(--warning-text, var(--warning)));
}
:is(.markdown-alert-caution, [data-callout="caution"], [data-callout="danger"]) {
  --_callout: var(--callout-caution, var(--danger));
  --_callout-fg: var(--callout-caution-text, var(--danger-text, var(--danger)));
}

/* === 3. Page-template pieces ============================================== */

/* --- .master-detail: a list beside its detail; on a phone the detail covers
 * the list, with a back control. Settings, master-detail and inbox use it.
 *   <div class="master-detail">
 *     [<nav class="nav-rail">…folders…</nav>]          optional third pane
 *     <nav class="master-detail-list"><ul class="list">
 *       <li><label class="list-item"><input type="radio" name="md" form="md">
 *         <input type="radio" name="md" form="md" class="master-detail-home" checked hidden>One</label></li>
 *       <li><label class="list-item"><input type="radio" name="md" form="md">Two</label></li>
 *     </ul></nav>
 *     <section class="master-detail-pane">
 *       <button class="btn btn-ghost master-detail-back" type="reset" form="md">Back</button>…</section>
 *     <section class="master-detail-pane">…</section>
 *   </div>
 *   <form id="md" hidden></form>
 * No JS: item n shows pane n (up to 12). The hidden .master-detail-home radio
 * is the "list" state: it marks item 1 as the default on wide screens, and
 * the reset button (a real, focusable button) restores it, which on a phone
 * goes back to the list. The radios belong to their own empty form, so the
 * reset touches nothing else on the page. A server-rendered app drops the
 * radios, renders the one pane, and adds .is-detail on phones when an item
 * is open (its back control is then a link to the list). */
.master-detail {
  container-type: inline-size;
  display: grid;
  grid-template-columns: var(--master-detail-list-width, minmax(14rem, 20rem)) minmax(0, 1fr);
  gap: var(--master-detail-gap, var(--space-m));
  align-items: start;
}
.master-detail:has(> .nav-rail) {
  grid-template-columns: auto var(--master-detail-list-width, minmax(14rem, 20rem)) minmax(0, 1fr);
}
.master-detail > * { grid-row: 1; min-inline-size: 0; }
.master-detail > .nav-rail { grid-column: 1; align-self: stretch; }
.master-detail > .master-detail-list { grid-column: -3; }
.master-detail > .master-detail-pane { grid-column: -2; }
.master-detail-back { display: none; }
/* Pane n hides unless item n is checked (the shown pane keeps its own
 * display, so a pane can be a .stack or a grid). */
.master-detail:where(:has(.master-detail-list input[type="radio"]):not(:has(.master-detail-list li:nth-child(1) input:checked))) > .master-detail-pane:where(:nth-child(1 of .master-detail-pane)),
.master-detail:where(:has(.master-detail-list input[type="radio"]):not(:has(.master-detail-list li:nth-child(2) input:checked))) > .master-detail-pane:where(:nth-child(2 of .master-detail-pane)),
.master-detail:where(:has(.master-detail-list input[type="radio"]):not(:has(.master-detail-list li:nth-child(3) input:checked))) > .master-detail-pane:where(:nth-child(3 of .master-detail-pane)),
.master-detail:where(:has(.master-detail-list input[type="radio"]):not(:has(.master-detail-list li:nth-child(4) input:checked))) > .master-detail-pane:where(:nth-child(4 of .master-detail-pane)),
.master-detail:where(:has(.master-detail-list input[type="radio"]):not(:has(.master-detail-list li:nth-child(5) input:checked))) > .master-detail-pane:where(:nth-child(5 of .master-detail-pane)),
.master-detail:where(:has(.master-detail-list input[type="radio"]):not(:has(.master-detail-list li:nth-child(6) input:checked))) > .master-detail-pane:where(:nth-child(6 of .master-detail-pane)),
.master-detail:where(:has(.master-detail-list input[type="radio"]):not(:has(.master-detail-list li:nth-child(7) input:checked))) > .master-detail-pane:where(:nth-child(7 of .master-detail-pane)),
.master-detail:where(:has(.master-detail-list input[type="radio"]):not(:has(.master-detail-list li:nth-child(8) input:checked))) > .master-detail-pane:where(:nth-child(8 of .master-detail-pane)),
.master-detail:where(:has(.master-detail-list input[type="radio"]):not(:has(.master-detail-list li:nth-child(9) input:checked))) > .master-detail-pane:where(:nth-child(9 of .master-detail-pane)),
.master-detail:where(:has(.master-detail-list input[type="radio"]):not(:has(.master-detail-list li:nth-child(10) input:checked))) > .master-detail-pane:where(:nth-child(10 of .master-detail-pane)),
.master-detail:where(:has(.master-detail-list input[type="radio"]):not(:has(.master-detail-list li:nth-child(11) input:checked))) > .master-detail-pane:where(:nth-child(11 of .master-detail-pane)),
.master-detail:where(:has(.master-detail-list input[type="radio"]):not(:has(.master-detail-list li:nth-child(12) input:checked))) > .master-detail-pane:where(:nth-child(12 of .master-detail-pane)) {
  display: none;
}
/* A drill-in list on phones: a chevron (an .icon last in the row) shows
 * only when the list drills in. */
.master-detail-list .list-item > .icon:last-child:not(:first-child) { display: none; margin-inline-start: auto; color: var(--muted); }
/* Phone-sized box: one column. The list (and the rail) show until an item
 * is chosen, then the chosen pane covers them. */
@container (max-width: 480px) {
  .master-detail > .master-detail-list,
  .master-detail > .master-detail-pane { grid-column: 2 / -1; }
  .master-detail:not(:has(> .nav-rail)) > :is(.master-detail-list, .master-detail-pane) { grid-column: 1 / -1; }
  .master-detail:not(.is-detail, :has(.master-detail-list input:checked:not(.master-detail-home))) > .master-detail-pane { display: none; }
  .master-detail:is(.is-detail, :has(.master-detail-list input:checked:not(.master-detail-home))) > :not(.master-detail-pane) { display: none; }
  .master-detail:is(.is-detail, :has(.master-detail-list input:checked:not(.master-detail-home))) > .master-detail-pane { grid-column: 1 / -1; }
  .master-detail-back { display: inline-flex; }
  .master-detail-list .list-item > .icon:last-child:not(:first-child) { display: block; }
}
@media (prefers-reduced-motion: no-preference) {
  @container (max-width: 480px) {
    html:not([data-motion="reduced"], [data-motion="none"]) .master-detail > .master-detail-pane {
      transition: translate var(--motion-duration-m, 200ms) var(--motion-ease, ease-out), opacity var(--motion-duration-m, 200ms) var(--motion-ease, ease-out);
      @starting-style { translate: 2rem 0; opacity: 0; }
    }
  }
}

  @container (max-width: 480px) {
    html[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) .master-detail > .master-detail-pane {
      transition: translate var(--motion-duration-m, 200ms) var(--motion-ease, ease-out), opacity var(--motion-duration-m, 200ms) var(--motion-ease, ease-out);
      @starting-style { translate: 2rem 0; opacity: 0; }
    }
  }


/* --- .otp: a one-time code in per-digit boxes ------------------------------
 *   <div class="otp">
 *     <input class="otp-input" id="code" inputmode="numeric" autocomplete="one-time-code"
 *            maxlength="6" pattern="[0-9]{6}" aria-label="6-digit code">
 *     <span></span><span></span><span></span><span></span><span></span><span></span>
 *   </div>
 * One real input (so typing, paste, autofill and the SMS/e-mail code
 * suggestion all work with no JS), its digits spaced onto the boxes drawn by
 * the empty spans. --otp-length matches the box count (default 6). */
.otp {
  --_otp-n: var(--otp-length, 6);
  --_otp-gap: var(--otp-gap, var(--space-xs));
  --_otp-cell: min(var(--otp-cell-size, 2.75rem), (100cqi - (var(--_otp-n) - 1) * var(--_otp-gap)) / var(--_otp-n));
  container-type: inline-size;
  position: relative;
  display: flex;
  gap: var(--_otp-gap);
  /* Its natural size anywhere (a container has no intrinsic width), shrinking on narrow screens. */
  inline-size: calc(var(--_otp-n) * var(--otp-cell-size, 2.75rem) + (var(--_otp-n) - 1) * var(--_otp-gap));
  max-inline-size: 100%;
  block-size: var(--otp-height, 3.25rem);
  overflow: hidden;
  direction: ltr;
}
.otp > span {
  flex: 0 0 var(--_otp-cell);
  border: var(--input-border-width, 1px) solid var(--input-border, var(--border));
  border-radius: var(--otp-radius, var(--input-radius, var(--radius)));
  background: var(--input-bg, var(--surface-2));
}
/* A separator cell (XXXX-XXXX): an .otp-sep counts in --otp-length and
   holds the dash the app inserts into the value. .is-alnum upper-cases. */
.otp > .otp-sep { border-color: transparent; background: none; }
.otp.is-alnum .otp-input { text-transform: uppercase; }
.otp-input {
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  z-index: var(--z-raised);
  inline-size: calc((var(--_otp-n) + 1) * (var(--_otp-cell) + var(--_otp-gap)));
  min-block-size: var(--tap-min);
  margin: 0;
  padding: 0 0 0 calc((var(--_otp-cell) - 1ch) / 2);
  border: 0;
  background: transparent;
  color: var(--input-fg, var(--text));
  caret-color: var(--input-caret, var(--focus));
  font-family: var(--font-mono);
  font-size: clamp(1rem, var(--_otp-cell) * 0.55, 1.5rem);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: calc(var(--_otp-cell) + var(--_otp-gap) - 1ch);
  outline: none;
}
.otp-input::placeholder { color: var(--input-placeholder, var(--muted)); opacity: 1; }
/* The input's own ring would sit off the boxes, so the boxes show focus. */
.otp:has(> .otp-input:focus-visible) > span {
  border-color: var(--focus);
  box-shadow: 0 0 0 1px var(--focus), var(--focus-ring, 0 0 0 0 transparent);
}
.otp:has(> .otp-input:is([aria-invalid="true"], .is-invalid)) > span {
  border-color: var(--danger);
  background: var(--input-error-bg, var(--input-bg, var(--surface-2)));
}
.otp:has(> .otp-input:disabled) { opacity: 0.55; }

/* --- Radio-driven wizard on ol.steps ----------------------------------------
 *   <div class="wizard">
 *     <ol class="steps" aria-label="Setup">
 *       <li><label><input type="radio" name="wiz" id="wiz-1" checked>Workspace</label></li>
 *       <li><label><input type="radio" name="wiz" id="wiz-2">Invite</label></li>
 *     </ol>
 *     <section class="wizard-step">… <label class="btn btn-primary" for="wiz-2">Continue</label></section>
 *     <section class="wizard-step">…</section>
 *   </div>
 * The checked step is current, the steps before it are done (the same
 * tokens and glyphs as .is-done and aria-current="step"), and step n shows
 * .wizard-step n (up to 8). The step labels are the radio group, so arrow
 * keys move through it; Back/Next/Skip are <label for> shortcuts. */
.steps > li > label {
  display: block;
  min-block-size: var(--tap-min);
  margin-block-start: calc(-1 * (var(--steps-marker) + var(--space-2xs)));
  padding-block-start: calc(var(--steps-marker) + var(--space-2xs));
  color: inherit;
  touch-action: manipulation;
}
.steps > li > label > input {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  margin: 0;
  overflow: hidden;
  clip-path: inset(50%);
  opacity: 0;
}
.steps > li > label:has(> input:focus-visible) { outline: 2px solid var(--focus); outline-offset: 2px; }
.steps > li:has(> label > input:checked) { color: var(--steps-fg-current, var(--text)); font-weight: 600; }
.steps > li:has(> label > input:checked)::before {
  border-color: var(--steps-current, var(--accent));
  color: var(--steps-current-fg, var(--accent-text, var(--accent)));
  box-shadow: 0 0 0 3px var(--steps-current-halo, transparent);
}
.steps > li:has(~ li > label > input:checked) { color: var(--steps-fg-done, var(--text)); }
.steps > li:has(~ li > label > input:checked)::before {
  content: var(--steps-done-glyph, "\2713");
  background: var(--steps-done-bg, var(--accent));
  border-color: var(--steps-done-bg, var(--accent));
  color: var(--steps-done-fg, var(--on-accent));
}
.steps > li:has(~ li > label > input:checked)::after { background: var(--steps-line-done, var(--accent)); }
/* Stacked steps (vertical, or a box/screen of 480px or less): the label
 * reaches back over the marker beside it instead. */
.steps.is-vertical > li > label {
  min-block-size: max(var(--tap-min), var(--steps-marker));
  margin-block-start: -0.3em;
  margin-inline-start: calc(-1 * (var(--steps-marker) + var(--space-s)));
  padding-block-start: 0.3em;
  padding-inline-start: calc(var(--steps-marker) + var(--space-s));
}
@container (max-width: 480px) {
  .steps > li > label {
    min-block-size: max(var(--tap-min), var(--steps-marker));
    margin-block-start: -0.3em;
    margin-inline-start: calc(-1 * (var(--steps-marker) + var(--space-s)));
    padding-block-start: 0.3em;
    padding-inline-start: calc(var(--steps-marker) + var(--space-s));
  }
}
@media (max-width: 480px) {
  .steps > li > label {
    min-block-size: max(var(--tap-min), var(--steps-marker));
    margin-block-start: -0.3em;
    margin-inline-start: calc(-1 * (var(--steps-marker) + var(--space-s)));
    padding-block-start: 0.3em;
    padding-inline-start: calc(var(--steps-marker) + var(--space-s));
  }
}
.wizard:not(:has(> .steps > li:nth-child(1) input:checked)) > .wizard-step:nth-child(1 of .wizard-step),
.wizard:not(:has(> .steps > li:nth-child(2) input:checked)) > .wizard-step:nth-child(2 of .wizard-step),
.wizard:not(:has(> .steps > li:nth-child(3) input:checked)) > .wizard-step:nth-child(3 of .wizard-step),
.wizard:not(:has(> .steps > li:nth-child(4) input:checked)) > .wizard-step:nth-child(4 of .wizard-step),
.wizard:not(:has(> .steps > li:nth-child(5) input:checked)) > .wizard-step:nth-child(5 of .wizard-step),
.wizard:not(:has(> .steps > li:nth-child(6) input:checked)) > .wizard-step:nth-child(6 of .wizard-step),
.wizard:not(:has(> .steps > li:nth-child(7) input:checked)) > .wizard-step:nth-child(7 of .wizard-step),
.wizard:not(:has(> .steps > li:nth-child(8) input:checked)) > .wizard-step:nth-child(8 of .wizard-step) {
  display: none;
}
.wizard > .steps { margin-block-end: var(--space-l); }
.wizard-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs);
  margin-block-start: var(--space-l);
  padding-block-start: var(--space-m);
  border-block-start: 1px solid var(--hairline);
}
.wizard-actions > .push ~ * { flex: none; }

/* --- Billing period price swap ---------------------------------------------
 *   <div class="pricing">
 *     <div class="segmented"><label class="segmented-item"><input type="radio" name="billing" value="monthly" checked>Monthly</label>
 *       <label class="segmented-item"><input type="radio" name="billing" value="annual">Annual</label></div>
 *     <div class="price"><span data-billing="monthly">$12<span class="price-period">/mo</span></span>
 *       <span data-billing="annual">$10<span class="price-period">/mo</span></span></div>
 *   </div>
 * Both versions share one grid cell, so nothing moves when the period
 * changes; the other one is hidden (visibility, so it leaves the
 * accessibility tree too). */
.pricing :not(td, th):has(> [data-billing]) { display: grid; }
.pricing [data-billing] { grid-area: 1 / 1; }
.pricing:has(input[value="annual"]:checked) [data-billing="monthly"],
.pricing:not(:has(input[value="annual"]:checked)) [data-billing="annual"] { visibility: hidden; }

/* .card.is-featured — the plan to pick: a heavier ring in the accent. Pair it
 * with a visible label ("Most popular"), so colour isn't the only signal. */
.card.is-featured {
  border-color: var(--card-featured-border, var(--accent));
  outline: var(--card-featured-ring-width, 2px) solid var(--card-featured-border, var(--accent));
  outline-offset: -1px;
}
/* .panel.is-danger — a danger zone: irreversible actions. */
.panel.is-danger { border-color: var(--panel-danger-border, var(--danger)); }
.panel.is-danger > .panel-header { color: var(--danger-text, var(--danger)); }

/* --- Forced colours --------------------------------------------------------- */
@media (forced-colors: active) {
  :is(.markdown-alert, [data-callout]) { border-inline-start-color: CanvasText; }
  pre :is(.line.highlighted, [data-highlighted-line], .line.diff) { forced-color-adjust: none; background: Highlight; color: HighlightText; }
  .otp > span { border-color: CanvasText; }
  .otp:has(> .otp-input:focus-visible) > span { border-color: Highlight; }
  .card.is-featured { outline-color: Highlight; }
}
/* --- v5 page sections: marketing and app UI ---------------------------------
 * Page and section headings, action panels, media objects, feature grids,
 * an announcement banner, a bento grid, FAQs, a logo cloud, testimonials,
 * a newsletter signup, people cards, post meta, a contact list, a site
 * footer and rich menu items for flyout and mega menus.
 * Builds on core's .hero / .band / .price / .feature-list, .card, .avatar,
 * .btn, .input, .grid and [popover] menus; every look reads a
 * --<component>-* token that falls back to the base palette.
 * See docs/components/sections.md. */

/* --- .page-heading — the title row at the top of a page --------------------
 *   <header class="page-heading">
 *     <div class="page-heading-main">
 *       <nav class="breadcrumbs">…</nav>
 *       <h1>Back end developer</h1>
 *       <p class="page-heading-meta"><span>Full-time</span><span>Remote</span></p>
 *     </div>
 *     <div class="page-heading-actions"><button class="btn">Edit</button>…</div>
 *   </header> */
.page-heading {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: 0.75rem 1.5rem;
  margin-block-end: var(--page-heading-gap, 1.5rem);
  padding-block-end: var(--page-heading-pad, 1rem);
  border-block-end: var(--page-heading-border-width, 1px) solid var(--page-heading-border, var(--hairline, var(--border)));
}
.page-heading-main { display: grid; gap: 0.35rem; min-inline-size: 0; }
.page-heading-main > :is(h1, h2) { margin: 0; overflow-wrap: anywhere; }
.page-heading-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1.25rem;
  margin: 0;
  color: var(--muted);
  font-size: 0.875em;
}
.page-heading-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; }

/* .section-heading — a smaller heading over one section of a page, with an
 * optional description and actions. */
.section-heading {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: 0.5rem 1rem;
  margin-block-end: 1rem;
  padding-block-end: 0.6rem;
  border-block-end: 1px solid var(--section-heading-border, var(--hairline, var(--border)));
}
.section-heading > div:first-child { min-inline-size: 0; }
.section-heading :is(h2, h3) { margin: 0; }
.section-heading p { margin: 0.25rem 0 0; color: var(--muted); font-size: 0.875em; }

/* --- .action-panel — a titled box with one decision in it ------------------
 *   <section class="action-panel">
 *     <div><h3>Delete your account</h3><p>Once you delete it, it's gone.</p></div>
 *     <button class="btn btn-danger">Delete account</button>
 *   </section>
 * The action sits beside the text when there is room, under it when not.
 * .is-danger edges it in the danger colour. */
.action-panel {
  container-type: inline-size;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem 1.5rem;
  padding: var(--action-panel-padding, 1.25rem);
  background: var(--action-panel-bg, var(--panel-bg, var(--surface)));
  color: var(--on-surface, var(--text));
  border: var(--panel-border-width, 1px) solid var(--action-panel-border, var(--border));
  border-radius: var(--panel-radius, var(--radius));
}
.action-panel > div:first-child { flex: 1 1 18rem; min-inline-size: 0; }
.action-panel :is(h2, h3) { margin: 0 0 0.25rem; font-size: 1.05em; }
.action-panel p { margin: 0; color: var(--muted); }
.action-panel.is-danger { border-color: var(--danger); }

/* --- .media-object — an image beside a body --------------------------------
 *   <div class="media-object"><img class="avatar" src="…" alt=""><div>…</div></div>
 * .is-center aligns the image to the body's middle, .is-reverse puts it on
 * the far side. Nests for threaded replies. */
.media-object { display: flex; gap: var(--media-object-gap, 0.9rem); align-items: flex-start; }
.media-object > :first-child { flex: none; }
.media-object > :last-child { flex: 1 1 0; min-inline-size: 0; }
.media-object.is-center { align-items: center; }
.media-object.is-reverse { flex-direction: row-reverse; }
.media-object :is(h3, h4) { margin: 0 0 0.2rem; }
.media-object p { margin: 0; }

/* --- .features — a grid of icon, title and text ----------------------------
 * Feature sections and shop incentives (free shipping, returns…).
 *   <ul class="features">
 *     <li><svg class="icon">…</svg><h3>Push to deploy</h3><p>…</p></li>
 *   </ul>
 * .is-centered centres each item; --features-min sets the column width. */
.features {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--features-min, 14rem)), 1fr));
  gap: var(--features-gap, 1.75rem);
  margin: 0;
  padding: 0;
  list-style: none;
}
.features > * { display: grid; gap: 0.4rem; align-content: start; }
.features > * > .icon,
.features > * > img {
  inline-size: var(--features-icon-size, 2.5rem);
  block-size: var(--features-icon-size, 2.5rem);
  padding: 0.5rem;
  border-radius: var(--radius);
  background: var(--features-icon-bg, color-mix(in srgb, var(--accent) 16%, var(--surface)));
  color: var(--features-icon-fg, var(--accent));
}
.features :is(h3, h4) { margin: 0; font-size: 1em; }
.features p { margin: 0; color: var(--muted); }
.features.is-centered > * { justify-items: center; text-align: center; }

/* --- .banner — an announcement bar across the top or bottom ---------------
 *   <div class="banner" role="region" aria-label="Announcement">
 *     <p><strong>Conf 2026</strong> is on 12 June. <a href="…">Get tickets</a></p>
 *     <label class="btn-close"><input type="checkbox" class="banner-dismiss" aria-label="Dismiss"></label>
 *   </div>
 * A checked .banner-dismiss hides the banner with no script (remember the
 * choice server-side, or let the app remove it). Add .sticky-top or
 * .fixed-bottom to pin it. .is-accent fills it with the accent. */
.banner {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  padding: var(--banner-padding, 0.55rem 1rem);
  background: var(--banner-bg, var(--surface-2));
  color: var(--banner-fg, var(--text));
  border-block-end: 1px solid var(--banner-border, var(--border));
  font-size: 0.9em;
  text-align: center;
}
.banner > p { flex: 1 1 auto; margin: 0; }
.banner > .btn-close { flex: none; }
.banner.is-accent { --banner-bg: var(--accent); --banner-fg: var(--on-accent); --link: var(--on-accent); }
.banner.is-accent a { color: inherit; text-decoration-line: underline; }
.banner-dismiss { position: absolute; opacity: 0; inline-size: 100%; block-size: 100%; margin: 0; }
label.btn-close:has(> .banner-dismiss) { position: relative; }
.banner:has(.banner-dismiss:checked) { display: none; }

/* --- .bento — a mosaic of cards in a fixed grid ----------------------------
 *   <div class="bento"><article class="card is-wide is-tall">…</article>…</div>
 * Four columns (two on tablets, one on phones); a child spans more with
 * .is-wide / .is-tall or --span-cols / --span-rows. */
.bento {
  display: grid;
  grid-template-columns: repeat(var(--bento-cols, 4), minmax(0, 1fr));
  grid-auto-rows: minmax(var(--bento-row, 10rem), auto);
  grid-auto-flow: dense;
  gap: var(--bento-gap, 1rem);
}
.bento > * {
  grid-column: span min(var(--span-cols, 1), var(--bento-cols, 4));
  grid-row: span var(--span-rows, 1);
  margin: 0;
}
.bento > .is-wide { --span-cols: 2; }
.bento > .is-tall { --span-rows: 2; }
@media (max-width: 900px) { .bento { --bento-cols: 2; } }
@media (max-width: 600px) { .bento { --bento-cols: 1; } .bento > * { --span-rows: 1; } }

/* --- .faq — questions and answers ------------------------------------------
 *   <dl class="faq"><div><dt>How do I pay?</dt><dd>By card or invoice.</dd></div>…</dl>
 * Open by default, in two columns when there is room. For a collapsible
 * list use .accordion (core) with the same text. */
.faq {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--faq-min, 22rem)), 1fr));
  gap: var(--faq-gap, 1.5rem 2.5rem);
  margin: 0;
}
.faq > div { margin: 0; }
.faq dt { font-weight: 600; }
.faq dd { margin: 0.35rem 0 0; color: var(--muted); }

/* --- .logo-cloud — a row of partner or customer logos ----------------------
 *   <ul class="logo-cloud"><li><img src="acme.svg" alt="Acme"></li>…</ul>
 * Logos are muted to one tone so no brand shouts; .is-color keeps their
 * colours. Every logo needs alt text (the company name). */
.logo-cloud {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--logo-cloud-gap, 1.5rem 3rem);
  margin: 0;
  padding: 0;
  list-style: none;
}
.logo-cloud > * { display: flex; align-items: center; }
.logo-cloud :is(img, svg) {
  max-block-size: var(--logo-cloud-height, 2rem);
  inline-size: auto;
  opacity: var(--logo-cloud-opacity, 0.7);
  filter: grayscale(1);
}
.logo-cloud.is-color :is(img, svg) { opacity: 1; filter: none; }

/* --- .testimonial — a quote with its author --------------------------------
 *   <figure class="testimonial">
 *     <blockquote><p>“It changed how we ship.”</p></blockquote>
 *     <figcaption><img class="avatar" src="…" alt=""><span><strong>Ana Ruiz</strong><span>CTO, Acme</span></span></figcaption>
 *   </figure>
 * .is-featured makes it large and centred (a single hero quote). */
.testimonial {
  display: grid;
  gap: 1rem;
  margin: 0;
  padding: var(--testimonial-padding, 1.5rem);
  background: var(--testimonial-bg, var(--panel-bg, var(--surface)));
  color: var(--on-surface, var(--text));
  border: var(--panel-border-width, 1px) solid var(--testimonial-border, var(--border));
  border-radius: var(--panel-radius, var(--radius));
}
.testimonial blockquote { margin: 0; font-size: 1.05em; }
.testimonial blockquote > p { margin: 0; }
.testimonial figcaption { display: flex; align-items: center; gap: 0.75rem; }
.testimonial figcaption > span { display: grid; }
.testimonial figcaption > span > span { color: var(--muted); font-size: 0.875em; }
.testimonial.is-featured {
  justify-items: center;
  padding: var(--testimonial-featured-padding, 2.5rem 1.5rem);
  text-align: center;
  background: none;
  border: 0;
}
.testimonial.is-featured blockquote { max-inline-size: 42rem; font-size: 1.35em; }

/* --- .newsletter — a heading, text and an inline signup form --------------
 *   <section class="newsletter">
 *     <div><h2>Stay in the loop</h2><p>One email a month.</p></div>
 *     <form class="newsletter-form"><label class="visually-hidden" for="nl">Email</label>
 *       <input class="input" id="nl" type="email" required autocomplete="email" placeholder="you@example.com">
 *       <button class="btn btn-primary">Subscribe</button></form>
 *   </section>
 * Side by side when there is room, stacked when not. */
.newsletter {
  container-type: inline-size;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem 2rem;
}
.newsletter > div { flex: 1 1 18rem; }
.newsletter :is(h2, h3) { margin: 0 0 0.3rem; }
.newsletter p { margin: 0; color: var(--muted); }
.newsletter-form { display: flex; flex: 1 1 22rem; flex-wrap: wrap; gap: 0.5rem; }
.newsletter-form > .input { flex: 1 1 14rem; inline-size: auto; }

/* --- .person — a team member card ------------------------------------------
 *   <ul class="people"><li class="person"><img src="…" alt="">
 *     <h3>Leo Park</h3><p>Designer</p><ul class="person-links">…</ul></li></ul> */
.people {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, var(--people-min, 11rem)), 1fr));
  gap: var(--people-gap, 2rem 1.5rem);
  margin: 0;
  padding: 0;
  list-style: none;
}
.person { display: grid; justify-items: center; gap: 0.25rem; text-align: center; }
.person > img:not(.avatar) { inline-size: 100%; block-size: auto; aspect-ratio: 1; object-fit: cover; border-radius: var(--radius); }
.person :is(h3, h4) { margin: 0.5rem 0 0; font-size: 1em; }
.person > p { margin: 0; color: var(--muted); font-size: 0.875em; }
.person-links { display: flex; gap: 0.5rem; margin: 0.25rem 0 0; padding: 0; list-style: none; }

/* --- .post-meta — the byline under a blog card's title --------------------
 *   <p class="post-meta"><time datetime="2026-10-01">1 Oct 2026</time><span>6 min read</span></p> */
.post-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem 0.5rem;
  margin: 0;
  color: var(--muted);
  font-size: 0.85em;
}
.post-meta > * + *::before { content: "·"; margin-inline-end: 0.5rem; }
.post-meta > .avatar + *::before { content: none; }

/* --- .contact-list — ways to get in touch ---------------------------------
 *   <dl class="contact-list"><div><dt><svg class="icon">…</svg><span class="visually-hidden">Email</span></dt>
 *     <dd><a href="mailto:hi@acme.dev">hi@acme.dev</a></dd></div>…</dl> */
.contact-list { display: grid; gap: 0.75rem; margin: 0; }
.contact-list > div { display: flex; gap: 0.75rem; align-items: baseline; }
.contact-list dt { flex: none; color: var(--contact-icon-fg, var(--accent)); }
.contact-list dt .icon { vertical-align: -0.15em; }
.contact-list dd { margin: 0; }

/* --- .site-footer — the page footer with link columns ----------------------
 *   <footer class="site-footer">
 *     <div class="site-footer-brand">…</div>
 *     <nav aria-label="Product"><h2>Product</h2><ul><li><a href="…">Pricing</a></li>…</ul></nav>
 *     …
 *     <div class="site-footer-bottom"><p>© 2026 Acme</p><ul class="cluster">…</ul></div>
 *   </footer> */
.site-footer {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--site-footer-min, 9rem)), 1fr));
  gap: 2rem 1.5rem;
  padding: var(--site-footer-padding, 2.5rem 1.5rem 1.5rem);
  background: var(--site-footer-bg, var(--surface));
  color: var(--site-footer-fg, var(--text));
  border-block-start: 1px solid var(--site-footer-border, var(--border));
  font-size: 0.9em;
}
.site-footer-brand { grid-column: 1 / -1; display: grid; gap: 0.5rem; max-inline-size: 28rem; }
.site-footer-brand p { margin: 0; color: var(--muted); }
.site-footer nav h2 {
  margin: 0 0 0.6rem;
  color: var(--site-footer-heading-fg, var(--text));
  font-size: 0.8em;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.site-footer nav ul { display: grid; gap: 0.15rem; margin: 0; padding: 0; list-style: none; }
.site-footer nav a { display: inline-block; min-block-size: 24px; line-height: 24px; color: var(--site-footer-link, var(--muted)); text-decoration: none; }
.site-footer nav a:hover { color: var(--text); text-decoration: underline; }
.site-footer-bottom {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding-block-start: 1.25rem;
  border-block-start: 1px solid var(--hairline, var(--border));
  color: var(--muted);
}
.site-footer-bottom > * { margin: 0; }
.site-footer-bottom ul { padding: 0; list-style: none; }
@media (min-width: 900px) { .site-footer-brand { grid-column: span 2; } }

/* --- .menu-feature and .mega-menu — rich menu items -----------------------
 * A flyout is a core [popover] .dropdown whose items are .menu-feature
 * links (icon, title, one line of help). A mega menu is the same popover
 * with .mega-menu: a wide grid of them, plus an optional .mega-menu-footer.
 *   <div class="dropdown mega-menu" popover id="products">
 *     <a class="menu-feature" href="…"><svg class="icon">…</svg>
 *       <span><strong>Analytics</strong><span>See where traffic comes from</span></span></a>
 *     …
 *     <div class="mega-menu-footer"><a href="…">Watch demo</a>…</div>
 *   </div> */
.menu-feature {
  display: flex;
  gap: 0.75rem;
  align-items: flex-start;
  padding: 0.6rem 0.75rem;
  border-radius: var(--radius);
  color: var(--text);
  text-decoration: none;
}
.menu-feature:is(:hover, :focus-visible) { background: var(--menu-feature-hover, var(--surface-2)); }
.menu-feature > .icon { flex: none; margin-block-start: 0.15em; color: var(--accent); }
.menu-feature > span { display: grid; gap: 0.1rem; }
.menu-feature > span > span { color: var(--muted); font-size: 0.85em; }
.mega-menu {
  inline-size: min(var(--mega-menu-width, 44rem), calc(100vw - 2rem));
  display: none;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
  gap: 0.25rem;
  padding: 0.5rem;
}
.mega-menu:popover-open { display: grid; }
.mega-menu-footer {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  margin: 0.25rem -0.5rem -0.5rem;
  padding: 0.75rem 1.25rem;
  background: var(--mega-menu-footer-bg, var(--surface-2));
  border-block-start: 1px solid var(--border);
}

/* Icons inside meta lines sit inline with their text. */
:is(.page-heading-meta, .post-meta) > * { display: inline-flex; align-items: center; gap: 0.3em; }
/* --- v5 shop: products, cart, checkout and orders ---------------------------
 * Product cards and grids, a product detail layout with a gallery, sale
 * prices, read-only star ratings and review summaries, choice cards
 * (delivery or payment options), category filters, cart lines, an order
 * summary and order history headers.
 * Builds on core's .card, .price, .badge, .btn, .stepper (forms.css),
 * .swatches, .rating, .progress and .list; every look reads a
 * --<component>-* token that falls back to the base palette.
 * Payment, stock, totals and validation are the app's.
 * See docs/components/shop.md. */

/* --- .product-grid and .product — a product card ---------------------------
 *   <ul class="product-grid">
 *     <li class="product">
 *       <img src="…" alt="Basic tee in black">
 *       <h3><a href="…" class="stretched-link">Basic tee</a></h3>
 *       <p class="product-meta">Black</p>
 *       <p class="price is-sm">$35</p>
 *     </li>
 *   </ul>
 * The image is square (--product-ratio), the whole card is one link via
 * core's .stretched-link, and a .badge in the card sits on the image. */
.product-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, var(--product-min, 13rem)), 1fr));
  gap: var(--product-gap, 2rem 1.25rem);
  margin: 0;
  padding: 0;
  list-style: none;
}
.product { position: relative; display: grid; gap: 0.3rem; align-content: start; }
.product > img,
.product > picture > img {
  inline-size: 100%;
  block-size: auto;
  aspect-ratio: var(--product-ratio, 1);
  object-fit: cover;
  background: var(--product-image-bg, var(--surface-2));
  border-radius: var(--product-radius, var(--radius));
}
.product :is(h3, h4) { margin: 0.4rem 0 0; font-size: 0.95em; font-weight: 500; }
.product :is(h3, h4) a { color: inherit; text-decoration: none; }
.product-meta { display: flex; align-items: center; gap: 0.3em; margin: 0; color: var(--muted); font-size: 0.85em; }
.product > .badge { position: absolute; inset: 0.5rem auto auto 0.5rem; z-index: var(--z-raised); }
.product > .btn { position: relative; z-index: var(--z-raised); justify-self: start; }

/* Sale and small prices, on core's .price. */
.price.is-sm { font-size: 1.1em; }
.price-was { color: var(--muted); font-size: 0.7em; font-weight: 400; text-decoration-line: line-through; }
.price-sale { color: var(--danger-text, var(--danger)); }

/* --- .stars — a read-only star rating --------------------------------------
 *   <span class="stars" style="--rating: 4.5" role="img" aria-label="4.5 out of 5 stars"></span>
 * Five glyphs filled to --rating (0–5) in --stars-fg. A theme can swap the
 * glyph with --stars-glyph. For a rating the user sets, use .rating. */
.stars {
  --_p: calc(var(--rating, 0) / 5 * 100%);
  display: inline-block;
  font-size: var(--stars-size, 1em);
  line-height: 1;
  letter-spacing: 0.1em;
  background: linear-gradient(90deg, var(--stars-fg, var(--warning)) var(--_p), var(--stars-empty, var(--border)) var(--_p));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.stars::before { content: var(--stars-glyph, "★★★★★") / ""; }
@media (forced-colors: active) { .stars { color: CanvasText; background: none; } }

/* --- .review-summary and .review -------------------------------------------
 *   <div class="review-summary">
 *     <p><span class="stars" style="--rating:4.2" role="img" aria-label="4.2 out of 5"></span> Based on 1,624 reviews</p>
 *     <dl><div><dt>5 stars</dt><dd><progress class="progress" max="100" value="63"></progress> 63%</dd></div>…</dl>
 *   </div>
 *   <article class="review"><header>…avatar, name, stars, date…</header><p>…</p></article> */
.review-summary { display: grid; gap: 0.75rem; }
.review-summary > p { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; margin: 0; color: var(--muted); }
.review-summary dl { display: grid; gap: 0.35rem; margin: 0; }
.review-summary dl > div { display: grid; grid-template-columns: 4.5rem 1fr 3rem; gap: 0.75rem; align-items: center; }
.review-summary dd { display: contents; }
.review-summary dd > .progress { margin: 0; }
.review { display: grid; gap: 0.5rem; padding-block: 1.25rem; border-block-start: 1px solid var(--hairline, var(--border)); }
.review > header { display: flex; flex-wrap: wrap; gap: 0.5rem 0.75rem; align-items: center; }
.review > header > :is(h3, h4, strong) { margin: 0; font-size: 1em; }
.review > header time { color: var(--muted); font-size: 0.85em; }
.review > p { margin: 0; }

/* --- .product-detail and .product-gallery ----------------------------------
 *   <div class="product-detail">
 *     <div class="product-gallery">
 *       <img src="front.jpg" alt="…">
 *       <div class="product-thumbs" role="radiogroup" aria-label="Images">…radio labels with thumbnails…</div>
 *     </div>
 *     <div class="product-info"><h1>…</h1><p class="price">…</p>…options…<button class="btn btn-primary">Add to bag</button></div>
 *   </div>
 * Gallery and info sit side by side from tablet width. The thumbnails are
 * .toggle-btn-style radios; the app (or a CSS :has() rule per image) swaps
 * the main image. */
.product-detail {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
  gap: var(--product-detail-gap, 2rem);
  align-items: start;
}
.product-gallery { display: grid; gap: 0.75rem; }
.product-gallery > img {
  inline-size: 100%;
  block-size: auto;
  aspect-ratio: var(--product-ratio, 1);
  object-fit: cover;
  background: var(--product-image-bg, var(--surface-2));
  border-radius: var(--product-radius, var(--radius));
}
.product-thumbs { display: grid; grid-template-columns: repeat(auto-fill, minmax(4rem, 1fr)); gap: 0.5rem; }
.product-thumbs > label { position: relative; display: block; border-radius: var(--radius); }
.product-thumbs > label > input { position: absolute; opacity: 0; inset: 0; margin: 0; }
.product-thumbs > label > img { display: block; inline-size: 100%; block-size: auto; aspect-ratio: 1; object-fit: cover; border-radius: inherit; }
.product-thumbs > label:has(:checked) { outline: 2px solid var(--accent); outline-offset: 2px; }
.product-thumbs > label:has(:focus-visible) { outline: 2px solid var(--focus); outline-offset: 2px; }
.product-info { display: grid; gap: 1rem; align-content: start; }
.product-info > * { margin: 0; }

/* --- .choice-card — a radio or checkbox drawn as a card --------------------
 * Delivery methods, payment options, plans.
 *   <div class="choice-cards" role="radiogroup" aria-label="Delivery">
 *     <label class="choice-card"><input type="radio" name="d" value="std" checked>
 *       <span><strong>Standard</strong><span>4–10 business days</span></span><span class="choice-card-aside">$5.00</span></label>
 *   </div> */
.choice-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--choice-card-min, 12rem)), 1fr));
  gap: 0.75rem;
}
.choice-card {
  display: flex;
  gap: 0.75rem;
  align-items: flex-start;
  min-block-size: var(--tap-min, 44px);
  padding: var(--choice-card-padding, 0.9rem 1rem);
  background: var(--choice-card-bg, var(--panel-bg, var(--surface)));
  color: var(--on-surface, var(--text));
  border: var(--choice-card-border-width, 1px) solid var(--choice-card-border, var(--border));
  border-radius: var(--choice-card-radius, var(--radius));
}
.choice-card > input { flex: none; margin: 0.2em 0 0; accent-color: var(--accent); }
.choice-card > span:not(.choice-card-aside) { display: grid; flex: 1 1 auto; gap: 0.15rem; }
.choice-card > span > span { color: var(--muted); font-size: 0.875em; }
.choice-card-aside { flex: none; font-weight: 600; }
.choice-card:has(> input:checked) {
  border-color: var(--choice-card-checked, var(--accent));
  box-shadow: inset 0 0 0 1px var(--choice-card-checked, var(--accent));
}
.choice-card:has(> input:focus-visible) { outline: 2px solid var(--focus); outline-offset: 2px; }
.choice-card:has(> input:disabled) { opacity: 0.5; }

/* --- .filters — a category filter rail --------------------------------------
 *   <form class="filters" aria-label="Filters">
 *     <details open><summary>Colour</summary>
 *       <label class="check"><input type="checkbox" name="c" value="white"> White</label>…
 *     </details>…
 *   </form>
 * Each group collapses; checked filters can repeat above the results as
 * .badge-button chips. On phones put the form in a core .drawer. */
.filters { display: grid; align-content: start; }
.filters > details { padding-block: 0.75rem; border-block-end: 1px solid var(--hairline, var(--border)); }
.filters > details > summary {
  display: flex;
  justify-content: space-between;
  align-items: center;
  min-block-size: var(--tap-min, 44px);
  font-weight: 600;
  list-style: none;
  cursor: default;
}
.filters > details > summary::-webkit-details-marker { display: none; }
.filters > details > summary::after { content: "+"; color: var(--muted); }
.filters > details[open] > summary::after { content: "−"; }
.filters > details > :not(summary) { margin-block-start: 0.35rem; }
.filters .check { display: flex; }

/* --- .cart-line — one line in a cart or order ------------------------------
 *   <ul class="cart"><li class="cart-line">
 *     <img src="…" alt="">
 *     <div><h3><a href="…">Basic tee</a></h3><p class="product-meta">Black · L</p></div>
 *     <div class="stepper">…</div>
 *     <p class="price is-sm">$35</p>
 *     <button class="btn-close" aria-label="Remove Basic tee"></button>
 *   </li></ul> */
.cart { display: grid; margin: 0; padding: 0; list-style: none; }
.cart-line {
  display: grid;
  grid-template-columns: var(--cart-image, 5rem) minmax(0, 1fr) auto auto auto;
  gap: 0.5rem 1rem;
  align-items: center;
  padding-block: 1rem;
  border-block-end: 1px solid var(--hairline, var(--border));
}
.cart-line > img {
  inline-size: 100%;
  block-size: auto;
  aspect-ratio: 1;
  object-fit: cover;
  background: var(--product-image-bg, var(--surface-2));
  border-radius: var(--radius);
}
.cart-line :is(h3, h4) { margin: 0; font-size: 1em; font-weight: 500; }
.cart-line :is(h3, h4) a { color: inherit; text-decoration: none; }
.cart-line > :is(.price, p) { margin: 0; }
/* Phones: image | details over the quantity | the action over the price. */
@media (max-width: 600px) {
  .cart-line { grid-template-columns: var(--cart-image, 4.5rem) minmax(0, 1fr) auto; }
  .cart-line > img { grid-row: 1 / span 2; }
  .cart-line > :nth-child(3) { grid-area: 2 / 2; justify-self: start; }
  .cart-line > :nth-child(4) { grid-area: 2 / 3; justify-self: end; }
  .cart-line > :nth-child(5) { grid-area: 1 / 3; justify-self: end; align-self: start; }
}

/* --- .order-summary — line totals with a grand total -----------------------
 *   <section class="order-summary" aria-labelledby="sum"><h2 id="sum">Order summary</h2>
 *     <dl><div><dt>Subtotal</dt><dd>$99.00</dd></div>…<div class="is-total"><dt>Total</dt><dd>$112.32</dd></div></dl>
 *     <button class="btn btn-primary">Checkout</button></section> */
.order-summary {
  display: grid;
  gap: 1rem;
  padding: var(--order-summary-padding, 1.25rem);
  background: var(--order-summary-bg, var(--surface-2));
  color: var(--text);
  border-radius: var(--panel-radius, var(--radius));
}
.order-summary > :is(h2, h3) { margin: 0; font-size: 1.1em; }
.order-summary dl { display: grid; margin: 0; }
.order-summary dl > div {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding-block: 0.6rem;
  border-block-end: 1px solid var(--hairline, var(--border));
}
.order-summary dl > div:last-child { border-block-end: 0; }
.order-summary dt { color: var(--muted); }
.order-summary dd { margin: 0; font-variant-numeric: tabular-nums; }
.order-summary .is-total :is(dt, dd) { color: var(--text); font-weight: 700; }
.order-summary .is-discount dd { color: var(--success-text, var(--success)); }
.order-summary > .btn { inline-size: 100%; justify-content: center; }

/* --- .order — one past order in an order history ---------------------------
 *   <article class="order">
 *     <header class="order-header">
 *       <dl><div><dt>Order number</dt><dd>WU88191111</dd></div><div><dt>Date placed</dt><dd><time …>Jan 6, 2026</time></dd></div>…</dl>
 *       <div class="cluster"><a class="btn btn-sm" href="…">View order</a></div>
 *     </header>
 *     <ul class="cart">…cart-lines without steppers…</ul>
 *   </article> */
.order {
  background: var(--order-bg, var(--panel-bg, var(--surface)));
  color: var(--on-surface, var(--text));
  border: var(--panel-border-width, 1px) solid var(--order-border, var(--border));
  border-radius: var(--panel-radius, var(--radius));
}
.order > .cart { padding-inline: 1rem; }
.order > .cart > .cart-line:last-child { border-block-end: 0; }
.order-header {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: 0.75rem 1.5rem;
  padding: 1rem;
  border-block-end: 1px solid var(--border);
}
.order-header dl { display: flex; flex-wrap: wrap; gap: 0.5rem 2rem; margin: 0; }
.order-header dt { color: var(--muted); font-size: 0.8em; }
.order-header dd { margin: 0; font-weight: 600; }

/* --- .shop-layout — filters beside results, or a form beside its summary --
 * Two columns from tablet width, one on phones. .is-summary puts a
 * narrower column (an .order-summary) at the end instead of the start. */
.shop-layout {
  display: grid;
  grid-template-columns: minmax(11rem, 15rem) minmax(0, 1fr);
  gap: var(--shop-layout-gap, 2rem);
  align-items: start;
}
.shop-layout.is-summary { grid-template-columns: minmax(0, 1.6fr) minmax(15rem, 1fr); }
@media (max-width: 760px) { .shop-layout, .shop-layout.is-summary { grid-template-columns: minmax(0, 1fr); } }
/* --- v5 event displays and signage ------------------------------------------
 * The pieces an event timer, a stage display or a room screen needs:
 * .countdown states (a live timer), .timeline-bar (the day as segments with a now
 * needle), .stage-edge (an alert ring round the screen), .is-attention /
 * .is-urgent, .lower-third (a message band), .qr-card, .word-cloud,
 * .ranked-list (a live Q&A wall), .count (a number that counts up), the
 * [data-enter] / [data-exit] motion vocabulary and the signage surface
 * (html[data-surface="signage"], .safe-area, data-fit).
 *
 * Unattended screens set html[data-motion="always"]: every animation in the
 * library then runs even when the device reports reduced motion (the build
 * copies each motion block for it). Never set it on an interactive page.
 * See docs/components/signage.md. */

/* --- .countdown states ------------------------------------------------------
 * Extends instruments.css's .countdown (parts with unit labels) for a live
 * timer, either form: <time class="countdown countdown-xl" data-state="running">09:59</time>
 * States: running (default), armed (cued, not started), paused, held (at 0),
 * alert1 (warning), alert2 (critical), overtime. Per-item colours are inline
 * --countdown-alert1/-alert2. A .countdown-sign ("+") and .countdown-tenths
 * (".4") sit smaller. It is never a live region: announce state changes in
 * a sibling .visually-hidden[role=status]. Not colour alone: paused shows a
 * pause glyph, overtime a "+", alert2 and overtime go heavier and pulse. */
.countdown[data-state] { white-space: nowrap; }
.countdown-xl { --countdown-size: var(--countdown-xl-size, clamp(3rem, 18vw, 14rem)); font-size: var(--countdown-size); font-weight: 700; line-height: 1; }
.countdown > :is(.countdown-sign, .countdown-tenths) { font-size: calc(var(--countdown-size, 1.75rem) * 0.55); }
.countdown-tenths { opacity: 0.75; }
.countdown[data-state="armed"] { --countdown-fg: var(--countdown-armed-fg, var(--muted)); --countdown-glow: none; }
.countdown[data-state="held"] { --countdown-fg: var(--countdown-held-fg, var(--muted)); --countdown-glow: none; }
.countdown[data-state="paused"] { --countdown-fg: var(--countdown-paused-fg, var(--warning-text, var(--warning))); }
.countdown[data-state="paused"]::before { content: "\23F8\FE0E" / ""; margin-inline-end: 0.15em; font-size: calc(var(--countdown-size, 1.75rem) * 0.45); align-self: center; }
.countdown[data-state="alert1"] { --countdown-fg: var(--countdown-alert1, var(--warning-text, var(--warning))); }
.countdown[data-state="alert2"] { --countdown-fg: var(--countdown-alert2, var(--danger-text, var(--danger))); }
.countdown[data-state="overtime"] { --countdown-fg: var(--countdown-overtime, var(--countdown-alert2, var(--danger-text, var(--danger)))); }
.countdown:is([data-state="paused"], [data-state="alert1"], [data-state="alert2"], [data-state="overtime"]) { --countdown-glow: 0 0 0.12em color-mix(in srgb, var(--countdown-fg) 55%, transparent); }
.countdown:is([data-state="alert2"], [data-state="overtime"]) { font-weight: 800; }
.countdown:is([data-state="alert2"], [data-state="overtime"]) > * { font-weight: 800; }
@keyframes countdown-pulse { 50% { opacity: var(--countdown-pulse-opacity, 0.72); } }
@media (prefers-reduced-motion: no-preference) {
  :root:not([data-motion="reduced"], [data-motion="none"]) .countdown:is([data-state="alert2"], [data-state="overtime"]) {
    animation: countdown-pulse var(--countdown-pulse-period, 1s) steps(2, jump-none) infinite;
  }
}

  :root[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) .countdown:is([data-state="alert2"], [data-state="overtime"]) {
    animation: countdown-pulse var(--countdown-pulse-period, 1s) steps(2, jump-none) infinite;
  }


/* --- .timeline-bar: the day as proportional segments ------------------------
 * <div class="timeline-bar" role="img" aria-label="Day 09:00–17:30, now 11:06, in Coffee">
 *   <span class="timeline-seg is-done" style="--start:0%; --size:12%">Welcome</span>
 *   <span class="timeline-seg" data-kind="break" style="--start:12%; --size:6%">Coffee</span>
 *   <span class="timeline-now" style="--at:15%"></span>
 * </div>
 * <div class="timeline-scale"><time>09:00</time><time>17:30</time></div>
 * Breaks are striped (not colour alone); --seg-color tints a segment. The
 * app keeps the bar's aria-label current; segments are decorative unless
 * they are links or buttons. Labels hide when the bar is under 30rem. */
.timeline-bar {
  position: relative;
  container-type: inline-size;
  block-size: var(--timeline-height, 2.6rem);
  overflow: hidden;
  background: var(--timeline-bg, var(--surface-2));
  border: 1px solid var(--timeline-border, var(--border));
  border-radius: var(--timeline-radius, var(--radius));
}
.timeline-seg {
  position: absolute;
  inset-block: 0;
  inset-inline-start: var(--start, 0%);
  inline-size: var(--size, 0%);
  box-sizing: border-box;
  overflow: hidden;
  padding: 0.3em 0.45em;
  border-inline-start: 1px solid var(--timeline-rule, var(--hairline));
  background: color-mix(in srgb, var(--seg-color, var(--accent)) var(--timeline-seg-alpha, 26%), transparent);
  color: var(--timeline-seg-fg, var(--text));
  font-size: var(--timeline-label-size, 0.75rem);
  line-height: 1.2;
  text-decoration: none;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.timeline-seg[data-kind="break"] {
  background: var(--timeline-break-pattern, repeating-linear-gradient(45deg, color-mix(in srgb, var(--border) 55%, transparent) 0 0.5rem, transparent 0.5rem 1rem));
}
.timeline-seg.is-done { filter: var(--timeline-done-filter, saturate(0.4) brightness(0.75)); }
.timeline-seg:is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))) { box-shadow: inset 0 -3px 0 var(--seg-color, var(--accent)); font-weight: 600; }
:is(a, button).timeline-seg { border-block: 0; border-inline-end: 0; }
:is(a, button).timeline-seg:focus-visible { z-index: var(--z-raised); outline: 2px solid var(--focus); outline-offset: -2px; }
.timeline-now {
  position: absolute;
  z-index: calc(var(--z-raised) + 1);
  inset-block: 0;
  inset-inline-start: var(--at, 0%);
  inline-size: 2px;
  translate: -50% 0;
  background: var(--timeline-now-color, var(--accent));
  box-shadow: 0 0 0 1px var(--timeline-bg, var(--surface-2));
  pointer-events: none;
}
.timeline-now::before {
  content: "";
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 50%;
  inline-size: 0.7rem;
  block-size: 0.45rem;
  translate: -50% 0;
  background: inherit;
  clip-path: polygon(0 0, 100% 0, 50% 100%);
}
.timeline-scale {
  display: flex;
  justify-content: space-between;
  margin-block-start: 0.2rem;
  color: var(--timeline-scale-fg, var(--muted));
  font: 0.75rem/1.2 var(--font-mono);
  font-variant-numeric: tabular-nums;
}
@container (inline-size < 30rem) {
  .timeline-seg { padding: 0; font-size: 0; }
}

/* --- .stage-edge, .is-attention, .is-urgent ---------------------------------
 * .stage-edge[data-level="warn|danger"] rings the element (the whole screen
 * on a stage display) inside its edge, sized in vmin so it reads in
 * peripheral vision; --stage-alert sets the colour (an operator's choice).
 * .is-attention blinks (1 Hz, steps) and .is-urgent pulses; without motion
 * both show a steady outline instead. Carry the state in text too. */
.stage-edge { position: relative; }
.stage-edge[data-level="warn"] { --_edge: var(--stage-alert, var(--warning)); --_edge-w: var(--stage-edge-warn-width, 0.45vmin); }
.stage-edge[data-level="danger"] { --_edge: var(--stage-alert, var(--danger)); --_edge-w: var(--stage-edge-danger-width, 0.7vmin); }
.stage-edge:is([data-level="warn"], [data-level="danger"])::after {
  content: "";
  position: absolute;
  z-index: var(--z-overlay, 30);
  inset: 0;
  box-shadow: inset 0 0 0 var(--_edge-w) var(--_edge);
  pointer-events: none;
}
:is(.is-attention, .is-urgent) { outline: 0.12em solid currentColor; outline-offset: 0.12em; }
@keyframes attention-blink { 50% { opacity: var(--attention-low, 0.12); } }
@media (prefers-reduced-motion: no-preference) {
  :root:not([data-motion="reduced"], [data-motion="none"]) .is-attention { outline: none; animation: attention-blink var(--attention-period, 1s) steps(1) infinite; }
  :root:not([data-motion="reduced"], [data-motion="none"]) .is-urgent { outline: none; animation: countdown-pulse var(--countdown-pulse-period, 1s) ease-in-out infinite; }
}

  :root[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) .is-attention { outline: none; animation: attention-blink var(--attention-period, 1s) steps(1) infinite; }
  :root[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) .is-urgent { outline: none; animation: countdown-pulse var(--countdown-pulse-period, 1s) ease-in-out infinite; }


/* --- .lower-third: a message band over a display ---------------------------
 * <div class="lower-third" role="status" style="--banner-edge: #ff8a00"><p>Wrap up</p></div>
 * Sits over the lower third of its positioned parent (.is-center: the
 * middle; .is-fixed: the viewport). The operator's colour is the leading
 * edge only; the text carries the meaning. */
.lower-third {
  position: absolute;
  z-index: var(--z-overlay, 30);
  inset-inline: 0;
  inset-block-end: var(--lower-third-bottom, max(12vh, env(safe-area-inset-bottom, 0px)));
  display: flex;
  justify-content: center;
  padding-inline: var(--space-l);
  pointer-events: none;
}
.lower-third.is-fixed { position: fixed; }
.lower-third.is-center { inset-block: 0; align-items: center; }
.lower-third > * {
  max-inline-size: 92%;
  margin: 0;
  padding: 0.35em 0.8em;
  border-inline-start: var(--banner-edge-width, 0.3em) solid var(--banner-edge, var(--accent));
  border-radius: var(--lower-third-radius, var(--radius));
  background: var(--lower-third-bg, color-mix(in srgb, var(--surface) 94%, transparent));
  color: var(--lower-third-fg, var(--text));
  box-shadow: var(--lower-third-shadow, 0 0.4rem 1.6rem rgb(0 0 0 / 0.45));
  font-size: var(--banner-size, clamp(1.7rem, 6vh, 6rem));
  font-weight: 700;
  line-height: 1.1;
  text-wrap: balance;
}

/* --- .qr-card: a scannable join card ---------------------------------------
 * <div class="qr-card"><img class="qr-card-code" src="join.svg" alt="QR code: join at timerpi.local/j">
 *   <span class="qr-card-label">Join the Q&amp;A</span><span class="qr-card-url">timerpi.local/j</span></div>
 * The code always sits on a light quiet zone with crisp pixels, whatever
 * the theme does to images; the URL is always printed beside it.
 * .is-corner pins it bottom-end inside the safe area. */
.qr-card {
  display: inline-grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-content: center;
  align-items: center;
  gap: 0.2em 0.8em;
  padding: var(--qr-card-pad, var(--space-s));
  background: var(--qr-card-bg, var(--surface));
  color: var(--qr-card-fg, var(--text));
  border: 1px solid var(--qr-card-border, var(--border));
  border-radius: var(--qr-card-radius, var(--radius));
}
.qr-card-code {
  grid-row: span 3;
  inline-size: var(--qr-size, 7rem);
  aspect-ratio: 1;
  box-sizing: border-box;
  padding: var(--qr-quiet, 0.45rem);
  background: var(--qr-bg, #ffffff);
  color: var(--qr-fg, #000000);
  border-radius: 2px;
  image-rendering: pixelated;
  filter: none;
  mix-blend-mode: normal;
  opacity: 1;
}
.qr-card-label { font-weight: 700; }
.qr-card-url { font: 0.9em/1.3 var(--font-mono); overflow-wrap: anywhere; }
.qr-card.is-corner {
  position: fixed;
  z-index: var(--z-overlay, 30);
  inset-block-end: max(var(--safe-inset-block, 1rem), env(safe-area-inset-bottom, 0px));
  inset-inline-end: max(var(--safe-inset-inline, 1rem), env(safe-area-inset-right, 0px));
}
@media (forced-colors: active) { .qr-card-code { forced-color-adjust: none; } }

/* --- .word-cloud ------------------------------------------------------------
 * <ul class="word-cloud" aria-label="Audience words">
 *   <li class="word-cloud-word" style="--weight:.8; --x:-.2; --y:.1; --rotate:-90deg" data-tone="2">
 *     coffee <span class="visually-hidden">, 14 votes</span></li>
 * The app places each word (--x/--y from -1 to 1 around the centre) and
 * weights it (--weight 0–1 maps to --word-cloud-min…-max); this paints and
 * moves it. data-tone="1..6" picks a chart colour; .is-top glows; .is-new
 * fades in. */
.word-cloud {
  position: relative;
  min-block-size: var(--word-cloud-height, 18rem);
  margin: 0;
  padding: 0;
  overflow: hidden;
  list-style: none;
}
.word-cloud-word {
  position: absolute;
  inset-inline-start: calc(50% + var(--x, 0) * 45%);
  inset-block-start: calc(50% + var(--y, 0) * 45%);
  translate: -50% -50%;
  rotate: var(--rotate, 0deg);
  --_tone: var(--chart-series-1, var(--accent));
  /* Mixed toward the text colour so every tone keeps 3:1 at word size. */
  color: color-mix(in oklab, var(--_tone) var(--word-cloud-tone-strength, 35%), var(--text));
  font-size: calc(var(--word-cloud-min, 1rem) + var(--weight, 0.5) * (var(--word-cloud-max, 4rem) - var(--word-cloud-min, 1rem)));
  font-weight: 700;
  line-height: 1;
  white-space: nowrap;
}
.word-cloud-word[data-tone="2"] { --_tone: var(--chart-series-2, var(--accent-2, var(--accent))); }
.word-cloud-word[data-tone="3"] { --_tone: var(--chart-series-3, var(--accent)); }
.word-cloud-word[data-tone="4"] { --_tone: var(--chart-series-4, var(--accent)); }
.word-cloud-word[data-tone="5"] { --_tone: var(--chart-series-5, var(--accent)); }
.word-cloud-word[data-tone="6"] { --_tone: var(--chart-series-6, var(--accent)); }
.word-cloud-word.is-top { --_tone: var(--word-cloud-top, var(--accent)); text-shadow: 0 0 0.4em color-mix(in srgb, currentColor 45%, transparent); }

/* --- .ranked-list: a live ranked wall ---------------------------------------
 * <ol class="ranked-list is-positioned" style="--ranked-height: 30rem">
 *   <li class="ranked-item is-spotlight" aria-current="true" style="--slot-y: 0px">… <span class="ranked-votes">12</span></li>
 * The DOM order is the rank order (screen readers hear the rank); with
 * .is-positioned the app also sets each item's --slot-y and the item slides
 * there, so a reorder animates. .is-spotlight (a heavy accent border),
 * .is-answered (success; say so in text), .is-leaving (fades out), .is-new
 * (rises in). Without motion the order just changes. */
.ranked-list { position: relative; display: grid; gap: var(--ranked-gap, 0.6rem); margin: 0; padding: 0; list-style: none; }
.ranked-list.is-positioned { display: block; block-size: var(--ranked-height, auto); overflow: hidden; }
.ranked-item {
  display: flex;
  align-items: center;
  gap: 0.8em;
  box-sizing: border-box;
  padding: var(--ranked-item-pad, 0.5em 0.8em);
  background: var(--ranked-item-bg, var(--card-bg, var(--panel-bg, var(--surface))));
  color: var(--ranked-item-fg, var(--text));
  border: var(--ranked-border-width, 2px) solid var(--ranked-border, var(--card-border, var(--border)));
  border-radius: var(--ranked-radius, var(--card-radius, var(--radius)));
}
.ranked-list.is-positioned > .ranked-item { position: absolute; inset-inline: 0; inset-block-start: 0; translate: 0 var(--slot-y, 0px); }
.ranked-item.is-spotlight { border-width: var(--ranked-spot-width, 6px); border-color: var(--ranked-spot, var(--accent)); }
.ranked-item.is-answered {
  border-color: var(--success);
  background: color-mix(in srgb, var(--success) 18%, var(--ranked-item-bg, var(--card-bg, var(--panel-bg, var(--surface)))));
}
.ranked-item.is-leaving { opacity: 0; }
.ranked-votes { margin-inline-start: auto; font: 700 1.2em/1 var(--font-mono); font-variant-numeric: tabular-nums; }
@keyframes ranked-rise { from { opacity: 0; translate: 0 calc(var(--slot-y, 0px) + 2rem); } }
@media (prefers-reduced-motion: no-preference) {
  :root:not([data-motion="reduced"], [data-motion="none"]) .ranked-item {
    transition: translate var(--ranked-move, 0.8s cubic-bezier(0.2, 0.8, 0.2, 1)), opacity 1s ease, border-color 0.4s, background-color 0.4s;
  }
  :root:not([data-motion="reduced"], [data-motion="none"]) .ranked-item.is-new { animation: ranked-rise 0.8s cubic-bezier(0.2, 0.8, 0.2, 1) both; }
}

  :root[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) .ranked-item {
    transition: translate var(--ranked-move, 0.8s cubic-bezier(0.2, 0.8, 0.2, 1)), opacity 1s ease, border-color 0.4s, background-color 0.4s;
  }
  :root[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) .ranked-item.is-new { animation: ranked-rise 0.8s cubic-bezier(0.2, 0.8, 0.2, 1) both; }


/* --- Count-up: .count and .is-counting -------------------------------------
 * <data class="count" value="42" style="--count:42; --stagger-index:1"><span class="visually-hidden">42</span></data>
 * <div class="meter is-counting" …><div class="meter-fill" style="--meter-level:42%"></div></div>
 * The number counts up from 0 (or --count-from) to --count, and a meter's
 * or progress bar's fill grows from empty, over --count-duration, each
 * --stagger-index later than the last. The final value is visually-hidden
 * text inside (what assistive tech reads); the digits you see are drawn,
 * in a box --count-digits (3) wide. Without motion they show the result. */
@property --count { syntax: "<integer>"; inherits: true; initial-value: 0; }
.count {
  display: inline-block;
  min-inline-size: calc(var(--count-digits, 3) * 1ch);
  font-variant-numeric: tabular-nums;
  text-align: end;
  counter-reset: count-up var(--count);
}
.count::after { content: counter(count-up) / ""; }
@keyframes count-up { from { --count: var(--count-from, 0); } }
@keyframes count-fill { from { width: 0; } }
@media (prefers-reduced-motion: no-preference) {
  :root:not([data-motion="reduced"], [data-motion="none"]) .count {
    animation: count-up var(--count-duration, 900ms) cubic-bezier(0.33, 1, 0.68, 1) both;
    animation-delay: calc(var(--stagger-index, 0) * var(--stagger, 120ms));
  }
  :root:not([data-motion="reduced"], [data-motion="none"]) .is-counting :is(.meter-fill, .progress-bar) {
    animation: count-fill var(--count-duration, 900ms) cubic-bezier(0.33, 1, 0.68, 1) both;
    animation-delay: calc(var(--stagger-index, 0) * var(--stagger, 120ms));
  }
}

  :root[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) .count {
    animation: count-up var(--count-duration, 900ms) cubic-bezier(0.33, 1, 0.68, 1) both;
    animation-delay: calc(var(--stagger-index, 0) * var(--stagger, 120ms));
  }
  :root[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) .is-counting :is(.meter-fill, .progress-bar) {
    animation: count-fill var(--count-duration, 900ms) cubic-bezier(0.33, 1, 0.68, 1) both;
    animation-delay: calc(var(--stagger-index, 0) * var(--stagger, 120ms));
  }


/* --- Enter and exit: [data-enter] / [data-exit] -----------------------------
 * <section class="widget is-entering" data-enter="pop" data-exit="slide">
 * The app adds .is-entering when the element appears and .is-exiting before
 * it removes it (after --motion-exit-duration). fade | slide | pop.
 * Durations: --motion-enter-duration, --motion-exit-duration. */
[data-enter="fade"] { --_enter: enter-fade; }
[data-enter="slide"] { --_enter: enter-slide; }
[data-enter="pop"] { --_enter: enter-pop; }
[data-exit="fade"] { --_exit: exit-fade; }
[data-exit="slide"] { --_exit: exit-slide; }
[data-exit="pop"] { --_exit: exit-pop; }
@keyframes enter-fade { from { opacity: 0; } }
@keyframes enter-slide { from { opacity: 0; translate: 0 1.5rem; } }
@keyframes enter-pop { from { opacity: 0; scale: 0.85; } 70% { scale: 1.03; } }
@keyframes exit-fade { to { opacity: 0; } }
@keyframes exit-slide { to { opacity: 0; translate: 0 1.5rem; } }
@keyframes exit-pop { to { opacity: 0; scale: 0.85; } }
@media (prefers-reduced-motion: no-preference) {
  :root:not([data-motion="reduced"], [data-motion="none"]) [data-enter].is-entering { animation: var(--_enter) var(--motion-enter-duration, 450ms) var(--motion-ease, ease-out) both; }
  :root:not([data-motion="reduced"], [data-motion="none"]) [data-exit].is-exiting { animation: var(--_exit) var(--motion-exit-duration, 400ms) ease-in forwards; }
}

  :root[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) [data-enter].is-entering { animation: var(--_enter) var(--motion-enter-duration, 450ms) var(--motion-ease, ease-out) both; }
  :root[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) [data-exit].is-exiting { animation: var(--_exit) var(--motion-exit-duration, 400ms) ease-in forwards; }


/* --- The signage surface -----------------------------------------------------
 * html[data-surface="signage"]: rem sizes scale with the screen (the root
 * is --signage-root, 1.5vh; 1.5vw in portrait), so 720p, 1080p and 4K look
 * the same, and nothing scrolls. .safe-area keeps content inside the TV's
 * overscan (--safe-inset-block/-inline, 4vh/4vw, or the device's own
 * inset if larger); data-fit="cover" drops the inset, "shadow" vignettes
 * the edge. Signage is read-only: operator pages keep the OS root size. */
html[data-surface="signage"] { font-size: var(--signage-root, 1.5vh); overflow: hidden; scrollbar-width: none; }
@media (orientation: portrait) {
  html[data-surface="signage"] { font-size: var(--signage-root-portrait, 1.5vw); }
}
html[data-surface="signage"] body { overflow: hidden; }
.safe-area {
  box-sizing: border-box;
  padding:
    max(var(--safe-inset-block, 4vh), env(safe-area-inset-top, 0px))
    max(var(--safe-inset-inline, 4vw), env(safe-area-inset-right, 0px))
    max(var(--safe-inset-block, 4vh), env(safe-area-inset-bottom, 0px))
    max(var(--safe-inset-inline, 4vw), env(safe-area-inset-left, 0px));
}
.safe-area[data-fit="cover"] { --safe-inset-block: 0px; --safe-inset-inline: 0px; }
.safe-area[data-fit="shadow"] { box-shadow: inset 0 0 6vmin 2vmin rgb(0 0 0 / 0.6); }
/* --- v5 social group (PLAN.md §19 "Collaboration", "Notifications and
 * activity") ------------------------------------------------------------------
 * Presence: .presence avatar stack, status on any .avatar[data-status],
 * .presence-status labels, per-user colours (data-color="1..6").
 * Live: .collab-layer holding .live-cursor and .comment-pin at --x/--y.
 * Comments: mark.comment-anchor, details.comment-thread (core .thread and
 * .message inside), .message.is-reply, .mention chips; the suggestion list
 * is forms.css's .combobox + .listbox (options holding an .avatar lay out
 * as a row here).
 * Notifications: .notifications (in a .panel, .drawer[popover] or
 * .popover[popover]), .notification items with unread/group/actions, the
 * do-not-disturb switch, and .has-badge.is-muted on the bell.
 * Activity: ol.timeline.activity (instruments.css's timeline with avatars).
 * Typing stays core's .typing, empty states core's .empty-state.
 * Docs: docs/components/social.md. */

/* --- Per-user colours -------------------------------------------------------
 * A short list a theme can repaint; defaults follow the chart palette, which
 * every theme already keeps distinct. data-color="n" picks one for an
 * avatar, a live cursor or a comment pin. */
:root {
  --presence-color-1: var(--chart-series-1, var(--accent));
  --presence-color-2: var(--chart-series-2, var(--success));
  --presence-color-3: var(--chart-series-3, var(--warning));
  --presence-color-4: var(--chart-series-4, var(--danger));
  --presence-color-5: var(--chart-series-5, var(--accent-2, var(--accent)));
  --presence-color-6: var(--chart-series-6, var(--muted));
}
[data-color="1"] { --user-color: var(--presence-color-1); }
[data-color="2"] { --user-color: var(--presence-color-2); }
[data-color="3"] { --user-color: var(--presence-color-3); }
[data-color="4"] { --user-color: var(--presence-color-4); }
[data-color="5"] { --user-color: var(--presence-color-5); }
[data-color="6"] { --user-color: var(--presence-color-6); }
/* Text on a user colour: black or white, whichever the colour's lightness
   calls for (relative colour syntax; older engines keep the fallback). */
:is(.live-cursor, .comment-pin)[data-color] {
  --user-fg: var(--live-cursor-fg, #fff);
  --user-fg: oklch(from var(--user-color) clamp(0, (0.56 - l) * 1000, 1) 0 0);
}
.avatar[data-color] {
  --avatar-bg: color-mix(in srgb, var(--user-color) 28%, var(--surface));
  --avatar-fg: var(--text);
}

/* --- Status on an avatar ------------------------------------------------------
 *   <span class="avatar" data-status="away">AT<span class="visually-hidden">, away</span></span>
 * Four shapes, so colour is never the only signal: online a filled disc,
 * away a crescent, busy a disc with a bar, offline a hollow ring. */
.avatar[data-status] {
  position: relative;
  overflow: visible;
}
.avatar[data-status] > img { border-radius: inherit; }
:is(.avatar[data-status], .presence-status)::after {
  content: "";
  box-sizing: border-box;
  inline-size: var(--presence-dot-size, 0.7rem);
  block-size: var(--presence-dot-size, 0.7rem);
  border-radius: 50%;
  background: var(--presence-dot);
  box-shadow: 0 0 0 var(--presence-dot-ring-width, 2px) var(--presence-dot-ring, var(--surface));
}
.avatar[data-status]::after {
  position: absolute;
  z-index: var(--z-raised); /* above the next avatar in a stack */
  inset-block-end: -1px;
  inset-inline-end: -1px;
}
[data-status="online"] { --presence-dot: var(--presence-online, var(--success)); }
[data-status="away"] {
  --presence-dot: radial-gradient(circle at 72% 28%, var(--presence-dot-ring, var(--surface)) 0 34%, var(--presence-away, var(--warning)) 37%);
}
[data-status="busy"] {
  --presence-dot: linear-gradient(var(--presence-dot-ring, var(--surface)) 0 0) center / 60% 22% no-repeat, var(--presence-busy, var(--danger));
}
[data-status="offline"] { --presence-dot: var(--presence-dot-ring, var(--surface)); }
:is(.avatar, .presence-status)[data-status="offline"]::after {
  box-shadow:
    0 0 0 var(--presence-dot-ring-width, 2px) var(--presence-dot-ring, var(--surface)),
    inset 0 0 0 0.16rem var(--presence-offline, var(--muted));
}
/* The shape as a label: <span class="presence-status" data-status="busy">Busy</span> */
.presence-status {
  display: inline-flex;
  flex-direction: row-reverse;
  justify-content: flex-end;
  align-items: center;
  gap: 0.45em;
  font-size: 0.85em;
  color: var(--presence-status-fg, var(--muted));
}
.presence-status::after { flex: none; }

/* --- .presence — the avatar stack ("5 people viewing") ---------------------
 *   <button class="presence" popovertarget="viewers" aria-label="5 people viewing">
 *     <span class="avatar" data-status="online">AT</span>…
 *     <span class="avatar presence-more">+3</span>
 *   </button>
 * A <div role="group"> for a stack that opens nothing. Avatars overlap by
 * --presence-overlap, each ringed in the surface colour. The button form
 * opens a popover listing who is viewing (.presence-list). Overlap defaults
 * to a quarter of the avatar size. */
.presence {
  display: inline-flex;
  align-items: center;
  max-inline-size: 100%;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  vertical-align: middle;
}
button.presence {
  min-block-size: var(--tap-min);
  border-radius: var(--presence-radius, 999px);
  touch-action: manipulation;
}
@media (hover: hover) {
  button.presence:hover > .avatar { box-shadow: 0 0 0 2px var(--presence-ring-hover, var(--accent)); }
}
button.presence:is(:focus-visible, [aria-expanded="true"]) > .avatar { box-shadow: 0 0 0 2px var(--presence-ring-hover, var(--accent)); }
.presence > .avatar {
  margin-inline-start: calc(-1 * var(--presence-overlap, var(--avatar-size, 2.2rem) * 0.25));
  box-shadow: 0 0 0 var(--presence-ring-width, 2px) var(--presence-ring, var(--surface));
}
.presence > .avatar:first-child { margin-inline-start: 0; }
.presence-more {
  --avatar-bg: var(--presence-more-bg, var(--surface-2));
  --avatar-fg: var(--presence-more-fg, var(--text));
  font-variant-numeric: tabular-nums;
}
.presence-label {
  margin-inline-start: var(--space-xs);
  font-size: 0.85em;
  color: var(--presence-label-fg, var(--muted));
  white-space: nowrap;
}
/* Who's viewing: rows of avatar, name and status. Lives in a .popover. */
.presence-list {
  display: grid;
  gap: var(--space-xs);
  min-inline-size: min(14rem, calc(100vw - 4rem));
}
.presence-list > li {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  min-inline-size: 0;
}
.presence-list > li > .presence-status { margin-inline-start: auto; }

/* --- .collab-layer, .live-cursor ------------------------------------------
 *   <div class="collab-layer">…document…
 *     <span class="live-cursor" data-color="2" style="--x: 40%; --y: 30%" aria-hidden="true">Leo</span>
 *   </div>
 * The app moves cursors by setting --x/--y (lengths or percentages of the
 * layer). The tip of the arrow is the point. Cursors are decoration: the
 * presence stack carries who is here for assistive tech. */
.collab-layer {
  position: relative;
  isolation: isolate;
  overflow: clip;
}
.live-cursor {
  position: absolute;
  z-index: var(--z-raised);
  left: var(--x, 0);
  top: var(--y, 0);
  inline-size: max-content;
  max-inline-size: 12rem;
  margin: 1.05rem 0 0 0.75rem;
  padding: 0.25rem 0.5rem;
  border-radius: var(--live-cursor-radius, var(--radius));
  background: var(--user-color, var(--accent));
  color: var(--user-fg, var(--on-accent));
  box-shadow: var(--live-cursor-shadow, 0 1px 3px rgba(0, 0, 0, 0.35));
  font-size: 0.72rem;
  font-weight: 600;
  line-height: 1.3;
  overflow-wrap: anywhere;
  pointer-events: none;
}
/* The arrow: its tip is the (--x, --y) point. */
.live-cursor::before {
  content: "";
  position: absolute;
  left: -0.75rem;
  top: -1.05rem;
  inline-size: 1rem;
  block-size: 1.15rem;
  background: var(--user-color, var(--accent));
  clip-path: polygon(0 0, 100% 62%, 52% 64%, 30% 100%);
  filter: drop-shadow(0 0 1px var(--live-cursor-outline, var(--surface)));
}
/* Idle: a faded arrow and a neutral label, so the name stays readable. */
.live-cursor.is-idle {
  background: var(--live-cursor-idle-bg, var(--surface-2));
  color: var(--live-cursor-idle-fg, var(--text));
  box-shadow: inset 0 0 0 1px var(--user-color, var(--border));
}
.live-cursor.is-idle::before { opacity: var(--live-cursor-idle-opacity, 0.45); }
@media (prefers-reduced-motion: no-preference) {
  html:not([data-motion="reduced"], [data-motion="none"]) .live-cursor {
    transition: left 0.12s linear, top 0.12s linear, opacity 0.3s ease;
  }
  html:not([data-motion="reduced"], [data-motion="none"]) .comment-thread-summary::before,
  html:not([data-motion="reduced"], [data-motion="none"]) .notification-summary::after {
    transition: rotate 0.15s ease;
  }
}

  html[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) .live-cursor {
    transition: left 0.12s linear, top 0.12s linear, opacity 0.3s ease;
  }
  html[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) .comment-thread-summary::before,html[data-motion="always"]:not([data-motion="reduced"], [data-motion="none"]) .notification-summary::after {
    transition: rotate 0.15s ease;
  }


/* --- Comments ---------------------------------------------------------------
 * mark.comment-anchor: the text a thread is about. Tinted and underlined
 * (dashed while idle, solid when its thread is open), so colour is not the
 * only signal. */
.comment-anchor {
  background: var(--comment-anchor-bg, color-mix(in srgb, var(--warning) 22%, transparent));
  padding: 0.25em;
  color: inherit;
  text-decoration: underline dashed var(--comment-anchor-line, var(--warning-text, var(--warning)));
  text-decoration-thickness: 2px;
  text-underline-offset: 0.2em;
  border-radius: 2px;
  box-decoration-break: clone;
  -webkit-box-decoration-break: clone;
}
.comment-anchor:is(:is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))), [aria-current]:not([aria-current="false"])) {
  background: var(--comment-anchor-bg-active, color-mix(in srgb, var(--warning) 40%, transparent));
  text-decoration-style: solid;
}
.comment-anchor.is-resolved { background: none; text-decoration-color: var(--muted); }

/* .comment-pin: a speech-bubble marker with a count or initials. A button.
 * Inside .collab-layer it sits at --x/--y (its tail is the point); anywhere
 * else it is an inline button (a margin rail, a table cell). */
.comment-pin {
  display: inline-grid;
  place-items: center;
  min-inline-size: var(--tap-min);
  min-block-size: var(--tap-min);
  padding: 0;
  border: 0;
  background: none;
  color: var(--user-fg, var(--comment-pin-fg, var(--on-accent)));
  font: inherit;
  font-size: 0.72rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  touch-action: manipulation;
  vertical-align: middle;
}
.comment-pin::before {
  content: "";
  grid-area: 1 / 1;
  inline-size: var(--comment-pin-size, 1.6rem);
  block-size: var(--comment-pin-size, 1.6rem);
  border-radius: var(--comment-pin-radius, 50% 50% 50% 0);
  background: var(--user-color, var(--comment-pin-bg, var(--accent)));
  box-shadow: 0 0 0 2px var(--comment-pin-ring, var(--surface)), var(--comment-pin-shadow, 0 1px 3px rgba(0, 0, 0, 0.4));
}
.comment-pin > * { grid-area: 1 / 1; position: relative; }
.collab-layer > .comment-pin {
  position: absolute;
  z-index: var(--z-raised);
  left: var(--x, 0);
  top: var(--y, 0);
  translate: calc(-50% + var(--comment-pin-size, 1.6rem) / 2) calc(-50% - var(--comment-pin-size, 1.6rem) / 2);
}
.comment-pin:is([aria-expanded="true"], :is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked)))) { --comment-pin-size: 2rem; --comment-pin-ring: var(--comment-pin-ring-active, var(--text)); }
.collab-layer > .comment-pin:is([aria-expanded="true"], :is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked)))) { z-index: calc(var(--z-raised) + 1); }
.comment-pin.is-resolved {
  --user-color: var(--comment-pin-resolved-bg, var(--surface-2));
  --user-fg: var(--comment-pin-resolved-fg, var(--muted));
}
.comment-pin.is-resolved::before { box-shadow: 0 0 0 2px var(--comment-pin-ring, var(--surface)), inset 0 0 0 1px var(--border); }
@media (hover: hover) {
  .comment-pin:hover::before { box-shadow: 0 0 0 2px var(--comment-pin-ring-active, var(--text)); }
}
.comment-pin:focus-visible { outline: 2px solid var(--focus); outline-offset: -4px; border-radius: 50%; }

/* details.comment-thread: the summary quotes the anchored text and says how
 * many replies there are; closing it collapses the thread. .is-resolved
 * mutes it (put a "Resolved" badge in the summary: text, not colour). */
.comment-thread {
  min-inline-size: 0;
  background: var(--comment-thread-bg, var(--surface));
  color: var(--text);
  border: var(--comment-thread-border-width, 1px) solid var(--comment-thread-border, var(--border));
  border-radius: var(--comment-thread-radius, var(--radius));
  box-shadow: var(--comment-thread-shadow, none);
  overflow-wrap: anywhere;
}
.comment-thread:is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))) { border-color: var(--comment-thread-border-active, var(--accent)); box-shadow: 0 0 0 1px var(--comment-thread-border-active, var(--accent)); }
.comment-thread.is-resolved { --comment-thread-bg: var(--comment-thread-resolved-bg, var(--surface)); border-style: dashed; }
.comment-thread-summary {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3xs) var(--space-xs);
  min-block-size: var(--tap-min);
  padding: var(--space-xs) var(--comment-thread-pad, var(--space-s));
  font-size: 0.85em;
  list-style: none;
  touch-action: manipulation;
}
.comment-thread-summary::-webkit-details-marker { display: none; }
.comment-thread-summary::before {
  content: var(--comment-thread-marker, "\203A");
  display: inline-block;
  inline-size: 0.8em;
  color: var(--muted);
}
.comment-thread[open] > .comment-thread-summary::before { rotate: 90deg; }
.comment-thread[open] > .comment-thread-summary { border-block-end: 1px solid var(--comment-thread-rule, var(--hairline)); }
.comment-thread-summary:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
@media (hover: hover) {
  .comment-thread-summary:hover { background: var(--comment-thread-summary-bg-hover, color-mix(in srgb, var(--text) 6%, transparent)); }
}
.comment-thread-quote {
  flex: 1 1 10rem;
  min-inline-size: 0;
  padding-inline-start: var(--space-xs);
  border-inline-start: 3px solid var(--comment-anchor-line, var(--warning-text, var(--warning)));
  color: var(--muted);
  font-style: italic;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.comment-thread-count { color: var(--muted); white-space: nowrap; }
.comment-thread.is-resolved .comment-thread-quote { border-inline-start-color: var(--muted); text-decoration: line-through; }
.comment-thread > .thread,
.comment-thread-footer { padding: var(--comment-thread-pad, var(--space-s)); }
.comment-thread > .thread { --thread-gap: var(--space-s); }
/* Comments read as a list, not a chat: bubbles drop their fill and border. */
.comment-thread .message-bubble {
  --message-bg: transparent;
  --message-border: transparent;
  --message-shadow: none;
  --message-padding: 0;
  max-inline-size: none;
  flex: 1;
}
.message.is-reply { margin-inline-start: calc(var(--avatar-size, 2.2rem) + 0.6rem); }
.message.is-reply > .avatar { --avatar-size: 1.6rem; font-size: 0.7em; }
.comment-thread-footer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs);
  border-block-start: 1px solid var(--comment-thread-rule, var(--hairline));
}
.comment-thread-footer > .input-group { flex: 1 1 12rem; min-inline-size: 0; }
/* Reaction chips are core's .badge-button in .message-reactions; a hidden
   checkbox (or aria-pressed) makes one toggle. */
.message-reactions > .badge-button { gap: 0.3em; padding: 0.35em 0.7em; }
.message-reactions > .badge-button[aria-pressed="true"] {
  background: var(--badge-bg-active, var(--accent));
  color: var(--badge-fg-active, var(--on-accent));
}

/* --- .mention — an @name chip in text ------------------------------------
 * <a class="mention" href="/u/ava">@Ava Tran</a>; .is-self for "you".
 * Inline, so it wraps with the sentence and stays in running text. */
.mention {
  padding: 0.25em 0.3em;
  border-radius: var(--mention-radius, var(--radius));
  background: var(--mention-bg, color-mix(in srgb, var(--accent) 16%, transparent));
  color: var(--mention-fg, var(--accent-text, var(--text)));
  font-weight: 600;
  text-decoration: none;
  box-decoration-break: clone;
  -webkit-box-decoration-break: clone;
}
.mention.is-self {
  background: var(--mention-self-bg, color-mix(in srgb, var(--warning) 30%, transparent));
  color: var(--mention-self-fg, var(--text));
  box-shadow: inset 0 -2px 0 var(--mention-self-line, var(--warning-text, var(--warning)));
}
/* On the user's own bubble (often an accent fill, where accent text on an
   accent tint drops to ~1.5:1) a mention takes the bubble's own ink, with
   an underline in place of the chip fill. */
.message.is-own .mention:not(.is-self) {
  background: var(--mention-own-bg, transparent);
  color: var(--mention-own-fg, inherit);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}
@media (hover: hover) {
  a.mention:hover { text-decoration: underline; }
}
a.mention:focus-visible { text-decoration: underline; }
/* People in a suggestion list (forms.css .listbox): avatar, name, handle. */
.listbox [role="option"]:has(> .avatar) {
  display: flex;
  align-items: center;
  gap: 0.55em;
}
.listbox [role="option"] > small { margin-inline-start: auto; color: inherit; }

/* --- .notifications — the notification centre ------------------------------
 * The same content in a page panel, a .drawer[popover] or a .popover[popover]
 * opened from the bell. Items are .notification rows in a .list. */
.notifications {
  display: flex;
  flex-direction: column;
  gap: var(--space-s);
  min-inline-size: 0;
}
.popover.notifications[popover] {
  inline-size: min(var(--notifications-width, 24rem), calc(100vw - 1rem));
  max-inline-size: none;
  max-block-size: min(var(--notifications-max-height, 36rem), calc(100dvh - 2rem));
}
.notifications-header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2xs) var(--space-xs);
}
.notifications-header > :is(h1, h2, h3, h4) { margin: 0; font-size: 1.05rem; }
.notifications-header > :is(.btn, .btn-close):last-child { margin-inline-start: auto; }
/* Do not disturb: a switch in a label. Checked (or .notifications.is-dnd,
   set by the app) shows the paused note. */
.notifications-dnd {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-s);
  min-block-size: var(--tap-min);
  font-size: 0.9em;
  touch-action: manipulation;
}
.notifications-paused {
  display: none;
  align-items: center;
  gap: var(--space-xs);
  margin: 0;
  padding: var(--space-xs) var(--space-s);
  border-radius: var(--radius);
  background: var(--notifications-paused-bg, var(--surface-2));
  color: var(--notifications-paused-fg, var(--text));
  font-size: 0.85em;
}
.notifications:is(.is-dnd, :has(.notifications-dnd input:checked)) .notifications-paused { display: flex; }
.notifications > .list { --list-bg: transparent; }

.notification {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: var(--space-xs);
  padding: var(--notification-pad, var(--space-xs) var(--space-s));
  padding-inline-start: calc(var(--space-s) + var(--notification-dot-size, 0.5rem));
  min-inline-size: 0;
  color: var(--notification-read-fg, var(--muted));
}
.notification-body {
  flex: 1;
  min-inline-size: 0;
  display: grid;
  gap: var(--space-3xs);
  overflow-wrap: anywhere;
}
.notification-body > * { margin: 0; }
.notification-body > time { font-size: 0.8em; color: var(--muted); font-variant-numeric: tabular-nums; }
.notification-actions {
  display: flex;
  flex: none;
  align-items: center;
  gap: var(--space-3xs);
  margin-block: calc(-1 * var(--space-3xs));
}
/* Unread: a tinted row, a dot at the start and bolder text (put "Unread"
   in a visually hidden span too). */
.notification.is-unread {
  background: var(--notification-unread-bg, color-mix(in srgb, var(--accent) 9%, transparent));
  color: var(--notification-unread-fg, var(--text));
}
.notification.is-unread .notification-body > :first-child { font-weight: 600; }
.notification.is-unread::before {
  content: "";
  position: absolute;
  inset-inline-start: calc(var(--space-s) / 2);
  top: calc(var(--space-xs) + 0.55em);
  inline-size: var(--notification-dot-size, 0.5rem);
  block-size: var(--notification-dot-size, 0.5rem);
  border-radius: 50%;
  background: var(--notification-dot, var(--accent));
}
/* Grouped: <li class="notification is-group"><details>
     <summary class="notification-summary">…3 new comments on…</summary>
     <ul class="notification-group">…</ul>…</details></li> */
.notification.is-group { display: block; padding: 0; }
.notification.is-group.is-unread::before { top: calc(var(--space-xs) + 1.2em); }
.notification-summary {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  min-block-size: var(--tap-min);
  padding: var(--notification-pad, var(--space-xs) var(--space-s));
  padding-inline-start: calc(var(--space-s) + var(--notification-dot-size, 0.5rem));
  list-style: none;
  touch-action: manipulation;
}
.notification-summary::-webkit-details-marker { display: none; }
.notification-summary::after {
  content: var(--comment-thread-marker, "\203A");
  margin-inline-start: auto;
  color: var(--muted);
}
details[open] > .notification-summary::after { rotate: 90deg; }
.notification-summary:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
@media (hover: hover) {
  .notification-summary:hover { background: var(--notification-bg-hover, color-mix(in srgb, var(--text) 6%, transparent)); }
}
.notification-group {
  display: grid;
  margin-inline-start: calc(var(--space-s) + var(--notification-dot-size, 0.5rem));
  border-inline-start: 2px solid var(--notification-group-rule, var(--hairline));
}
.notification-group > .notification { padding-inline-start: var(--space-s); font-size: 0.92em; }
.notification-group > .notification::before { display: none; }
.notification-group-footer { display: flex; justify-content: flex-end; padding: 0 var(--space-s) var(--space-xs); }

/* The bell while notifications are paused: a quiet grey count. */
.has-badge.is-muted {
  --has-badge-bg: var(--has-badge-muted-bg, var(--surface-2));
  --has-badge-fg: var(--has-badge-muted-fg, var(--muted));
}

/* --- .activity — an activity feed on core's timeline -----------------------
 *   <ol class="timeline activity">
 *     <li><span class="avatar">AT</span>
 *       <p><strong>Ava Tran</strong> commented on <a href="…">Q4 plan</a></p>
 *       <time datetime="…">5 min ago</time></li>
 * The actor's avatar replaces the timeline dot; a row without one (a system
 * event) keeps the dot and its state (.is-done, .is-warn…). */
.timeline.activity {
  --timeline-dot-size: var(--activity-avatar-size, 1.8rem);
  --timeline-gap: var(--activity-gap, var(--space-s));
}
.activity > li > .avatar:first-child {
  --avatar-size: var(--activity-avatar-size, 1.8rem);
  position: absolute;
  inset-inline-start: 0;
  top: 0;
  font-size: 0.7em;
}
.activity > li:has(> .avatar:first-child)::before { visibility: hidden; }
.activity > li > p { margin: 0; color: var(--activity-fg, var(--text)); }
.activity > li > time { margin-block-start: var(--space-3xs); }
.activity-quote {
  margin: var(--space-2xs) 0 0;
  padding-inline-start: var(--space-xs);
  border-inline-start: 3px solid var(--activity-quote-rule, var(--hairline));
  color: var(--muted);
  font-size: 0.9em;
}
/* --- v5 surfaces and media (PLAN.md §9 #47, #49, #50) --------------------
 * Image card, draggable-window states, brand sign-in buttons.
 * Docs: docs/components/surfaces.md. Demo: components-surfaces.html. */

/* ---- #47 Image card ------------------------------------------------------
 * <article class="card has-media">
 *   <img src="…" alt="…" loading="lazy" width="1200" height="675">
 *   <div class="card-body">…</div>
 * </article>
 * Media is the first child: <img>, <video>, <picture> or a .card-media box.
 * It bleeds to the card edge and is locked to --card-media-ratio (16 / 9),
 * so a lazy image reserves its space and nothing jumps when it arrives. */
.card.has-media {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  align-content: start;
  position: relative; /* anchors a .stretched-link (whole card clickable) */
  padding: 0;
  overflow: hidden; /* the media takes the card's corners */
}
.card.has-media > :is(img, video, picture, .card-media) {
  display: block;
  inline-size: 100%;
  block-size: auto; /* beats the height="" attribute, so the ratio holds */
  aspect-ratio: var(--card-media-ratio, 16 / 9);
  object-fit: cover;
  overflow: hidden;
  /* The lazy-load placeholder: shows until the image paints over it. */
  background: var(--card-media-placeholder, var(--skeleton-bg, var(--surface-2)));
}
.card.has-media > :is(picture, .card-media) > :is(img, video) {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
}
.card-body {
  display: grid;
  align-content: start;
  gap: var(--space-2xs);
  padding: var(--card-body-pad, var(--surface-pad, var(--space-m)));
  min-inline-size: 0;
}

/* Loading: aria-busy="true" on the card (the app clears it on load) runs
   core's skeleton shimmer over the reserved box and hides the alt text. */
.card.has-media[aria-busy="true"] > :is(img, video, picture, .card-media) {
  color: transparent;
  background-image: linear-gradient(90deg, transparent, var(--skeleton-shine, rgba(255, 255, 255, 0.08)), transparent);
  background-size: 40% 100%;
  background-repeat: no-repeat;
}
@media (prefers-reduced-motion: no-preference) {
  .card.has-media[aria-busy="true"] > :is(img, video, picture, .card-media) {
    animation: skeleton-shimmer 1.4s ease-in-out infinite;
  }
}

  :root[data-motion="always"] .card.has-media[aria-busy="true"] > :is(img, video, picture, .card-media) {
    animation: skeleton-shimmer 1.4s ease-in-out infinite;
  }

html[data-motion="reduced"] .card.has-media > * { animation: none; }

/* Overlay title: the body sits on the bottom of the media behind a scrim.
   The scrim is dark enough for white text over any image (≥ 4.5:1 even
   over pure white), so the text tokens are set for it locally. */
.card.has-media.is-overlay > * { grid-area: 1 / 1; }
.card.has-media.is-overlay > .card-body {
  align-self: end;
  padding-block-start: calc(var(--surface-pad, var(--space-m)) + var(--space-l));
  background: var(--card-overlay-bg, linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.78) var(--space-l), rgba(0, 0, 0, 0.85)));
  --text: var(--card-overlay-fg, #fff);
  --on-surface: var(--card-overlay-fg, #fff);
  --muted: var(--card-overlay-muted, rgba(255, 255, 255, 0.82));
  color: var(--text);
}

/* Horizontal: media beside the body once there's room. It follows the
   nearest size container (.modal, .drawer or .cq; put .cq on the grid
   cell or column that holds the card) and falls back to the viewport when
   there is none. Media takes --card-media-size of the width and the full
   height of the card. */
@media (min-width: 481px) {
  .card.has-media.is-horizontal:not(.is-overlay) {
    grid-template-columns: minmax(0, var(--card-media-size, 40%)) minmax(0, 1fr);
  }
  .card.has-media.is-horizontal:not(.is-overlay) > :is(img, video, picture, .card-media) {
    block-size: 100%;
    align-self: stretch;
  }
}
@container (max-width: 480px) {
  .card.has-media.is-horizontal:not(.is-overlay) { grid-template-columns: minmax(0, 1fr); }
  .card.has-media.is-horizontal:not(.is-overlay) > :is(img, video, picture, .card-media) { block-size: auto; }
}

/* ---- #49 Draggable window ------------------------------------------------
 * <div class="modal" data-drag> + assets/js/window.js. The header is the
 * handle: grab cursor, and touch-action: none on it only, so a touch drag
 * moves the window while the body still scrolls. */
.modal[data-drag] > .modal-header {
  cursor: var(--cursor-grab, grab);
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
}
.modal[data-drag].is-dragging { box-shadow: var(--window-drag-shadow, var(--modal-shadow, var(--panel-shadow, 0 0.75rem 2rem rgba(0, 0, 0, 0.35)))); }
.modal[data-drag].is-dragging > .modal-header { cursor: var(--cursor-grabbing, grabbing); }
/* A focusable header (tabindex="0", for Alt+Arrow moves) keeps its ring
   inside the window edge, where overflow can't clip it. */
.modal[data-drag] > .modal-header:focus-visible {
  outline: 2px solid var(--focus, var(--accent));
  outline-offset: -2px;
}
/* Maximized wins over the inline left/top/width a drag leaves behind (the
   checkbox path has no JS to clear them), and the header stops looking
   draggable. */
.modal[data-drag]:is(.is-maximized, .window-max:checked + *) {
  inset: 0 !important;
  margin: 0 !important;
  width: 100% !important;
}
.modal[data-drag]:is(.is-maximized, .window-max:checked + *) > .modal-header { cursor: var(--cursor-default, default); }

/* ---- #50 Brand sign-in and connect buttons -------------------------------
 * <button class="btn-brand is-google">
 *   <img class="btn-brand-logo" src="assets/logos/google/google-g.svg" alt="">
 *   Sign in with Google
 * </button>
 * Deliberately NOT themed: colours, sizes, radius, font and spacing are each
 * company's published values (sources in docs/components/surfaces.md) and
 * live in private --_bb-* properties on the button itself, so no theme token
 * reaches them. The logo is always an <img> the app supplies (a logo is never
 * part of a theme bundle; see assets/logos/README.md). Only the focus ring
 * and the hit area follow the library. */
.btn-brand {
  /* Defaults are Google's (light). Each brand below overrides what differs. */
  --_bb-h: 40px;
  --_bb-bg: #fff;
  --_bb-fg: #1f1f1f;
  --_bb-border: #747775;
  --_bb-radius: 4px;
  --_bb-pad: 12px;
  --_bb-pad-end: 12px;
  --_bb-gap: 10px;
  --_bb-logo: 20px;
  --_bb-font: "Google Sans", Roboto, Arial, sans-serif;
  --_bb-size: 14px;
  --_bb-weight: 500;
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--_bb-gap);
  box-sizing: border-box;
  block-size: var(--_bb-h);
  max-inline-size: 100%;
  margin: 0;
  padding: 0 var(--_bb-pad-end) 0 var(--_bb-pad);
  border: 1px solid var(--_bb-border);
  border-radius: var(--_bb-radius);
  background: var(--_bb-bg);
  box-shadow: none;
  color: var(--_bb-fg);
  font: var(--_bb-weight) var(--_bb-size) / 1.25 var(--_bb-font);
  letter-spacing: normal;
  text-transform: none;
  text-shadow: none;
  text-decoration: none;
  white-space: nowrap;
  vertical-align: middle;
  touch-action: manipulation;
}
/* Logical padding flips in RTL along with the logo. */
.btn-brand:dir(rtl) { padding: 0 var(--_bb-pad) 0 var(--_bb-pad-end); }
.btn-brand > .btn-brand-logo {
  flex: none;
  inline-size: auto;
  block-size: var(--_bb-logo);
  max-inline-size: none;
}
/* Hit area: the visual height is the brand's; the ::after grows the
   target to --tap-min on touch tiers without changing the look. */
.btn-brand::after {
  content: "";
  position: absolute;
  inset: min(0px, calc((var(--_bb-h) - var(--tap-min, 24px)) / 2 - 1px)) 0; /* - 1px: the border */
}
/* State layer (Material-style tint of the text colour) — the brand's
   colours are kept underneath. Hover has a focus-visible equivalent. */
.btn-brand:focus-visible {
  outline: 2px solid var(--focus, var(--accent));
  outline-offset: 2px;
  background-image: linear-gradient(color-mix(in srgb, var(--_bb-fg) 10%, transparent) 0 0);
}
@media (hover: hover) {
  .btn-brand:hover { background-image: linear-gradient(color-mix(in srgb, var(--_bb-fg) 8%, transparent) 0 0); }
}
.btn-brand:active { background-image: linear-gradient(color-mix(in srgb, var(--_bb-fg) 14%, transparent) 0 0); }
.btn-brand:is(:disabled, [aria-disabled="true"]) { opacity: 0.38; cursor: var(--cursor-not-allowed, not-allowed); background-image: none; }
/* Full width (a sign-in stack): content stays centred, like the providers' own. */
.btn-brand.is-block { display: flex; inline-size: 100%; }
/* Icon-only (Google, Apple and Microsoft allow logo-only buttons): square. */
.btn-brand.is-icon { inline-size: var(--_bb-h); padding: 0; }
.btn-brand.is-icon::after { inset: min(0px, calc((var(--_bb-h) - var(--tap-min, 24px)) / 2 - 1px)); }

/* Google — developers.google.com/identity/branding-guidelines:
   40px, 12 / 10 / 12px padding, 20px G, Google Sans Medium 14/20,
   light / dark / neutral, rectangular (4px) or pill. */
.btn-brand.is-google.is-dark { --_bb-bg: #131314; --_bb-fg: #e3e3e3; --_bb-border: #8e918f; }
.btn-brand.is-google.is-neutral { --_bb-bg: #f2f2f2; --_bb-fg: #1f1f1f; --_bb-border: #f2f2f2; }
.btn-brand.is-google.is-pill { --_bb-radius: 20px; }

/* Microsoft — learn.microsoft.com …/howto-add-branding-in-apps redlines:
   41px, 12px padding and gap, Segoe UI 15px weight 600, square corners;
   light #fff / #5e5e5e text / 1px #8c8c8c border, dark #2f2f2f / #fff. */
.btn-brand.is-microsoft {
  --_bb-h: 41px;
  --_bb-fg: #5e5e5e;
  --_bb-border: #8c8c8c;
  --_bb-radius: 0;
  --_bb-gap: 12px;
  --_bb-logo: 21px;
  --_bb-font: "Segoe UI", system-ui, sans-serif;
  --_bb-size: 15px;
  --_bb-weight: 600;
}
.btn-brand.is-microsoft.is-dark { --_bb-bg: #2f2f2f; --_bb-fg: #fff; --_bb-border: #2f2f2f; }

/* Apple — developer.apple.com HIG "Sign in with Apple": black (default),
   .is-white, .is-outline (white with a black outline); logo and title both
   black or white; title 43% of the button height in the system font;
   minimum 140 × 30px; the logo file is as tall as the button and carries
   its own leading padding, so the button adds none; at least 8% of the
   width after the title. Rounded corners by default (any radius allowed). */
.btn-brand.is-apple {
  --_bb-h: 44px;
  --_bb-bg: #000;
  --_bb-fg: #fff;
  --_bb-border: #000;
  --_bb-radius: 6px;
  --_bb-pad: 0px;
  --_bb-pad-end: max(16px, 2em);
  --_bb-gap: 0px;
  --_bb-logo: var(--_bb-h);
  --_bb-font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif;
  --_bb-size: calc(var(--_bb-h) * 0.43);
  min-inline-size: 140px;
}
.btn-brand.is-apple.is-white { --_bb-bg: #fff; --_bb-fg: #000; --_bb-border: #fff; }
.btn-brand.is-apple.is-outline { --_bb-bg: #fff; --_bb-fg: #000; --_bb-border: #000; }
.btn-brand.is-apple.is-icon { min-inline-size: 0; }

/* GitHub — brand.github.com/foundations/logo: the Invertocat only in black
   or white, high contrast, no effects. GitHub publishes no button spec, so
   the box follows the Google/Microsoft proportions. Dark by default. */
.btn-brand.is-github {
  --_bb-bg: #000;
  --_bb-fg: #fff;
  --_bb-border: #000;
  --_bb-radius: 6px;
  --_bb-font: "Mona Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  --_bb-weight: 600;
}
.btn-brand.is-github.is-light { --_bb-bg: #fff; --_bb-fg: #000; --_bb-border: #000; }

/* Spotify — developer.spotify.com/documentation/design: green icon only on
   black or white; icon ≥ 21px; exclusion zone half the icon height; link
   text OPEN SPOTIFY / PLAY ON SPOTIFY / LISTEN ON SPOTIFY / GET SPOTIFY
   FREE; platform sans-serif (Helvetica Neue, Helvetica, Arial). */
.btn-brand.is-spotify {
  --_bb-h: 48px;
  --_bb-bg: #121212;
  --_bb-fg: #fff;
  --_bb-border: #121212;
  --_bb-radius: 24px;
  --_bb-logo: 24px;
  --_bb-gap: 12px;
  --_bb-pad: 12px;
  --_bb-pad-end: 20px;
  --_bb-font: "Helvetica Neue", Helvetica, Arial, sans-serif;
  --_bb-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.btn-brand.is-spotify.is-light { --_bb-bg: #fff; --_bb-fg: #121212; --_bb-border: #121212; }

/* A row or stack of providers: equal widths, as Google and Apple ask
   ("at least as prominent as other sign-in options"). */
.btn-brand-stack {
  display: grid;
  gap: var(--space-xs);
  inline-size: min(100%, 22rem);
}
.btn-brand-stack > .btn-brand { display: flex; inline-size: 100%; }

/* Brands keep their own colours in forced-colors mode only where the OS
   allows; give them a visible border there. */
@media (forced-colors: active) {
  .btn-brand { border-color: ButtonText; }
}
/* --- v5 tables (PLAN.md §7 editable tables and small screens, §18 advanced
 * tables, §9 #48 table in a modal) ------------------------------------------
 * Builds on core's .table, .is-sticky, .is-striped, aria-sort, .is-selected,
 * .table-wrap and --sticky-top. Apps provide the behaviour (open an editor,
 * save, resize, reorder, select); this file styles every state.
 * Docs: docs/components/tables.md. Example: components-tables.html.
 *
 * Private custom properties (--_*) are internal plumbing, not theme tokens.
 * A table resets them so a table nested in a cell starts clean. */
.table {
  --_cell-paint: initial;
  --_row-tint: initial;
  --_base: initial;
  --_dir: 1;
}
.table:dir(rtl) { --_dir: -1; }

/* Cell padding as tokens (same values as core's literal padding), so the
   density modifiers and the in-cell editors all read one source. */
.table :is(th, td) {
  padding: var(--cell-pad-y, calc(0.5em * var(--density))) var(--cell-pad-x, calc(0.75em * var(--density)));
}
.table.is-compact {
  --cell-pad-y: max(var(--space-2xs), calc(0.25em * var(--density)));
  --cell-pad-x: calc(0.5em * var(--density));
}
.table.is-comfortable {
  --cell-pad-y: calc(0.85em * var(--density));
  --cell-pad-x: calc(1em * var(--density));
}

/* Touch switch, set once: the pencil on idle editable cells. Same
   conditions as core's --tap-min. */
:root { --_cell-pencil: none; }
@media (max-width: 900px), (any-pointer: coarse) {
  :root { --_cell-pencil: inline-block; }
}
html[data-pointer="touch"] { --_cell-pencil: inline-block; }

/* Spreadsheet-style navigation: an app gives cells tabindex and moves focus
   with the arrow keys. The ring sits inside the cell so a .table-wrap
   scroller can't clip it. */
.table :is(td, th):focus-visible { outline-offset: -2px; }
.table td:is(.is-editable, .is-editing, [tabindex]:not([tabindex="-1"])) { height: var(--tap-min); }

/* --- Editable cells (§7) -----------------------------------------------
   The same idle/dirty/saving/saved/invalid states work outside a table on
   any element with .editable (a card title, a label): */

/* Idle: editable on double-click (desktop) or tap (touch). A dotted
   underline on hover and focus; a pencil on touch tiers, where there is no
   hover. */
:is(.table td.is-editable, .editable) { cursor: var(--cursor-text, text); }
:is(.table td.is-editable, .editable):focus-visible,
:is(.table td.is-editable, .editable):focus-within {
  text-decoration: underline dotted var(--cell-editable-underline, var(--muted));
  text-underline-offset: 0.25em;
}
@media (hover: hover) {
  :is(.table td.is-editable, .editable):hover {
    text-decoration: underline dotted var(--cell-editable-underline, var(--muted));
    text-underline-offset: 0.25em;
  }
}
:is(.table td.is-editable, .editable)::after {
  display: var(--_cell-pencil);
  content: var(--cell-edit-glyph, "\270E") / "";
  margin-inline-start: 0.5em;
  color: var(--cell-edit-glyph-fg, var(--muted));
  font-size: 0.9em;
}
/* Read-only: no affordance, quieter text, the arrow cursor. */
.table td:is(.is-readonly, [aria-readonly="true"]) {
  cursor: var(--cursor-default, default);
  color: var(--cell-readonly-fg, var(--muted));
  text-decoration: none;
}
.table td.is-editable:is(.is-readonly, [aria-readonly="true"])::after { content: none; }

/* Editing: the control fills the cell exactly. The cell's padding moves onto
   the control (less its border), the font and line height are the cell's,
   and field-sizing keeps its intrinsic width at its content, so neither the
   row height nor the column width jumps. Works for a single cell
   (td.is-editing) and a whole row (tr.is-editing). */
.table :is(td.is-editing, tr.is-editing > td):has(> :is(.input, .select, .textarea, .input-group)) { padding: 0; }
.table :is(td.is-editing, tr.is-editing > td) > :is(.input, .select, .textarea),
.table :is(td.is-editing, tr.is-editing > td) > .input-group > :is(.input, .select) {
  display: block;
  width: 100%;
  min-inline-size: 0;
  margin: 0;
  padding: calc(var(--cell-pad-y, calc(0.5em * var(--density))) - var(--input-border-width, 1px))
    calc(var(--cell-editor-pad-x, var(--cell-pad-x, calc(0.75em * var(--density)))) - var(--input-border-width, 1px));
  font: inherit;
  font-size: var(--cell-editor-font-size, inherit);
  line-height: inherit;
  letter-spacing: inherit;
  border-radius: var(--cell-input-radius, 0);
  field-sizing: content;
  outline-offset: -2px;
  text-align: start;
}
/* Chrome pads the date fields and its picker icon; the cell's padding rules. */
.table :is(td.is-editing, tr.is-editing > td) > .input::-webkit-datetime-edit,
.table :is(td.is-editing, tr.is-editing > td) > .input::-webkit-datetime-edit-fields-wrapper { padding-block: 0; }
.table :is(td.is-editing, tr.is-editing > td) > .input::-webkit-calendar-picker-indicator { block-size: 1em; padding-block: 0; margin-block: 0; }
.table :is(td.is-editing, tr.is-editing > td) > .select,
.table :is(td.is-editing, tr.is-editing > td) > .input-group > .select {
  padding-inline-end: calc(var(--cell-editor-pad-x, var(--cell-pad-x, calc(0.75em * var(--density)))) + var(--cell-editor-arrow-space, 1.5em));
}
/* An addon beside an editing control (.input-group): the group fills the cell
   and its controls get the same treatment; the inner control flexes. */
.table :is(td.is-editing, tr.is-editing > td) > .input-group { display: flex; align-items: center; inline-size: 100%; }
.table :is(td.is-editing, tr.is-editing > td) > .input-group > :is(.input, .select) { flex: 1 1 auto; inline-size: auto; }
.table :is(td.is-editing, tr.is-editing > td) > .textarea { min-height: 0; resize: none; }
.table :is(td.is-editing, tr.is-editing > td) > :is(.input, .select, .textarea):not(.is-invalid, [aria-invalid="true"]),
.table :is(td.is-editing, tr.is-editing > td) > .input-group > :is(.input, .select):not(.is-invalid, [aria-invalid="true"]) {
  background-color: var(--cell-input-bg, var(--input-bg, var(--surface)));
  border-color: var(--cell-editing-border, var(--accent));
}
/* iOS zooms on focus below 16px; the 44px touch row absorbs the difference. */
@media (any-pointer: coarse) {
  .table :is(td.is-editing, tr.is-editing > td) > :is(.input, .select, .textarea) { font-size: max(16px, 1em); }
}
/* A validation message in an editing cell lines up with the control's text. */
.table :is(td.is-editing, tr.is-editing > td) > .field-error {
  display: block;
  margin: 0;
  padding: var(--space-3xs) var(--cell-pad-x, calc(0.75em * var(--density))) var(--space-2xs);
}

/* Cell states paint with an inset box-shadow (--_cell-paint), above the row's
   hover/stripe/selection fill, so a state always shows whatever the row is
   doing. Each state also has a non-colour signal: a bar, a glyph, a ring or
   the error text. */
:is(.table tr > td, .editable):is(.is-dirty, .is-saving, .htmx-request, .is-saved, [aria-invalid="true"], :has(> [aria-invalid="true"])),
.table tr.is-editing > td {
  box-shadow: var(--_cell-paint);
}
.table tr.is-editing > td {
  --_cell-paint: inset 0 0 0 100vmax var(--row-editing-bg, color-mix(in srgb, var(--accent) 8%, transparent));
}
.table tr.is-editing {
  --_marker: inset 0.25rem 0 0 var(--row-editing-marker, var(--accent));
}
.table tr.is-editing > td:first-child { box-shadow: var(--_marker), var(--_cell-paint); }
/* Row actions (save, cancel) sit in the last cell, kept on one line. */
.table tr.is-editing > td:last-child:not(:has(> :is(.input, .select, .textarea))) {
  white-space: nowrap;
  text-align: end;
}

/* Dirty: changed, not saved. A bar on the leading edge plus a tint. */
:is(.table tr > td, .editable).is-dirty {
  --_cell-paint: inset calc(var(--_dir, 1) * 0.2rem) 0 0 var(--cell-dirty-marker, var(--warning)),
    inset 0 0 0 100vmax var(--cell-dirty-bg, color-mix(in srgb, var(--warning) 14%, transparent));
}
/* Saving: a request in flight (htmx's .htmx-request works too). Dimmed text
   and a spinner; the app should also set aria-busy="true" on the cell. */
:is(.table tr > td, .editable):is(.is-saving, .htmx-request) {
  color: color-mix(in srgb, currentColor calc(var(--pending-opacity, 0.8) * 100%), transparent);
}
:is(.table tr > td, .editable):is(.is-saving, .htmx-request)::after {
  content: "";
  display: inline-block;
  inline-size: 0.85em;
  block-size: 0.85em;
  margin-inline-start: 0.5em;
  vertical-align: -0.1em;
  border: 2px solid var(--indicator-track, var(--border));
  border-top-color: var(--indicator-fg, var(--accent));
  border-radius: 50%;
}
/* Saved: a tick and a success tint; the app removes .is-saved after a moment. */
:is(.table tr > td, .editable).is-saved {
  --_cell-paint: inset 0 0 0 100vmax var(--cell-saved-bg, color-mix(in srgb, var(--success) 14%, transparent));
}
:is(.table tr > td, .editable).is-saved::after {
  content: var(--cell-saved-glyph, "\2713") / "";
  margin-inline-start: 0.5em;
  color: var(--cell-saved-fg, var(--success-text, var(--success)));
  font-weight: 700;
}
/* Error: aria-invalid on the cell or its control, plus a .field-error. */
:is(.table tr > td, .editable):is([aria-invalid="true"], :has(> [aria-invalid="true"])) {
  --_cell-paint: inset 0 0 0 2px var(--cell-error-border, var(--danger)),
    inset 0 0 0 100vmax var(--cell-error-bg, color-mix(in srgb, var(--danger) 10%, transparent));
}
@media (prefers-reduced-motion: no-preference) {
  :is(.table tr > td, .editable):is(.is-saving, .htmx-request)::after { animation: spinner-rotate 0.7s linear infinite; }
  :is(.table tr > td, .editable).is-saved { animation: cell-saved 1.6s ease-out forwards; }
}

  :root[data-motion="always"] :is(.table tr > td, .editable):is(.is-saving, .htmx-request)::after { animation: spinner-rotate 0.7s linear infinite; }
  :root[data-motion="always"] :is(.table tr > td, .editable).is-saved { animation: cell-saved 1.6s ease-out forwards; }

@keyframes cell-saved { from { box-shadow: inset 0 0 0 100vmax var(--cell-saved-flash, color-mix(in srgb, var(--success) 35%, transparent)); } }

/* Dropdown, date and combobox in a cell: give the list the popover
   attribute so it lives in the top layer, where the table's scroller and
   sticky header can't clip it (core places top-layer popovers). Where anchor
   positioning is supported, it opens under its own cell, at least as wide:
     <td class="is-editing"><input class="input" role="combobox" …>
       <ul class="dropdown" popover role="listbox">…</ul></td> */
@supports (position-area: block-end) and (anchor-scope: all) {
  .table :is(td, th):has(> [popover]) { anchor-name: --cell; anchor-scope: --cell; }
  .table :is(td, th) > :is(.popover, .dropdown, .context-menu)[popover] {
    position-anchor: --cell;
    min-inline-size: anchor-size(inline);
    margin: 0;
  }
}

/* --- Frozen columns (§7 small screens, §18) ------------------------------
 * A .table-wrap keeps its first column in view while the table scrolls
 * sideways. .is-frozen-1 / .is-frozen-2 do the same in any scroller (and
 * freeze two columns: the first must be a narrow column, e.g. a selection
 * checkbox, of width --table-frozen-1-size). A stacked table is not frozen.
 *
 * A frozen cell must be opaque while the table is scrolled, or the rows
 * passing under it show through. Core paints row states as translucent cell
 * backgrounds, so a frozen body cell paints the row's state (--_row-tint,
 * mirrored from core's row rules just below) over an opaque base
 * (--table-bg). It is !important because core's and themes' row-state
 * selectors outweigh any plain selector here; themes restyle it through the
 * --row-* and --table-bg tokens. box-shadow is left alone: themes draw row
 * markers with it. */
.table.is-striped tbody tr:nth-child(even) { --_row-tint: var(--row-alt-bg, var(--surface-2)); }
/* :nth-child(n) matches every row; it only lifts these to the stripe's weight. */
.table tbody tr:nth-child(n):is(:hover, :focus-within):not(.table-empty, .table-more) {
  --_row-tint: var(--row-hover-bg, color-mix(in srgb, var(--accent) 8%, transparent));
}
.table tbody tr:is(.is-active, :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked))):nth-child(n) { --_row-tint: var(--row-active-bg, transparent); }
.table tbody tr:is(.is-selected, :is([aria-selected="true"], :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked)))):nth-child(n) {
  --_row-tint: var(--row-selected-bg, color-mix(in srgb, var(--accent) 18%, transparent));
}

/* The scroller is the containing block for anything absolutely positioned
   in the table (a .visually-hidden checkbox name), or it escapes the
   scroller and widens the page. */
.table-wrap { position: relative; }

:is(.table-wrap > .table, .table.is-frozen-1, .table.is-frozen-2):not(.is-stacked) > * > tr > :first-child,
.table.is-frozen-2:not(.is-stacked) > * > tr > :nth-child(2) {
  position: sticky;
  inset-inline-start: 0;
  z-index: var(--z-raised);
  --_base: var(--table-bg, var(--surface));
}
.table.is-frozen-2:not(.is-stacked) > * > tr > :first-child {
  box-sizing: border-box;
  inline-size: var(--table-frozen-1-size, calc(var(--tap-min) + 1.5rem));
  min-inline-size: var(--table-frozen-1-size, calc(var(--tap-min) + 1.5rem));
}
.table.is-frozen-2:not(.is-stacked) > * > tr > :nth-child(2) {
  inset-inline-start: var(--table-frozen-1-size, calc(var(--tap-min) + 1.5rem));
}
:is(.table-wrap > .table, .table.is-frozen-1, .table.is-frozen-2):not(.is-stacked) > :is(tbody, tfoot) > tr > td:first-child,
.table.is-frozen-2:not(.is-stacked) > :is(tbody, tfoot) > tr > td:nth-child(2) {
  background: linear-gradient(var(--_row-tint, transparent) 0 0), var(--_base) !important;
}
/* Header cells in a frozen column sit above the frozen body cells and a
   sticky header's other cells (the top-left corner). */
:is(.table-wrap > .table, .table.is-frozen-1, .table.is-frozen-2):not(.is-stacked) > thead > tr > th:first-child,
.table.is-frozen-2:not(.is-stacked) > thead > tr > th:nth-child(2) {
  background: linear-gradient(var(--table-head-bg, transparent) 0 0), var(--_base);
  z-index: calc(var(--z-sticky) + 1);
}
/* The edge of the last frozen column: a hairline for .is-frozen-N, and a
   shadow once the table is scrolled sideways (where scroll-driven
   animations are supported). */
.table.is-frozen-1:not(.is-stacked) > * > tr > :first-child,
.table.is-frozen-2:not(.is-stacked) > * > tr > :nth-child(2) {
  border-inline-end: 1px solid var(--table-frozen-edge, var(--hairline));
}
/* Where scroll-driven animations are supported, the opaque base and the
   edge shadow only appear once the table is scrolled, so a table at rest
   looks exactly as it would unfrozen, whatever surface it sits on. */
@supports (animation-timeline: scroll()) {
  :is(.table-wrap > .table, .table.is-frozen-1, .table.is-frozen-2):not(.is-stacked) > * > tr > :first-child,
  .table.is-frozen-2:not(.is-stacked) > * > tr > :nth-child(2) {
    --_base: transparent;
    animation: table-frozen-base linear both;
    animation-timeline: scroll(nearest inline);
    animation-range: 0 2px;
  }
  :is(.table-wrap > .table:not(.is-frozen-2), .table.is-frozen-1):not(.is-stacked) > * > tr > :first-child,
  .table.is-frozen-2:not(.is-stacked) > * > tr > :nth-child(2) { animation-name: table-frozen-edge; }
}
/* data-motion="reduced" pauses every animation, which would freeze these
   at rest; such pages get the always-opaque form instead. */
html[data-motion="reduced"] :is(.table-wrap > .table, .table.is-frozen-1, .table.is-frozen-2):not(.is-stacked) > * > tr > :is(:first-child, :nth-child(2)) {
  --_base: var(--table-bg, var(--surface));
  animation: none;
}
@keyframes table-frozen-base { to { --_base: var(--table-bg, var(--surface)); } }
@keyframes table-frozen-edge {
  from { filter: none; }
  to {
    --_base: var(--table-bg, var(--surface));
    filter: drop-shadow(calc(var(--_dir) * 3px) 0 3px var(--table-frozen-shadow, rgb(0 0 0 / 0.4)));
  }
}

/* --- Tables on small screens: stacked cards (§7) -------------------------
 * Opt in with .table.is-stacked: below the Tablet tier (the nearest .modal,
 * .drawer or .cq container, or the viewport) each row becomes a card and each
 * cell shows its column name from data-label. The thead stays for screen
 * readers. The block is written twice: once for a container, once for the
 * viewport when there is no container. */
@media (max-width: 480px) {
  .table.is-stacked,
  .table.is-stacked > :is(thead, tbody, tfoot),
  .table.is-stacked > * > tr:not([hidden]),
  .table.is-stacked > * > tr > :is(th, td):nth-child(n) { display: block; }
  .table.is-stacked > thead {
    position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap;
  }
  .table.is-stacked > * > tr {
    margin-block-end: var(--space-xs);
    border: 1px solid var(--table-card-border, var(--border));
    border-radius: var(--table-card-radius, var(--radius));
    background: var(--table-card-bg, transparent);
    overflow: hidden;
  }
  .table.is-stacked > * > tr > :is(th, td):nth-child(n) {
    padding: var(--space-2xs) var(--space-s);
    text-align: start;
    height: auto;
  }
  .table.is-stacked > * > tr > td:is(.is-editable, .is-editing, [tabindex]:not([tabindex="-1"])) { min-block-size: var(--tap-min); }
  .table.is-stacked > * > tr > :last-child { border-bottom: 0; }
  .table.is-stacked > * > tr > [data-label]:nth-child(n) {
    display: grid;
    grid-template-columns: minmax(0, var(--table-label-size, 38%)) minmax(0, 1fr) auto;
    gap: var(--space-xs);
    align-items: center;
  }
  .table.is-stacked > * > tr > [data-label]::before {
    content: attr(data-label);
    color: var(--table-label-fg, var(--muted));
    font-size: 0.78em;
    text-transform: var(--table-head-transform, uppercase);
    letter-spacing: var(--table-head-tracking, 0.06em);
  }
  .table.is-stacked > * > tr > [data-label] > * { min-inline-size: 0; }
  .table.is-stacked > * > tr > :first-child { inline-size: auto; }
  .table.is-stacked > * > tr:is(.is-selected, :is([aria-selected="true"], :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked)))) { --table-label-fg: var(--row-selected-fg, color-mix(in srgb, currentColor 75%, transparent)); }
}
@container (max-width: 480px) {
  .table.is-stacked,
  .table.is-stacked > :is(thead, tbody, tfoot),
  .table.is-stacked > * > tr:not([hidden]),
  .table.is-stacked > * > tr > :is(th, td):nth-child(n) { display: block; }
  .table.is-stacked > thead {
    position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap;
  }
  .table.is-stacked > * > tr {
    margin-block-end: var(--space-xs);
    border: 1px solid var(--table-card-border, var(--border));
    border-radius: var(--table-card-radius, var(--radius));
    background: var(--table-card-bg, transparent);
    overflow: hidden;
  }
  .table.is-stacked > * > tr > :is(th, td):nth-child(n) {
    padding: var(--space-2xs) var(--space-s);
    text-align: start;
    height: auto;
  }
  .table.is-stacked > * > tr > td:is(.is-editable, .is-editing, [tabindex]:not([tabindex="-1"])) { min-block-size: var(--tap-min); }
  .table.is-stacked > * > tr > :last-child { border-bottom: 0; }
  .table.is-stacked > * > tr > [data-label]:nth-child(n) {
    display: grid;
    grid-template-columns: minmax(0, var(--table-label-size, 38%)) minmax(0, 1fr) auto;
    gap: var(--space-xs);
    align-items: center;
  }
  .table.is-stacked > * > tr > [data-label]::before {
    content: attr(data-label);
    color: var(--table-label-fg, var(--muted));
    font-size: 0.78em;
    text-transform: var(--table-head-transform, uppercase);
    letter-spacing: var(--table-head-tracking, 0.06em);
  }
  .table.is-stacked > * > tr > [data-label] > * { min-inline-size: 0; }
  .table.is-stacked > * > tr > :first-child { inline-size: auto; }
  .table.is-stacked > * > tr:is(.is-selected, :is([aria-selected="true"], :where(:has(> input:is([type="radio"], [type="checkbox"]):checked, > td:first-child input[type="checkbox"]:checked)))) { --table-label-fg: var(--row-selected-fg, color-mix(in srgb, currentColor 75%, transparent)); }
}

/* --- Column resize and reorder (§18) ------------------------------------
 * th.is-resizable draws a grip on its trailing edge for pointer users; add a
 * focusable <span class="table-resizer" role="separator" tabindex="0"> inside
 * it for keyboard users (it replaces the drawn grip). .is-resizing on the th
 * while dragging. The app does the resizing. */
.table th.is-resizable { position: relative; height: var(--tap-min); }
.table th.is-resizable::before,
.table th.is-resizable > .table-resizer {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-end: 0;
  inline-size: var(--table-resizer-size, var(--tap-min));
  border-inline-end: 2px solid var(--table-resizer-fg, var(--hairline));
  cursor: var(--cursor-resize-x, col-resize);
  touch-action: none;
}
.table th.is-resizable:has(> .table-resizer)::before { content: none; }
.table th.is-resizable > .table-resizer:focus-visible { outline-offset: -2px; }
.table th.is-resizable.is-resizing::before,
.table th.is-resizable > .table-resizer:is(:focus-visible, :active),
.table th.is-resizing > .table-resizer { border-inline-end-color: var(--table-resizer-fg-active, var(--accent)); }
@media (hover: hover) {
  .table th.is-resizable:hover::before,
  .table th.is-resizable > .table-resizer:hover { border-inline-end-color: var(--table-resizer-fg-active, var(--accent)); }
}

/* Reorder: a draggable header, the column being dragged (.is-dragging) and
   the drop indicator between columns (.is-drop-before / .is-drop-after on
   the header, or on every cell of the target column). */
.table th[draggable="true"] { cursor: var(--cursor-grab, grab); }
.table th[draggable="true"]:active { cursor: var(--cursor-grabbing, grabbing); }
.table :is(th, td).is-dragging {
  opacity: var(--table-drag-opacity, 1);
  outline: 1px dashed var(--table-drop-marker, var(--accent));
  outline-offset: -1px;
}
.table :is(th, td).is-drop-before { box-shadow: inset calc(var(--_dir) * 3px) 0 0 var(--table-drop-marker, var(--accent)); }
.table :is(th, td).is-drop-after { box-shadow: inset calc(var(--_dir) * -3px) 0 0 var(--table-drop-marker, var(--accent)); }

/* Row drag-to-reorder: a grip cell (td.drag-handle, holding a <button> that
   also takes Alt+Up/Down or opens "Move to…" — never drag-only), the lifted
   row (tr.is-dragging) and the drop line drawn on the target row
   (tr.is-drop-before / -after), with no extra element inside the scroller.
   .table.is-dragging (or body) sets the grabbing cursor while dragging.
   Announce the move in a live region ("Moved Coffee to position 3"). */
.table td.drag-handle { inline-size: var(--tap-min); padding-inline: 0; text-align: center; color: var(--muted); cursor: var(--cursor-grab, grab); touch-action: none; }
.table td.drag-handle:empty::before { content: var(--drag-handle-glyph, "\283F") / ""; }
.table td.drag-handle > button { min-inline-size: var(--tap-min); min-block-size: var(--tap-min); padding: 0; border: 0; background: none; color: inherit; font: inherit; cursor: inherit; touch-action: none; }
.table td.drag-handle > button:empty::before { content: var(--drag-handle-glyph, "\283F") / ""; }
.table tr.is-dragging { outline: 2px dashed var(--drop-line-color, var(--table-drop-marker, var(--accent))); outline-offset: -2px; }
.table tr.is-dragging > td { opacity: var(--table-row-drag-opacity, 0.55); }
.table tr.is-drop-before > td { box-shadow: inset 0 var(--drop-line-size, 3px) 0 var(--drop-line-color, var(--table-drop-marker, var(--accent))); }
.table tr.is-drop-after > td { box-shadow: inset 0 calc(-1 * var(--drop-line-size, 3px)) 0 var(--drop-line-color, var(--table-drop-marker, var(--accent))); }
:is(body, .table).is-dragging { cursor: var(--cursor-grabbing, grabbing); user-select: none; }

/* Ordered rows (agendas, playlists, queues): tr.is-past is done (muted, the
   .row-title — or the first cell — struck through; not by opacity, which
   fails contrast); tr.is-next is up next (a dashed leading marker, unlike
   .is-active's solid one); tr.is-current / aria-current="time" is now.
   tr.has-row-color with --row-color paints a leading bar and a light tint
   (a category, a user-picked colour). Give past and next a text
   equivalent too (a visually-hidden "done", "next"). */
.table tr.is-past > td { color: var(--row-past-fg, var(--muted)); }
.table tr.is-past .row-title,
.table tr.is-past:not(:has(.row-title)) > td:first-child:not(.drag-handle),
.table tr.is-past:not(:has(.row-title)) > td.drag-handle + td {
  text-decoration: line-through var(--row-past-strike, color-mix(in srgb, var(--muted) 60%, transparent)) 1px;
}
.table tr.is-next > td:first-child {
  background-image: repeating-linear-gradient(180deg, var(--row-next-marker, var(--accent)) 0 0.35rem, transparent 0.35rem 0.6rem);
  background-repeat: no-repeat;
  background-size: 0.25rem 100%;
  background-position: 0 0;
}
.table:dir(rtl) tr.is-next > td:first-child { background-position: 100% 0; }
.table tr:is(.is-current, [aria-current="time"]) > td { font-weight: 600; }
.table tr:is(.is-current, [aria-current="time"]) > td:first-child { box-shadow: var(--row-active-marker, inset 0.25rem 0 0 var(--accent)); }
.table tr.has-row-color > td { box-shadow: inset 0 0 0 100vmax color-mix(in srgb, var(--row-color, var(--accent)) var(--row-color-tint, 7%), transparent); }
.table tr.has-row-color > td:first-child {
  box-shadow: inset calc(var(--_dir, 1) * 3px) 0 0 var(--row-color, var(--accent)), inset 0 0 0 100vmax color-mix(in srgb, var(--row-color, var(--accent)) var(--row-color-tint, 7%), transparent);
}

/* A NOW divider row between two rows: <tr class="table-now" aria-hidden="true"><td colspan="6">Now · 11:06</td></tr>
   (the current row carries aria-current="time"). .table.is-daysheet prints
   black on white, the current row still shaded. */
.table tr.table-now > td {
  padding-block: 0.1rem;
  border-block: 2px solid var(--table-now-rule, var(--accent));
  background: color-mix(in srgb, var(--table-now-rule, var(--accent)) 8%, transparent);
  color: var(--table-now-fg, var(--accent-text, var(--accent)));
  font-size: 0.65em;
  font-weight: 800;
  letter-spacing: 0.2em;
  text-transform: uppercase;
}
@media print {
  .table.is-daysheet { --row-past-fg: #444444; background: #ffffff; color: #000000; }
  .table.is-daysheet :is(th, td) { background: #ffffff; color: #000000; border-color: #000000; box-shadow: none; }
  .table.is-daysheet tr:is(.is-current, [aria-current="time"]) > td { background: #e6e6e6; print-color-adjust: exact; -webkit-print-color-adjust: exact; }
  .table.is-daysheet tr.table-now > td { border-color: #000000; color: #000000; }
}

/* --- Row groups and tree rows (§18) --------------------------------------
 * <tbody class="table-group"> with a header row and an optional totals row.
 * A .table-toggle with aria-expanded collapses the group (the app flips the
 * attribute) and marks expandable tree rows. */
.table-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  min-block-size: var(--tap-min);
  min-inline-size: var(--tap-min);
  margin-block: calc(-1 * var(--cell-pad-y, calc(0.5em * var(--density))));
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  text-align: start;
  touch-action: manipulation;
}
.table-toggle::before {
  content: var(--table-toggle-glyph, "\25B8") / "";
  display: inline-block;
  inline-size: 1em;
  text-align: center;
  color: var(--table-toggle-fg, var(--muted));
}
.table-toggle:dir(rtl)::before { scale: -1 1; }
.table-toggle[aria-expanded="true"]::before { rotate: 90deg; }
.table-toggle:dir(rtl)[aria-expanded="true"]::before { rotate: -90deg; }
@media (prefers-reduced-motion: no-preference) {
  .table-toggle::before { transition: rotate 0.15s ease; }
}

  :root[data-motion="always"] .table-toggle::before { transition: rotate 0.15s ease; }


.table .table-group-header > :is(th, td) {
  background: var(--table-group-bg, var(--surface-2));
  color: var(--table-group-fg, var(--text));
  font-weight: 600;
}
.table .table-group-total > :is(th, td) {
  border-top: 1px solid var(--table-total-border, var(--border));
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.table .table-group:has(> .table-group-header [aria-expanded="false"]) > tr:not(.table-group-header, .table-group-total) {
  display: none;
}

/* Tree rows: <tr data-level="2"> (or aria-level in a treegrid) indents the
   first cell. Rows with children carry a .table-toggle; leaf rows are
   indented by its width too so their text lines up. The app hides the rows
   of a collapsed branch (the hidden attribute). */
.table tr:is([data-level="2"], [aria-level="2"]) { --_level: 1; }
.table tr:is([data-level="3"], [aria-level="3"]) { --_level: 2; }
.table tr:is([data-level="4"], [aria-level="4"]) { --_level: 3; }
.table tr:is([data-level="5"], [aria-level="5"]) { --_level: 4; }
.table tr:is([data-level="6"], [aria-level="6"]) { --_level: 5; }
.table tr:is([data-level="7"], [aria-level="7"]) { --_level: 6; }
.table tr:is([data-level], [aria-level]) > :first-child {
  padding-inline-start: calc(var(--cell-pad-x, calc(0.75em * var(--density))) + var(--_level, 0) * var(--table-indent, 1.25rem));
}
.table tr:is([data-level], [aria-level]) > :first-child:not(:has(> .table-toggle)) {
  padding-inline-start: calc(var(--cell-pad-x, calc(0.75em * var(--density))) + var(--_level, 0) * var(--table-indent, 1.25rem) + 1.4em);
}

/* --- Selection column and bulk-action bar (§18) ---------------------------
 * Rows select with core's tr.is-selected (add aria-selected="true" in a
 * grid). A checkbox-only first column shrinks to its content. A .bulk-bar
 * placed before the table (or its .table-wrap) shows only while a row in it
 * is selected (tr.is-selected, or the checkbox in its first cell checked); elsewhere, the app shows and hides it. */
/* :where() so .is-frozen-2's fixed first-column width wins. */
:where(.table tr > :first-child:has(> :is(.checkbox, .check):only-child)) { inline-size: 1px; text-align: center; }
/* A row checkbox wrapped in a label with a hidden name (CONTRACT.md "Touch
   and pointer") gets the full target width at every tier, not just touch. */
.table .check:has(> .visually-hidden:last-child) { min-inline-size: var(--tap-min); justify-content: center; }
.bulk-bar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-xs);
  padding: var(--bulk-bar-pad, var(--space-2xs) var(--space-s));
  background: var(--bulk-bar-bg, color-mix(in srgb, var(--accent) 14%, var(--surface)));
  color: var(--bulk-bar-fg, var(--text));
  border: 1px solid var(--bulk-bar-border, var(--accent));
  border-radius: var(--bulk-bar-radius, var(--radius));
}
.bulk-bar-count { margin-inline-end: auto; font-weight: 600; }
.bulk-bar:has(~ :is(.table, .table-wrap)):not(:has(~ .table tr.is-selected, ~ .table-wrap tr.is-selected,
  ~ .table td:first-child input[type="checkbox"]:checked, ~ .table-wrap td:first-child input[type="checkbox"]:checked)) { display: none; }

/* --- Loading, empty and load-more rows (§18) ----------------------------
 * .table.is-loading (with aria-busy="true") turns every body cell into a
 * skeleton bar, keeping the rows' size. .table-empty holds an .empty-state;
 * .table-more is the infinite-scroll row ("Loading more…", a spinner, or a
 * Load more button for htmx's revealed trigger). */
.table.is-loading tbody td { position: relative; color: transparent; }
.table.is-loading tbody td > * { visibility: hidden; }
.table.is-loading tbody td::after {
  content: "";
  display: block;
  position: absolute;
  inset-block-start: calc(50% - 0.45em);
  inset-inline: var(--cell-pad-x, calc(0.75em * var(--density)));
  block-size: 0.9em;
  border-radius: 0.2em;
  background-color: var(--skeleton-bg, var(--surface-2));
}
.table.is-loading tbody td:nth-child(3n + 2)::after { inset-inline-end: 30%; }
.table.is-loading tbody td:nth-child(3n)::after { inset-inline-end: 50%; }
@media (prefers-reduced-motion: no-preference) {
  .table.is-loading tbody td::after {
    background-image: linear-gradient(90deg, transparent, rgb(255 255 255 / 0.08), transparent);
    background-size: 40% 100%;
    background-repeat: no-repeat;
    animation: skeleton-shimmer 1.4s ease-in-out infinite;
  }
}

  :root[data-motion="always"] .table.is-loading tbody td::after {
    background-image: linear-gradient(90deg, transparent, rgb(255 255 255 / 0.08), transparent);
    background-size: 40% 100%;
    background-repeat: no-repeat;
    animation: skeleton-shimmer 1.4s ease-in-out infinite;
  }


/* Two classes plus :nth-child(n) to outweigh core's striped-and-hovered
   cell rule: these rows never take a hover or stripe fill. */
.table.table tbody tr:nth-child(n):is(.table-empty, .table-more) > td {
  background: transparent;
  border-bottom: 0;
}
.table tr.table-empty > td { padding: 0; }
.table tr.table-more > td {
  text-align: center;
  color: var(--muted);
  font-size: 0.9em;
}
.table tr.table-more > td > :is(.spinner, .indicator) { vertical-align: middle; margin-inline-end: 0.5em; }

/* --- Table in a modal (§9 #48) -------------------------------------------
 * <div class="modal"> … <div class="table-wrap"><table class="table is-sticky">
 * The modal becomes a column: its header and footer stay put, the
 * .table-wrap is the scrolling body, and the sticky header sticks to it.
 * Zero specificity so .is-minimized and themes still win; only applied
 * while the modal is shown, so a closed popover or dialog stays hidden. */
:where(.modal:has(> .table-wrap):is(:popover-open, [open], :not([popover], dialog))) {
  display: flex;
  flex-direction: column;
}
.modal > .table-wrap {
  flex: 1 1 auto;
  min-block-size: 0;
  overflow: auto;
}
/* --- v5 vehicle instruments (cockpit-car/-plane/-jet.html) -----------------
 * Generic cockpit parts for cars, aircraft, boats, submarines, spacecraft:
 *   .attitude      artificial horizon (--pitch, --roll in degrees), pitch
 *                  ladder, roll scale, aircraft symbol; .is-hud = see-through
 *                  combiner style that fills its positioned parent
 *   .tape          moving vertical scale with a boxed readout (--value),
 *                  labels/bugs/bands at --at (speed, altitude, depth, V/S)
 *   .radar         scope with rings and a sweep; .radar-blip at
 *                  --bearing/--range (or --x/--y), friend/foe/unknown
 *                  shapes; .is-sector fan, .is-static (RWR, passive sonar)
 *   .annunciator   caption-light panel (caution/warning/advisory/on), the
 *                  .annunciator-master push-to-reset light, .is-gear and
 *                  .is-fma layouts
 *   .telltale      icon warning lamp (car cluster), .is-blinking
 *   .tyres         four-corner pressure diagram round a body outline
 *   .mfd           multi-function display: screen ringed by soft keys; top
 *                  keys (radios) switch .mfd-page
 *   .guard         guarded switch: a cover checkbox that must be lifted
 *   .checklist     challenge/response list with leader dots
 *   .throttle      quadrant housing core .fader levers and .scale detents
 *   .pfd           primary-flight-display layout (FMA, speed, attitude,
 *                  altitude, V/S, heading)
 *   .flight-hud    head-up display over an .attitude.is-hud scene: heading
 *                  strip, tapes, gun cross, flight-path marker, designator
 * Reuse the rest: .gauge-dial (+ --gauge-redline) for speedo/tacho/N1/G,
 * .gauge-arc for fuel, .compass/.compass-strip for heading, .map for nav,
 * .fader for levers, .toggle-group/.toggle-btn for switches.
 *
 * Colours read --<component>-* tokens with base-token fallbacks; values come
 * in through inline custom properties. No JavaScript. Motion runs only under
 * prefers-reduced-motion: no-preference (core's data-motion="reduced"
 * pauses it). Docs: docs/components/vehicles.md. */

@property --vehicles-sweep {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}
@keyframes vehicles-sweep { to { --vehicles-sweep: 360deg; } }
@keyframes vehicles-blink { 50% { opacity: var(--vehicles-blink-dim, 0.25); } }

/* ---- .attitude: artificial horizon --------------------------------------
 * <div class="attitude" style="--pitch:4; --roll:-12" role="img" aria-label="…">
 *   <div class="attitude-horizon">
 *     <ol class="attitude-ladder" aria-hidden="true"><li style="--at:10"></li>…</ol>
 *   </div>
 * </div>
 * ::before = fixed roll scale, ::after = aircraft symbol, the horizon layer
 * (sky, ground, ladder, roll pointer) turns by --roll and slides by --pitch.
 * Everything is in cqi, so --attitude-size scales the lot. */
.attitude {
  --_deg: var(--attitude-pitch-scale, 1.6cqi);
  --_ink: var(--attitude-ink, var(--text));
  container-type: inline-size;
  position: relative;
  isolation: isolate;
  overflow: hidden;
  display: inline-block;
  vertical-align: middle;
  flex: none;
  inline-size: var(--attitude-size, 16rem);
  max-inline-size: 100%;
  aspect-ratio: 1;
  background: var(--attitude-ground, color-mix(in oklab, var(--warning) 42%, var(--bg)));
  border: var(--attitude-border-width, 1px) solid var(--attitude-border, var(--border));
  border-radius: var(--attitude-radius, calc(var(--radius) * 2));
  box-shadow: var(--attitude-shadow, none);
  font-family: var(--attitude-font, var(--font-mono));
  font-variant-numeric: tabular-nums;
}
.attitude-horizon {
  position: absolute;
  inset: -100cqi;
  transform: rotate(calc(var(--roll, 0) * -1deg)) translateY(calc(var(--pitch, 0) * var(--_deg)));
  background:
    linear-gradient(var(--attitude-horizon-line, var(--_ink)), var(--attitude-horizon-line, var(--_ink))) center / 100% var(--attitude-horizon-width, 2px) no-repeat,
    linear-gradient(
      var(--attitude-sky-top, color-mix(in oklab, var(--attitude-sky, color-mix(in oklab, var(--accent) 50%, var(--bg))) 55%, var(--bg))) calc(50% - 70cqi),
      var(--attitude-sky, color-mix(in oklab, var(--accent) 50%, var(--bg))) 50%,
      var(--attitude-ground, color-mix(in oklab, var(--warning) 42%, var(--bg))) 50%,
      var(--attitude-ground-bottom, color-mix(in oklab, var(--attitude-ground, color-mix(in oklab, var(--warning) 42%, var(--bg))) 60%, var(--bg))) calc(50% + 70cqi));
}
@media (prefers-reduced-motion: no-preference) {
  .attitude-horizon { transition: transform var(--attitude-speed, 0.4s) ease-out; }
}

  :root[data-motion="always"] .attitude-horizon { transition: transform var(--attitude-speed, 0.4s) ease-out; }

/* Ladder rungs every 10° (long) and 5° (short), ±90°. */
.attitude-horizon::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  translate: -50% -50%;
  inline-size: var(--attitude-rung, 30cqi);
  block-size: calc(180 * var(--_deg));
  background:
    linear-gradient(var(--_ink) 0 2px, transparent 0) center calc(-1px) / 100% calc(10 * var(--_deg)) repeat-y,
    linear-gradient(var(--_ink) 0 1px, transparent 0) center calc(5 * var(--_deg) - 0.5px) / 45% calc(10 * var(--_deg)) repeat-y;
  mask: var(--attitude-rung-mask, none);
}
/* Roll pointer: rides the horizon, cancelling the pitch shift. */
.attitude-horizon::after {
  content: "";
  position: absolute;
  left: 50%;
  top: calc(50% - var(--pitch, 0) * var(--_deg) - 38.5cqi);
  translate: -50% 0;
  inline-size: 5cqi;
  block-size: 4cqi;
  background: var(--attitude-pointer, var(--_ink));
  clip-path: polygon(50% 0, 100% 100%, 0 100%);
}
.attitude-ladder {
  position: absolute;
  inset: 0;
  margin: 0;
  padding: 0;
  list-style: none;
}
/* Rung labels: --at (integer degrees) prints on both ends of the rung. */
.attitude-ladder > li {
  counter-reset: attitude-pitch var(--at, 0);
  position: absolute;
  left: 50%;
  top: calc(50% - var(--at, 0) * var(--_deg));
  translate: -50% -50%;
  display: flex;
  justify-content: space-between;
  inline-size: calc(var(--attitude-rung, 30cqi) + 2 * var(--attitude-label-room, 9cqi));
  color: var(--_ink);
  font-size: var(--attitude-label-size, 4.5cqi);
  line-height: 1;
}
.attitude-ladder > li::before,
.attitude-ladder > li::after { content: counter(attitude-pitch); }
/* Fixed roll scale: ticks at 0, ±10, ±20, ±30, ±45, ±60 on a ring. */
.attitude::before,
.attitude::after {
  content: "";
  position: absolute;
  z-index: var(--z-raised);
  pointer-events: none;
}
.attitude::before {
  inset: 6cqi;
  border-radius: 50%;
  --_t: var(--attitude-scale-ink, var(--_ink));
  background: conic-gradient(from -61deg,
    var(--_t) 0 2deg, transparent 0 15.4deg, var(--_t) 0 16.6deg, transparent 0 30.4deg,
    var(--_t) 0 31.6deg, transparent 0 40.4deg, var(--_t) 0 41.6deg, transparent 0 50.4deg,
    var(--_t) 0 51.6deg, transparent 0 59deg, var(--_t) 0 63deg, transparent 0 70.4deg,
    var(--_t) 0 71.6deg, transparent 0 80.4deg, var(--_t) 0 81.6deg, transparent 0 90.4deg,
    var(--_t) 0 91.6deg, transparent 0 105.4deg, var(--_t) 0 106.6deg, transparent 0 120deg,
    var(--_t) 0 122deg, transparent 0);
  mask: radial-gradient(farthest-side, transparent calc(100% - 5cqi), #000 calc(100% - 5cqi + 0.5px));
}
/* Aircraft symbol: two wings and a centre dot, fixed. */
.attitude::after {
  inset: 0;
  --_s: var(--attitude-symbol, var(--warning));
  background:
    linear-gradient(var(--_s), var(--_s)) center / 3.2cqi 3.2cqi no-repeat,
    linear-gradient(var(--_s), var(--_s)) left 14cqi center / 22cqi 2.6cqi no-repeat,
    linear-gradient(var(--_s), var(--_s)) right 14cqi center / 22cqi 2.6cqi no-repeat,
    linear-gradient(var(--_s), var(--_s)) left 33.4cqi top calc(50% + 1.3cqi) / 2.6cqi 5cqi no-repeat,
    linear-gradient(var(--_s), var(--_s)) right 33.4cqi top calc(50% + 1.3cqi) / 2.6cqi 5cqi no-repeat;
  filter: drop-shadow(0 0 1px var(--attitude-symbol-edge, var(--bg)));
}
/* HUD style: fills its positioned parent, the scene behind the glass. */
.attitude.is-hud {
  --_deg: var(--attitude-pitch-scale, 1.2cqi);
  --_ink: var(--attitude-ink, var(--flight-hud-ink, var(--accent-text, var(--accent))));
  --attitude-sky: var(--attitude-hud-sky, color-mix(in oklab, var(--accent) 24%, var(--bg)));
  --attitude-sky-top: var(--attitude-hud-sky-top, var(--bg));
  --attitude-ground: var(--attitude-hud-ground, color-mix(in oklab, var(--text) 16%, var(--bg)));
  --attitude-ground-bottom: var(--attitude-hud-ground-bottom, var(--bg));
  --attitude-horizon-line: var(--_ink);
  --attitude-rung: 26cqi;
  --attitude-rung-mask: linear-gradient(90deg, #000 0 32%, transparent 0 68%, #000 0);
  --attitude-label-size: clamp(0.7rem, 1.6cqi, 1.05rem);
  --attitude-label-room: 4.5rem;
  position: absolute;
  inset: 0;
  z-index: -1;
  inline-size: auto;
  max-inline-size: none;
  aspect-ratio: auto;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}
.attitude.is-hud::before,
.attitude.is-hud::after,
.attitude.is-hud .attitude-horizon::after { display: none; }
.attitude.is-hud .attitude-ladder > li { text-shadow: var(--flight-hud-glow, 0 0 0.35em color-mix(in srgb, currentColor 55%, transparent)); }

/* ---- .tape: moving vertical scale ----------------------------------------
 * <div class="tape" role="meter" aria-label="Airspeed, knots" aria-valuenow="248"
 *      aria-valuemin="0" aria-valuemax="400" style="--value:248; --tape-span:80; --tape-major:20; --tape-minor:10">
 *   <ol class="tape-scale" aria-hidden="true"><li style="--at:220">220</li>…</ol>
 *   <span class="tape-band is-danger" style="--from:340; --to:400"></span>
 *   <span class="tape-bug" style="--at:250"></span>
 *   <span class="tape-readout">248</span>
 * </div>
 * The scale slides so --value sits at the readout; --tape-span units are
 * visible. Ticks on the end edge; .is-end puts them on the start edge
 * (altitude on the right of a PFD). */
.tape {
  --_u: calc(100cqb / var(--tape-span, 100));
  --_y: calc(50cqb + var(--value, 0) * var(--_u));
  --_tick: var(--tape-tick, var(--text));
  container-type: size;
  position: relative;
  overflow: hidden;
  flex: none;
  inline-size: var(--tape-width, 4.75rem);
  block-size: var(--tape-height, 16rem);
  background: var(--tape-bg, var(--surface-2));
  color: var(--tape-fg, var(--text));
  border: var(--tape-border-width, 1px) solid var(--tape-border, var(--border));
  border-radius: var(--tape-radius, var(--radius));
  font-family: var(--tape-font, var(--font-mono));
  font-size: var(--tape-label-size, 0.75rem);
  font-variant-numeric: tabular-nums;
}
.tape::before {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-end: 0;
  inline-size: 0.8rem;
  background:
    linear-gradient(var(--_tick) 0 1px, transparent 0) right calc(var(--_y) - 0.5px) / 100% calc(var(--tape-major, 20) * var(--_u)) repeat-y,
    linear-gradient(var(--_tick) 0 1px, transparent 0) right calc(var(--_y) - 0.5px) / 50% calc(var(--tape-minor, 10) * var(--_u)) repeat-y;
}
.tape.is-end::before {
  inset-inline: 0 auto;
  background-position-x: left, left;
}
.tape-scale { position: absolute; inset: 0; margin: 0; padding: 0; list-style: none; }
.tape-scale > li {
  position: absolute;
  inset-inline: 0.35rem 1.1rem;
  top: calc(50% + (var(--value, 0) - var(--at, 0)) * var(--_u));
  translate: 0 -50%;
  line-height: 1;
  text-align: end;
  white-space: nowrap;
}
.tape.is-end .tape-scale > li { inset-inline: 1.1rem 0.35rem; text-align: start; }
/* A unit label hides the scale passing under it (tapes on a see-through face). */
.tape:has(> .tape-unit) .tape-scale { mask: linear-gradient(transparent 1.5rem, #000 2.2rem); }
/* Coloured range (flap limits, overspeed, shallows). .is-danger = barber pole. */
.tape-band {
  --_c: var(--tape-band, var(--success));
  position: absolute;
  inset-inline-end: 0;
  inline-size: 0.3rem;
  top: calc(50% + (var(--value, 0) - var(--to, 0)) * var(--_u));
  block-size: calc((var(--to, 0) - var(--from, 0)) * var(--_u));
  background: var(--_c);
}
.tape.is-end .tape-band { inset-inline: 0 auto; }
.tape-band.is-caution { --_c: var(--tape-band-caution, var(--warning)); }
.tape-band.is-danger {
  --_c: repeating-linear-gradient(180deg, var(--tape-band-danger, var(--danger)) 0 0.4rem, var(--tape-band-danger-alt, var(--bg)) 0 0.8rem);
  inline-size: 0.45rem;
}
/* Selected value (speed bug, target altitude); parks at the edge off-scale. */
.tape-bug {
  position: absolute;
  z-index: var(--z-raised);
  inset-inline-end: 0;
  top: clamp(0px, calc(50% + (var(--value, 0) - var(--at, 0)) * var(--_u)), 100cqb);
  translate: 0 -50%;
  inline-size: 0.6rem;
  block-size: 1.1rem;
  background: var(--tape-bug, var(--flare, var(--accent)));
  clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%, 0 70%, 60% 50%, 0 30%);
}
.tape.is-end .tape-bug { inset-inline: 0 auto; scale: -1 1; }
.tape-readout {
  position: absolute;
  z-index: calc(var(--z-raised) + 1);
  inset-inline: 0.15rem 0.5rem;
  top: 50%;
  translate: 0 -50%;
  padding: 0.3rem 0.4rem;
  background: var(--tape-readout-bg, var(--bg));
  color: var(--tape-readout-fg, var(--readout-fg, var(--accent-text, var(--accent))));
  border: var(--tape-readout-border-width, 1px) solid var(--tape-readout-border, var(--text));
  border-radius: var(--tape-readout-radius, 2px);
  font-size: min(var(--tape-readout-size, 1.05rem), 19cqi);
  font-weight: 700;
  line-height: 1.15;
  text-align: end;
  white-space: nowrap;
}
.tape.is-end .tape-readout { inset-inline: 0.5rem 0.15rem; text-align: start; }
/* Unit or label printed at the top edge of the tape. */
.tape-unit {
  position: absolute;
  z-index: calc(var(--z-raised) + 1);
  inset: 0 0 auto;
  padding: 0.2rem 0.35rem;
  background: var(--tape-bg, var(--surface-2));
  color: var(--tape-unit-fg, var(--muted));
  font-size: 0.7rem;
  text-align: center;
}

/* ---- .radar: scope with range rings and a sweep --------------------------
 * <div class="radar" role="img" aria-label="Radar, 3 contacts …" style="--sweep:40">
 *   <span class="radar-blip is-foe" style="--bearing:35; --range:.62">B1</span>
 * </div>
 * --bearing in degrees clockwise from up, --range 0–1 of the radius (or
 * --x/--y as percentages). .is-sector draws a --radar-span fan with its
 * origin at the bottom centre; .is-static drops the sweep; .is-rwr prints
 * the label inside the symbol. */
.radar {
  --_ring: var(--radar-ring, color-mix(in srgb, var(--radar-fg, var(--accent)) 38%, transparent));
  container-type: inline-size;
  position: relative;
  isolation: isolate;
  overflow: hidden;
  display: inline-block;
  vertical-align: middle;
  flex: none;
  inline-size: var(--radar-size, 14rem);
  max-inline-size: 100%;
  aspect-ratio: 1;
  background: var(--radar-bg, color-mix(in oklab, var(--radar-fg, var(--accent)) 7%, var(--bg)));
  color: var(--radar-fg, var(--accent-text, var(--accent)));
  border: var(--radar-border-width, 1px) solid var(--radar-border, var(--border));
  border-radius: 50%;
  box-shadow: var(--radar-shadow, none);
  font-family: var(--radar-font, var(--font-mono));
}
.radar::before,
.radar::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  mask: var(--_fan, none);
}
/* Scope: --radar-rings range rings and a cross, centred 50cqi down. */
.radar::before {
  --_step: calc(50cqi / var(--radar-rings, 4));
  background:
    radial-gradient(circle at 50% 50cqi, var(--_ring) 0 0.6cqi, transparent 0.7cqi),
    repeating-radial-gradient(circle at 50% 50cqi, transparent 0 calc(var(--_step) - 1px), var(--_ring) 0 var(--_step)),
    linear-gradient(var(--_ring), var(--_ring)) 50% 0 / 1px 100% no-repeat,
    linear-gradient(var(--_ring), var(--_ring)) 0 50cqi / 100% 1px no-repeat;
}
.radar::after {
  z-index: -1;
  --vehicles-sweep: calc(var(--sweep, 0) * 1deg);
  background: conic-gradient(from calc(var(--vehicles-sweep) - var(--radar-trail, 70deg)) at 50% 50cqi,
    transparent, color-mix(in srgb, var(--radar-sweep, var(--radar-fg, var(--accent))) 45%, transparent) var(--radar-trail, 70deg),
    transparent 0);
}
@media (prefers-reduced-motion: no-preference) {
  .radar:not(.is-static)::after { animation: vehicles-sweep var(--radar-speed, 4s) linear infinite; }
}

  :root[data-motion="always"] .radar:not(.is-static)::after { animation: vehicles-sweep var(--radar-speed, 4s) linear infinite; }

.radar.is-static::after { display: none; }
.radar.is-sector {
  --_fan: conic-gradient(from calc(var(--radar-span, 120deg) / -2) at 50% 100%, #000 0 var(--radar-span, 120deg), transparent 0);
  aspect-ratio: 2 / 1;
  border-radius: 50% 50% var(--radius) var(--radius) / 100% 100% var(--radius) var(--radius);
}
.radar-blip {
  --_c: var(--radar-blip, var(--radar-fg, var(--accent-text, var(--accent))));
  --_size: var(--radar-blip-size, max(0.55rem, 3.6cqi));
  position: absolute;
  z-index: var(--z-raised);
  left: var(--x, calc(50% + sin(var(--bearing, 0) * 1deg) * var(--range, 0) * 50cqi));
  top: var(--y, calc(50cqi - cos(var(--bearing, 0) * 1deg) * var(--range, 0) * 50cqi));
  padding-inline-start: calc(var(--_size) / 2 + 0.25em);
  translate: 0 -50%;
  color: var(--radar-label-fg, var(--radar-fg, var(--text)));
  font-size: var(--radar-label-size, max(0.65rem, 3.4cqi));
  line-height: 1;
  white-space: nowrap;
}
.radar-blip::before {
  content: "";
  position: absolute;
  left: 0;
  top: 50%;
  translate: -50% -50%;
  inline-size: var(--_size);
  aspect-ratio: 1;
  border-radius: 50%;
  background: var(--_c);
  box-shadow: 0 0 0.35rem var(--_c);
}
/* Not colour alone: friend = ring, foe = diamond, unknown = square. */
.radar-blip.is-friend { --_c: var(--radar-friend, var(--success)); }
.radar-blip.is-friend::before { background: none; border: 2px solid var(--_c); }
.radar-blip.is-foe { --_c: var(--radar-foe, var(--danger)); }
.radar-blip.is-foe::before { border-radius: 0; rotate: 45deg; scale: 0.85; }
.radar-blip.is-unknown { --_c: var(--radar-unknown, var(--warning)); }
.radar-blip.is-unknown::before { border-radius: 0; background: none; border: 2px solid var(--_c); }
.radar-blip.is-faint { opacity: 0.55; }
/* RWR / contact codes: the label sits inside the symbol. */
.radar.is-rwr .radar-blip { padding: 0.25em 0.3em; translate: -50% -50%; color: var(--_c); font-weight: 700; }
.radar.is-rwr .radar-blip::before { inset: 0; left: 0; top: 0; translate: none; inline-size: auto; aspect-ratio: auto; background: none; border: 2px solid var(--_c); box-shadow: none; }
.radar.is-rwr .radar-blip.is-foe::before { rotate: none; scale: none; clip-path: none; border-radius: 0; border-style: solid; }
.radar.is-rwr .radar-blip:not(.is-foe, .is-unknown, .is-friend)::before { border-color: transparent; }

/* ---- .annunciator: caption lights ----------------------------------------
 * <ul class="annunciator" aria-label="Caution panel">
 *   <li>Fuel press</li> <li class="is-caution">Hyd 2 <span class="visually-hidden">(lit)</span></li>
 *   <li class="is-warning">Eng fire</li> <li class="is-advisory">APU run</li> <li class="is-on">Seat belts</li>
 * </ul>
 * <label class="annunciator-master is-caution"><input type="checkbox"> Master caution</label>
 * Unlit captions are dim and etched; lit ones take a tint, border and glow in
 * the state colour and full-contrast text (--annunciator-lit-fg). The state
 * text colours fail contrast on the tint in many themes.
 * The master light flashes until its checkbox (push to reset) is checked. */
.annunciator {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, var(--annunciator-min, 6.5rem)), 1fr));
  gap: var(--annunciator-gap, 3px);
  margin: 0;
  padding: var(--annunciator-pad, 3px);
  list-style: none;
  background: var(--annunciator-bg, var(--bg));
  border: var(--annunciator-border-width, 1px) solid var(--annunciator-border, var(--border));
  border-radius: var(--annunciator-radius, var(--radius));
}
:where(.annunciator > li, .annunciator-master) {
  --_c: var(--annunciator-on, var(--accent));
  display: grid;
  place-items: center;
  min-block-size: var(--annunciator-cell-height, 2.6rem);
  padding: 0.3rem 0.45rem;
  background: var(--annunciator-off, var(--surface-2));
  color: var(--annunciator-off-fg, var(--muted));
  border: 1px solid var(--annunciator-cell-border, var(--hairline));
  border-radius: var(--annunciator-cell-radius, 2px);
  font-family: var(--annunciator-font, var(--font-mono));
  font-size: var(--annunciator-size, 0.7rem);
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: 0.05em;
  text-align: center;
  text-transform: uppercase;
}
:is(.annunciator > li, .annunciator-master).is-caution { --_c: var(--annunciator-caution, var(--warning)); }
:is(.annunciator > li, .annunciator-master).is-warning { --_c: var(--annunciator-warning, var(--danger)); }
:is(.annunciator > li, .annunciator-master).is-advisory { --_c: var(--annunciator-advisory, var(--success)); }
.annunciator > li:is(.is-caution, .is-warning, .is-advisory, .is-on),
.annunciator-master:not(:has(input:checked)) {
  background: color-mix(in srgb, var(--_c) var(--annunciator-lit-mix, 12%), var(--annunciator-off, var(--surface-2)));
  color: var(--annunciator-lit-fg, var(--text));
  border-color: var(--_c);
  box-shadow: var(--annunciator-glow, inset 0 0 0.7rem color-mix(in srgb, var(--_c) 35%, transparent), 0 0 0.45rem color-mix(in srgb, var(--_c) 40%, transparent));
}
.annunciator-master {
  position: relative;
  display: inline-grid;
  min-inline-size: var(--tap-min);
  min-block-size: max(var(--tap-min), var(--annunciator-cell-height, 2.6rem));
  padding-inline: 0.7rem;
  user-select: none;
}
.annunciator-master input { position: absolute; inline-size: 1px; block-size: 1px; margin: 0; opacity: 0; clip-path: inset(50%); }
.annunciator-master:has(input:focus-visible) { outline: 2px solid var(--focus); outline-offset: 2px; }
@media (prefers-reduced-motion: no-preference) {
  .annunciator > li.is-blinking:is(.is-caution, .is-warning, .is-advisory, .is-on),
  .annunciator-master:not(:has(input:checked)) { animation: vehicles-blink 1s steps(1, end) infinite; }
}

  :root[data-motion="always"] .annunciator > li.is-blinking:is(.is-caution, .is-warning, .is-advisory, .is-on),:root[data-motion="always"] .annunciator-master:not(:has(input:checked)) { animation: vehicles-blink 1s steps(1, end) infinite; }

/* Landing gear: nose on top, mains below. */
.annunciator.is-gear { grid-template-columns: repeat(2, minmax(0, 1fr)); max-inline-size: 14rem; }
.annunciator.is-gear > li:first-child { grid-column: 1 / -1; justify-self: center; inline-size: calc(50% - var(--annunciator-gap, 3px) / 2); }
/* Flight mode annunciator: one row of equal boxes. */
.annunciator.is-fma { grid-template-columns: none; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); }
.annunciator.is-fma > li { min-block-size: 2rem; padding-inline: 0.25rem; }
@media (forced-colors: active) {
  .annunciator > li:is(.is-caution, .is-warning, .is-advisory, .is-on),
  .annunciator-master:not(:has(input:checked)) { border: 3px solid Highlight; }
}

/* ---- .telltale: icon warning lamp ----------------------------------------
 * <span class="telltale is-on is-warning" role="img" aria-label="Check engine, on">
 *   <svg class="icon"><use href="…#icon-warning"/></svg></span>
 * Off = dim; .is-on lights it (green by default; .is-info, .is-warning,
 * .is-danger), .is-blinking flashes (turn signals, hazards). */
.telltale {
  --_c: var(--telltale-on, var(--success-text, var(--success)));
  display: inline-grid;
  place-items: center;
  flex: none;
  inline-size: var(--telltale-size, 1.9rem);
  aspect-ratio: 1;
  color: var(--telltale-off, color-mix(in srgb, var(--muted) 45%, transparent));
}
.telltale .icon { --icon-size: 72%; inline-size: 72%; block-size: 72%; }
.telltale.is-info { --_c: var(--telltale-info, var(--accent-text, var(--accent))); }
.telltale.is-warning { --_c: var(--telltale-warning, var(--warning-text, var(--warning))); }
.telltale.is-danger { --_c: var(--telltale-danger, var(--danger-text, var(--danger))); }
.telltale.is-on { color: var(--_c); filter: var(--telltale-glow, drop-shadow(0 0 0.3rem color-mix(in srgb, var(--_c) 70%, transparent))); }
@media (prefers-reduced-motion: no-preference) {
  .telltale.is-on.is-blinking { animation: vehicles-blink 0.8s steps(1, end) infinite; --vehicles-blink-dim: 0.12; }
}

  :root[data-motion="always"] .telltale.is-on.is-blinking { animation: vehicles-blink 0.8s steps(1, end) infinite; --vehicles-blink-dim: 0.12; }


/* ---- .tyres: pressure per wheel round a body outline ---------------------
 * <ul class="tyres" aria-label="Tyre pressures">
 *   <li>FL <span class="readout readout-sm">2.4</span></li> FR, RL, RR …
 *   <li class="is-low">RR <span class="readout readout-sm">1.9</span></li>
 * </ul>
 * Order: front-left, front-right, rear-left, rear-right. .is-low (caution),
 * .is-flat (warning) recolour the tyre and its text. */
.tyres {
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--tyres-body-width, 5.5rem) minmax(0, 1fr);
  grid-template-rows: repeat(2, minmax(var(--tyres-row, 4.5rem), auto));
  column-gap: 0.6rem;
  margin: 0;
  padding: 0;
  list-style: none;
  font-variant-numeric: tabular-nums;
}
.tyres::before {
  content: "";
  grid-column: 2;
  grid-row: 1 / 3;
  border: 2px solid var(--tyres-body-border, var(--border));
  border-radius: 42% 42% 30% 30% / 18% 18% 12% 12%;
  background:
    linear-gradient(var(--tyres-glass, color-mix(in srgb, var(--accent) 22%, var(--surface-2))), var(--tyres-glass, color-mix(in srgb, var(--accent) 22%, var(--surface-2)))) center 22% / 74% 16% no-repeat,
    linear-gradient(var(--tyres-glass, color-mix(in srgb, var(--accent) 22%, var(--surface-2))), var(--tyres-glass, color-mix(in srgb, var(--accent) 22%, var(--surface-2)))) center 82% / 70% 10% no-repeat,
    var(--tyres-body, var(--surface-2));
}
.tyres > li {
  --_c: var(--tyres-ok, var(--muted));
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-inline-size: 0;
  font-size: 0.8rem;
}
.tyres > li:nth-child(odd) { grid-column: 1; justify-content: flex-end; text-align: end; }
.tyres > li:nth-child(even) { grid-column: 3; flex-direction: row-reverse; justify-content: flex-end; }
.tyres > li:nth-child(-n + 2) { grid-row: 1; }
.tyres > li:nth-child(n + 3) { grid-row: 2; }
.tyres > li::after {
  content: "";
  flex: none;
  inline-size: 0.8rem;
  block-size: 2.4rem;
  border-radius: 0.3rem;
  background: var(--_c);
}
.tyres > li.is-low { --_c: var(--tyres-low, var(--warning)); color: var(--warning-text, var(--warning)); }
.tyres > li.is-flat { --_c: var(--tyres-flat, var(--danger)); color: var(--danger-text, var(--danger)); }
.tyres > li:is(.is-low, .is-flat) .readout { --readout-fg: currentColor; }

/* ---- .mfd: multi-function display with soft keys -------------------------
 * <div class="mfd">
 *   <div class="mfd-keys is-top" role="radiogroup" aria-label="Format">
 *     <label class="mfd-key"><input type="radio" name="mfd-l" checked>RDR</label> … (≤ 6)
 *   </div>
 *   <div class="mfd-keys is-start"> <button class="mfd-key" type="button">RNG+</button> … </div>
 *   <div class="mfd-screen"> <section class="mfd-page">…</section> … </div>
 *   <div class="mfd-keys is-end">…</div>  <div class="mfd-keys is-bottom">…</div>
 * </div>
 * Top key n shows page n (no checked key = page 1). Other keys are
 * buttons or checkbox labels (.mfd-key, pressed = checked/aria-pressed). */
.mfd {
  display: grid;
  grid-template:
    ". top ." auto
    "start screen end" minmax(0, 1fr)
    ". bottom ." auto / auto minmax(0, 1fr) auto;
  gap: var(--mfd-gap, 0.5rem);
  min-inline-size: 0;
  padding: var(--mfd-pad, 0.6rem);
  background: var(--mfd-bezel, var(--surface-2));
  border: var(--mfd-border-width, 1px) solid var(--mfd-border, var(--border));
  border-radius: var(--mfd-radius, calc(var(--radius) * 2));
  box-shadow: var(--mfd-shadow, none);
}
.mfd-keys { display: flex; justify-content: space-evenly; gap: max(8px, var(--mfd-key-gap, 0.4rem)); min-inline-size: 0; }
.mfd-keys.is-top { grid-area: top; }
.mfd-keys.is-bottom { grid-area: bottom; }
.mfd-keys.is-start { grid-area: start; flex-direction: column; }
.mfd-keys.is-end { grid-area: end; flex-direction: column; }
.mfd-key {
  position: relative;
  display: inline-grid;
  place-items: center;
  min-inline-size: max(var(--tap-min), 2.75rem);
  min-block-size: var(--tap-min);
  padding: 0.35rem 0.45rem;
  background: var(--mfd-key-bg, var(--surface));
  color: var(--mfd-key-fg, var(--text));
  border: 1px solid var(--mfd-key-border, var(--border));
  border-radius: var(--mfd-key-radius, var(--radius));
  box-shadow: var(--mfd-key-shadow, none);
  font: 700 0.7rem/1.1 var(--mfd-font, var(--font-mono));
  letter-spacing: 0.04em;
  text-transform: uppercase;
  user-select: none;
}
.mfd-key input { position: absolute; inline-size: 1px; block-size: 1px; margin: 0; opacity: 0; clip-path: inset(50%); }
.mfd-key:is(:has(input:checked), [aria-pressed="true"]) {
  color: var(--mfd-key-on-fg, var(--accent-text, var(--accent)));
  border-color: var(--mfd-key-on, var(--accent));
  box-shadow: inset 0 0 0 1px var(--mfd-key-on, var(--accent));
}
.mfd-key:is(:has(input:focus-visible), :focus-visible) { outline: 2px solid var(--focus); outline-offset: 2px; }
.mfd-screen {
  grid-area: screen;
  container-type: inline-size;
  position: relative;
  overflow: hidden;
  min-inline-size: 0;
  min-block-size: var(--mfd-screen-min, 14rem);
  padding: var(--mfd-screen-pad, var(--space-s));
  background: var(--mfd-screen-bg, var(--bg));
  color: var(--mfd-screen-fg, var(--text));
  border: 1px solid var(--mfd-screen-border, var(--hairline));
  border-radius: var(--mfd-screen-radius, var(--radius));
  font-family: var(--mfd-font, var(--font-mono));
  font-size: 0.8rem;
}
.mfd-screen > .mfd-page:not(:is(
  .mfd:not(:has(> .mfd-keys.is-top input:checked)) > .mfd-screen > .mfd-page:nth-child(1),
  .mfd:has(> .mfd-keys.is-top > :nth-child(1) input:checked) > .mfd-screen > .mfd-page:nth-child(1),
  .mfd:has(> .mfd-keys.is-top > :nth-child(2) input:checked) > .mfd-screen > .mfd-page:nth-child(2),
  .mfd:has(> .mfd-keys.is-top > :nth-child(3) input:checked) > .mfd-screen > .mfd-page:nth-child(3),
  .mfd:has(> .mfd-keys.is-top > :nth-child(4) input:checked) > .mfd-screen > .mfd-page:nth-child(4),
  .mfd:has(> .mfd-keys.is-top > :nth-child(5) input:checked) > .mfd-screen > .mfd-page:nth-child(5),
  .mfd:has(> .mfd-keys.is-top > :nth-child(6) input:checked) > .mfd-screen > .mfd-page:nth-child(6)
)) { display: none; }

/* ---- .guard: guarded switch ----------------------------------------------
 * <div class="guard">
 *   <label class="guard-cover"><input type="checkbox"><span class="guard-closed">Safe · lift guard</span><span class="guard-open">Close guard</span></label>
 *   <label class="toggle-btn is-danger"><input type="checkbox">Master arm</label>
 * </div>
 * Closed, the striped cover hides the switch (it can't be reached, by
 * pointer or keyboard); lifted, the switch shows under the open cover. */
.guard {
  display: inline-grid;
  grid-template-rows: auto auto;
  gap: 0.4rem;
  min-inline-size: 0;
}
.guard > * { grid-column: 1; grid-row: 2; }
.guard-cover {
  position: relative;
  display: grid;
  place-items: center;
  min-block-size: var(--tap-min);
  padding: 0.4rem 0.6rem;
  border: 2px solid var(--guard-color, var(--danger));
  border-radius: var(--guard-radius, var(--radius));
  font: 700 0.72rem/1.15 var(--guard-font, var(--font-mono));
  letter-spacing: 0.05em;
  text-align: center;
  text-transform: uppercase;
  user-select: none;
}
.guard > .guard-cover { grid-row: 1; }
.guard:not(:has(> .guard-cover input:checked)) > .guard-cover {
  grid-row: 1 / 3;
  background: var(--guard-stripes, repeating-linear-gradient(-45deg, var(--guard-color, var(--danger)) 0 0.55rem, color-mix(in srgb, var(--guard-color, var(--danger)) 55%, var(--bg)) 0 1.1rem));
}
.guard-cover > span { padding: 0.3rem 0.5rem; background: var(--guard-label-bg, var(--bg)); color: var(--guard-label-fg, var(--text)); border-radius: 2px; }
.guard-cover input { position: absolute; inline-size: 1px; block-size: 1px; margin: 0; opacity: 0; clip-path: inset(50%); }
.guard-cover:has(input:focus-visible) { outline: 2px solid var(--focus); outline-offset: 2px; }
.guard:not(:has(> .guard-cover input:checked)) > :not(.guard-cover) { visibility: hidden; }
.guard-cover:has(input:checked) .guard-closed,
.guard-cover:not(:has(input:checked)) .guard-open { display: none; }

/* ---- .checklist: challenge … response ------------------------------------
 * <ol class="checklist">
 *   <li><label><input class="checkbox" type="checkbox"><span>Parking brake</span><b>Set</b></label></li>
 * </ol>
 * Leader dots join challenge and response; done items take the done colour. */
.checklist { margin: 0; padding: 0; list-style: none; font-family: var(--checklist-font, var(--font-mono)); font-size: 0.85rem; }
.checklist > li + li { border-block-start: 1px solid var(--checklist-rule, var(--hairline)); }
.checklist label {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  min-block-size: max(var(--tap-min), 2.25rem);
}
.checklist label > span { display: flex; flex: 1; align-items: baseline; gap: 0.4rem; min-inline-size: 0; }
.checklist label > span::after { content: ""; flex: 1; min-inline-size: 1rem; border-block-end: 2px dotted var(--checklist-leader, var(--muted)); }
.checklist label > :is(b, strong) { text-transform: uppercase; color: var(--checklist-response, var(--accent-text, var(--accent))); }
.checklist > li:has(input:checked) { color: var(--checklist-done, var(--success-text, var(--success))); }
.checklist > li:has(input:checked) :is(b, strong) { color: inherit; }

/* ---- .throttle: lever quadrant -------------------------------------------
 * <div class="throttle" role="group" aria-label="Thrust levers">
 *   <div class="scale"><span style="--at:1">TOGA</span>…</div>
 *   <label class="throttle-lever"><input class="fader" type="range" …><span>1</span></label> …
 * </div>
 * Core .fader levers (vertical, native) with a .scale of detents. */
.throttle {
  display: inline-flex;
  align-items: stretch;
  gap: var(--throttle-gap, 0.75rem);
  block-size: var(--throttle-height, 14rem);
  padding: 0.75rem;
  background: var(--throttle-bg, var(--surface-2));
  border: 1px solid var(--throttle-border, var(--border));
  border-radius: var(--throttle-radius, var(--radius));
  --fader-cap-width: var(--throttle-handle-width, 2.75rem);
  --fader-cap-height: var(--throttle-handle-height, 1.4rem);
  --fader-cap: var(--throttle-handle, var(--muted));
  --scale-width: 2.6rem;
}
.throttle > .scale { padding-block-end: 1.6rem; font-size: 0.62rem; }
.throttle-lever {
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  justify-items: center;
  gap: 0.35rem;
  font: 700 0.7rem var(--font-mono);
  color: var(--muted);
}
.throttle-lever > .fader { min-block-size: 0; block-size: 100%; }

/* ---- .pfd: primary flight display layout ---------------------------------
 * <div class="pfd">
 *   <ul class="annunciator is-fma">…</ul>        (row of modes)
 *   <div class="tape">…</div>                     speed, left
 *   <div class="attitude">…</div>                 centre
 *   <div class="tape is-end">…</div>              altitude
 *   <div class="tape is-end">…</div>              (second is-end tape: V/S)
 *   <ol class="compass-strip">…</ol>              heading, below
 * </div>
 * Tapes stretch to the attitude's height. */
.pfd {
  container: pfd / inline-size;
  display: grid;
  grid-template-columns: auto minmax(0, var(--pfd-attitude-max, 22rem)) auto auto;
  grid-template-areas: "fma fma fma fma" "speed att alt vs" ". hdg hdg .";
  justify-content: center;
  gap: var(--pfd-gap, 0.5rem);
  padding: var(--pfd-pad, 0.6rem);
  background: var(--pfd-bg, var(--bg));
  border: 1px solid var(--pfd-border, var(--border));
  border-radius: var(--pfd-radius, var(--radius));
}
.pfd > .annunciator { grid-area: fma; }
.pfd > .attitude { grid-area: att; inline-size: 100%; }
.pfd > .tape { grid-area: speed; block-size: auto; }
.pfd > .tape.is-end { grid-area: alt; }
.pfd > .tape.is-end ~ .tape.is-end { grid-area: vs; --tape-width: 3rem; }
.pfd > .tape.is-end ~ .tape.is-end .tape-readout { inset-inline: 0; padding-inline: 0.25rem; font-size: min(0.7rem, 30cqi); text-align: center; }
.pfd > :is(.compass-strip, .compass) { grid-area: hdg; }
@container pfd (max-width: 26rem) {
  .pfd { gap: 0.25rem; padding: 0.3rem; }
  .pfd > .tape { --tape-width: 3.4rem; --tape-readout-size: 0.85rem; --tape-label-size: 0.65rem; }
  .pfd > .tape.is-end ~ .tape.is-end { --tape-width: 2.1rem; }
}

/* ---- .flight-hud: head-up display ----------------------------------------
 * <div class="flight-hud" role="img" aria-label="HUD: …">
 *   <div class="attitude is-hud" style="--pitch:3; --roll:-8">…</div>  scene + ladder
 *   <ol class="compass-strip" style="--heading:274">…</ol>             top
 *   <div class="tape">…</div>  <div class="tape is-end">…</div>       speed, altitude
 *   <span class="flight-hud-gun"></span>                              gun cross (waterline)
 *   <span class="flight-hud-fpm" style="--x:52%; --y:47%"></span>     flight-path marker
 *   <span class="flight-hud-target is-locked" style="--x:63%; --y:32%">TGT 4.2</span>
 *   <div class="flight-hud-data">…</div> <div class="flight-hud-data is-end">…</div>
 * </div>
 * Symbology wears --flight-hud-ink and a glow; the instruments inside are
 * made see-through. */
.flight-hud {
  --_ink: var(--flight-hud-ink, var(--accent-text, var(--accent)));
  container: flight-hud / inline-size;
  position: relative;
  isolation: isolate;
  overflow: hidden;
  display: grid;
  grid-template:
    "head head head" auto
    "speed . alt" minmax(var(--flight-hud-center-min, 15rem), 1fr)
    "data . data-end" auto / auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-s);
  min-inline-size: 0;
  min-block-size: var(--flight-hud-min-height, 26rem);
  padding: var(--flight-hud-pad, var(--space-m));
  color: var(--_ink);
  border: var(--flight-hud-border-width, 1px) solid var(--flight-hud-border, var(--border));
  border-radius: var(--flight-hud-radius, var(--radius));
  font-family: var(--flight-hud-font, var(--font-mono));
  font-size: 0.85rem;
  text-shadow: var(--flight-hud-glow, 0 0 0.35em color-mix(in srgb, currentColor 55%, transparent));
}
.flight-hud > .compass-strip {
  grid-area: head;
  justify-self: center;
  inline-size: min(30rem, 72%);
  --compass-strip-bg: transparent;
  --compass-strip-fg: var(--_ink);
  --compass-strip-border: transparent;
  --compass-tick: var(--_ink);
  --compass-tick-minor: var(--_ink);
  --compass-lubber: var(--_ink);
  --compass-north: var(--_ink);
}
.flight-hud > .tape {
  grid-area: speed;
  --tape-height: min(16rem, 52cqi);
  --tape-bg: transparent;
  --tape-fg: var(--_ink);
  --tape-tick: var(--_ink);
  --tape-border: transparent;
  --tape-readout-bg: color-mix(in srgb, var(--bg) 55%, transparent);
  --tape-readout-fg: var(--_ink);
  --tape-readout-border: var(--_ink);
  --tape-unit-fg: var(--_ink);
}
.flight-hud > .tape .tape-unit { background: none; }
.flight-hud > .tape.is-end { grid-area: alt; }
.flight-hud-data { grid-area: data; display: grid; gap: 0.15rem; line-height: 1.2; }
.flight-hud-data.is-end { grid-area: data-end; text-align: end; }
:is(.flight-hud-gun, .flight-hud-fpm, .flight-hud-target) { position: absolute; z-index: var(--z-raised); pointer-events: none; }
/* Gun cross: a fixed cross on the boresight. */
.flight-hud-gun {
  left: 50%;
  top: var(--flight-hud-boresight, 36%);
  translate: -50% -50%;
  inline-size: 2.4rem;
  aspect-ratio: 1;
  background:
    linear-gradient(var(--_ink), var(--_ink)) center / 100% 2px no-repeat,
    linear-gradient(var(--_ink), var(--_ink)) center / 2px 100% no-repeat;
  mask: radial-gradient(circle, transparent 0.35rem, #000 0.36rem);
  filter: drop-shadow(0 0 2px var(--_ink));
}
/* Flight-path marker: a ring with wings and a tail at --x/--y. */
.flight-hud-fpm {
  left: var(--x, 50%);
  top: var(--y, 50%);
  translate: -50% -50%;
  inline-size: 3.2rem;
  block-size: 1.6rem;
  background:
    radial-gradient(circle, transparent 0.42rem, var(--_ink) 0.44rem 0.56rem, transparent 0.58rem),
    linear-gradient(var(--_ink), var(--_ink)) left center / 0.95rem 2px no-repeat,
    linear-gradient(var(--_ink), var(--_ink)) right center / 0.95rem 2px no-repeat,
    linear-gradient(var(--_ink), var(--_ink)) center top / 2px 0.3rem no-repeat;
  filter: drop-shadow(0 0 2px var(--_ink));
}
/* Target designator box (.is-locked: a diamond inside). Its text is the label. */
.flight-hud-target {
  left: var(--x, 50%);
  top: var(--y, 40%);
  translate: -50% -1.2rem;
  padding-block-start: 2.8rem;
  font-size: 0.75rem;
  white-space: nowrap;
}
.flight-hud-target::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 0;
  translate: -50% 0;
  inline-size: 2.4rem;
  aspect-ratio: 1;
  border: 2px solid var(--_ink);
  box-shadow: 0 0 4px var(--_ink);
}
.flight-hud-target.is-locked::before {
  background: linear-gradient(var(--_ink), var(--_ink)) center / 0.4rem 0.4rem no-repeat;
  outline: 2px solid var(--_ink);
  outline-offset: -0.7rem;
}
@container flight-hud (max-width: 34rem) {
  .flight-hud { gap: var(--space-2xs); padding: var(--space-xs); font-size: 0.75rem; }
  .flight-hud > .compass-strip { inline-size: 92%; }
  .flight-hud > .attitude.is-hud { --attitude-pitch-scale: 2.6cqi; --attitude-rung: 40cqi; --attitude-label-room: 1.6rem; --attitude-label-size: 0.65rem; }
  .flight-hud > .tape { --tape-width: 3.6rem; --tape-readout-size: 0.85rem; --tape-label-size: 0.65rem; }
}

/* =========================================================================
 * ADDITIONS for cockpit-boat/-submarine/-spaceship.html (generic parts):
 *   .radar-line / .radar-vrm  heading line, electronic bearing line and
 *                  variable range marker on a .radar
 *   .ownship       own-vehicle silhouette in a dial, radar or map
 *   .dial-bug      rim pointer at --at degrees on any round instrument
 *   .annunciator-switch  lighted push-button (checkbox) caption cell;
 *                  .is-dual lights the off state as a warning
 *   .waterfall     sonar waterfall / spectrogram with drifting traces
 *   .orbit         orbit diagram: body at the focus, ellipses from --a/--e,
 *                  marks (apoapsis, periapsis, craft) at --at degrees
 * Same rules as above: tokens with base fallbacks, inline values, no JS.
 * ========================================================================= */

/* ---- .radar-line / .radar-vrm --------------------------------------------
 * <span class="radar-line" style="--bearing:0"></span>                 heading line
 * <span class="radar-line is-dashed" style="--bearing:48">EBL 048</span>
 * <span class="radar-vrm" style="--range:.5">VRM 1.5</span>
 * Children of .radar, from the same origin as the blips. */
.radar-line,
.radar-vrm {
  position: absolute;
  z-index: var(--z-raised);
  pointer-events: none;
  color: var(--radar-line, var(--radar-fg, var(--accent-text, var(--accent))));
  font-size: var(--radar-label-size, max(0.65rem, 3.4cqi));
  line-height: 1;
  white-space: nowrap;
}
.radar-line {
  left: 50%;
  top: calc(50cqi - var(--range, 1) * 50cqi);
  inline-size: 0;
  block-size: calc(var(--range, 1) * 50cqi);
  border-inline-start: 2px solid currentColor;
  translate: -1px 0;
  transform-origin: 1px 100%;
  rotate: calc(var(--bearing, 0) * 1deg);
}
.radar-line.is-dashed { border-inline-start-style: dashed; }
.radar-vrm {
  left: 50%;
  top: 50cqi;
  inline-size: calc(var(--range, 0.5) * 100cqi);
  aspect-ratio: 1;
  translate: -50% -50%;
  /* The label rides the ring at its top. */
  display: grid;
  align-content: start;
  justify-items: center;
  padding-block-start: 0.2em;
  text-shadow: 0 0 2px var(--bg), 0 0 2px var(--bg);
}

.radar-vrm::before { content: ""; position: absolute; inset: 0; border: 2px dashed currentColor; border-radius: 50%; }

/* ---- .ownship: own-vehicle silhouette ------------------------------------
 * <span class="ownship" style="--heading:0" aria-hidden="true"></span>
 * A hull outline (bow up) at --x/--y (default centre) of its positioned
 * parent, turned by --heading; .is-arrow draws a chevron instead. */
.ownship {
  position: absolute;
  z-index: calc(var(--z-raised) + 1);
  left: var(--x, 50%);
  top: var(--y, 50%);
  inline-size: var(--ownship-size, 14%);
  aspect-ratio: 1 / 2.4;
  translate: -50% -50%;
  rotate: calc(var(--heading, 0) * 1deg);
  pointer-events: none;
  /* The outline: a drop-shadow round the clipped hull below. */
  filter: drop-shadow(1.5px 0 0 var(--ownship-edge, var(--text))) drop-shadow(-1.5px 0 0 var(--ownship-edge, var(--text))) drop-shadow(0 1.5px 0 var(--ownship-edge, var(--text))) drop-shadow(0 -1.5px 0 var(--ownship-edge, var(--text)));
}
.ownship::before {
  content: "";
  position: absolute;
  inset: 0;
  background:
    linear-gradient(var(--ownship-edge, var(--text)), var(--ownship-edge, var(--text))) 50% 62% / 44% 18% no-repeat,
    var(--ownship-fill, var(--surface));
  clip-path: polygon(50% 0, 80% 22%, 92% 50%, 88% 100%, 12% 100%, 8% 50%, 20% 22%);
}
.ownship.is-arrow { aspect-ratio: 1; }
.ownship.is-arrow::before { background: var(--ownship-fill, var(--surface)); clip-path: polygon(50% 0, 100% 100%, 50% 72%, 0 100%); }

/* ---- .dial-bug: rim pointer ----------------------------------------------
 * <span class="dial-bug" style="--at:62" aria-hidden="true">T</span>
 * Inside .compass, .gauge-dial or .radar: a triangle on the rim at --at
 * degrees clockwise from up, its text printed in it. .is-hollow outlines
 * it (a second pointer that must differ by shape). */
.dial-bug {
  position: absolute;
  z-index: calc(var(--z-raised) + 1);
  inset: 0;
  rotate: calc(var(--at, 0) * 1deg);
  pointer-events: none;
  color: var(--dial-bug, var(--flare, var(--accent)));
  font: 700 max(0.6rem, 6cqi) / 1 var(--font-mono);
  text-align: center;
}
.dial-bug::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 1cqi;
  translate: -50% 0;
  inline-size: 11cqi;
  aspect-ratio: 1 / 1.1;
  background: currentColor;
  clip-path: polygon(0 0, 100% 0, 50% 100%);
}
.dial-bug.is-hollow::before {
  clip-path: polygon(0 0, 100% 0, 50% 100%, 50% 72%, 78% 16%, 22% 16%, 50% 72%, 50% 100%);
}
.dial-bug:not(:empty) { padding-block-start: 13cqi; text-shadow: 0 0 2px var(--bg); }

/* ---- .annunciator-switch: lighted push-button ----------------------------
 * <ul class="annunciator">
 *   <li><label class="annunciator-switch is-advisory is-dual"><input type="checkbox" checked>
 *     MBT 1 <span class="annunciator-on">— shut</span><span class="annunciator-off">○ open</span></label></li>
 * </ul>
 * Checked = lit in its state colour (default --accent); unchecked = dark,
 * or with .is-dual lit as a warning (a hull valve shown open). Optional
 * .annunciator-on / .annunciator-off spans print the state in words. */
.annunciator > li:has(> .annunciator-switch) { display: grid; place-items: stretch; padding: 0; border: 0; background: none; }
.annunciator-switch {
  --_c: var(--annunciator-on, var(--accent));
  position: relative;
  display: grid;
  place-items: center;
  align-content: center;
  min-block-size: max(var(--tap-min), var(--annunciator-cell-height, 2.6rem));
  padding: 0.3rem 0.45rem;
  background: var(--annunciator-off, var(--surface-2));
  color: var(--annunciator-off-fg, var(--muted));
  border: 1px solid var(--annunciator-cell-border, var(--hairline));
  border-radius: var(--annunciator-cell-radius, 2px);
  font-family: var(--annunciator-font, var(--font-mono));
  font-size: var(--annunciator-size, 0.7rem);
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: 0.05em;
  text-align: center;
  text-transform: uppercase;
  user-select: none;
}
.annunciator-switch.is-caution { --_c: var(--annunciator-caution, var(--warning)); }
.annunciator-switch.is-warning { --_c: var(--annunciator-warning, var(--danger)); }
.annunciator-switch.is-advisory { --_c: var(--annunciator-advisory, var(--success)); }
.annunciator-switch.is-dual:not(:has(input:checked)) { --_c: var(--annunciator-warning, var(--danger)); }
.annunciator-switch:is(:has(input:checked), .is-dual) {
  background: color-mix(in srgb, var(--_c) var(--annunciator-lit-mix, 12%), var(--annunciator-off, var(--surface-2)));
  color: var(--annunciator-lit-fg, var(--text));
  border-color: var(--_c);
  box-shadow: var(--annunciator-glow, inset 0 0 0.7rem color-mix(in srgb, var(--_c) 35%, transparent), 0 0 0.45rem color-mix(in srgb, var(--_c) 40%, transparent));
}
.annunciator-switch input { position: absolute; inline-size: 1px; block-size: 1px; margin: 0; opacity: 0; clip-path: inset(50%); }
.annunciator-switch:has(input:focus-visible) { outline: 2px solid var(--focus); outline-offset: 2px; }
.annunciator-switch:has(input:checked) .annunciator-off,
.annunciator-switch:not(:has(input:checked)) .annunciator-on { display: none; }
:is(.annunciator-on, .annunciator-off) { font-weight: 400; }
@media (forced-colors: active) {
  .annunciator-switch:is(:has(input:checked), .is-dual) { border: 3px solid Highlight; }
}

/* ---- .waterfall: sonar waterfall / spectrogram ---------------------------
 * <div class="waterfall" role="img" aria-label="…">
 *   <span class="waterfall-trace" style="--at:.32; --drift:-.04">S1</span>
 *   <span class="waterfall-trace is-faint" style="--at:.7">S3</span>
 * </div>
 * <ol class="waterfall-scale" aria-hidden="true"><li>000</li>…<li>350</li></ol>
 * Across = bearing (0–1 of the width), down = time (now at the top). A
 * trace starts at --at and ends --drift further across at the bottom;
 * its text labels it. The noise and traces roll down under motion. */
@keyframes vehicles-waterfall { to { background-position-y: var(--waterfall-roll, 48px), var(--waterfall-roll, 48px), 0; } }
.waterfall {
  --_fg: var(--waterfall-fg, var(--radar-fg, var(--accent)));
  position: relative;
  isolation: isolate;
  overflow: hidden;
  block-size: var(--waterfall-height, 14rem);
  min-inline-size: 0;
  background:
    repeating-linear-gradient(180deg, color-mix(in srgb, var(--_fg) 16%, transparent) 0 1px, transparent 1px 3px, color-mix(in srgb, var(--_fg) 8%, transparent) 3px 5px, transparent 5px 8px, color-mix(in srgb, var(--_fg) 22%, transparent) 8px 9px, transparent 9px 12px) 0 0 / 100% 48px,
    repeating-linear-gradient(90deg, transparent 0 5px, color-mix(in srgb, var(--_fg) 12%, transparent) 5px 6px, transparent 6px 13px, color-mix(in srgb, var(--_fg) 7%, transparent) 13px 15px, transparent 15px 22px) 0 0 / 100% 48px,
    var(--waterfall-bg, color-mix(in oklab, var(--_fg) 6%, var(--bg)));
  border: var(--waterfall-border-width, 1px) solid var(--waterfall-border, var(--border));
  border-radius: var(--waterfall-radius, var(--radius));
  font-family: var(--waterfall-font, var(--font-mono));
}
.waterfall-trace {
  --_t: var(--waterfall-trace-width, 3px);
  --_a: calc(var(--at, 0.5) * 100%);
  --_b: calc((var(--at, 0.5) + var(--drift, 0)) * 100%);
  position: absolute;
  inset: 0;
  padding: 0.4rem 0 0 calc(var(--at, 0.5) * 100% + 0.4rem);
  pointer-events: none;
  color: var(--waterfall-label-fg, var(--text));
  font-size: 0.7rem;
  line-height: 1;
  white-space: nowrap;
  text-shadow: 0 0 2px var(--bg), 0 0 2px var(--bg);
}
.waterfall-trace::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    repeating-linear-gradient(180deg, var(--waterfall-trace, var(--_fg)) 0 7px, color-mix(in srgb, var(--waterfall-trace, var(--_fg)) 45%, transparent) 7px 9px, var(--waterfall-trace, var(--_fg)) 9px 14px, transparent 14px 16px) 0 0 / 100% 48px;
  clip-path: polygon(calc(var(--_a) - var(--_t)) 0, calc(var(--_a) + var(--_t)) 0, calc(var(--_b) + var(--_t)) 100%, calc(var(--_b) - var(--_t)) 100%);
  filter: drop-shadow(0 0 3px var(--waterfall-trace, var(--_fg)));
}
.waterfall-trace.is-faint::before { opacity: 0.45; }
.waterfall-trace.is-foe { --waterfall-trace: var(--radar-foe, var(--danger)); }
.waterfall-trace.is-friend { --waterfall-trace: var(--radar-friend, var(--success)); }
.waterfall-trace.is-unknown { --waterfall-trace: var(--radar-unknown, var(--warning)); }
@media (prefers-reduced-motion: no-preference) {
  .waterfall { animation: vehicles-waterfall var(--waterfall-speed, 3s) linear infinite; }
  .waterfall-trace::before { animation: vehicles-waterfall-trace var(--waterfall-speed, 3s) linear infinite; }
}

  :root[data-motion="always"] .waterfall { animation: vehicles-waterfall var(--waterfall-speed, 3s) linear infinite; }
  :root[data-motion="always"] .waterfall-trace::before { animation: vehicles-waterfall-trace var(--waterfall-speed, 3s) linear infinite; }

@keyframes vehicles-waterfall-trace { to { background-position-y: var(--waterfall-roll, 48px); } }
.waterfall-scale {
  display: flex;
  justify-content: space-between;
  margin: 0.2rem 0 0;
  padding: 0;
  list-style: none;
  color: var(--muted);
  font: 0.7rem var(--font-mono);
}

/* ---- .orbit: orbit diagram -----------------------------------------------
 * <div class="orbit" role="img" aria-label="…">
 *   <div class="orbit-path" style="--a:.36; --e:.12; --tilt:-20">
 *     <span class="orbit-mark" style="--at:0">Pe 212</span>
 *     <span class="orbit-mark" style="--at:180">Ap 418</span>
 *     <span class="orbit-mark is-craft" style="--at:64">You</span>
 *   </div>
 *   <div class="orbit-path is-target" style="--a:.42; --e:.02">…</div>
 * </div>
 * The body (::before, --orbit-body of the width) sits at the centre, which
 * is the focus of every path. --a is the semi-major axis (0–0.5 of the
 * width), --e the eccentricity, --tilt turns the line of apsides
 * (degrees). Marks sit at --at degrees of eccentric anomaly from
 * periapsis (0 = periapsis, 180 = apoapsis) and stay upright. */
.orbit {
  --_fg: var(--orbit-fg, var(--accent-text, var(--accent)));
  container-type: inline-size;
  position: relative;
  isolation: isolate;
  overflow: hidden;
  inline-size: var(--orbit-size, 100%);
  max-inline-size: 100%;
  aspect-ratio: var(--orbit-ratio, 1);
  background:
    radial-gradient(circle at 20% 30%, color-mix(in srgb, var(--text) 50%, transparent) 0 1px, transparent 1.5px) 0 0 / 23% 31%,
    radial-gradient(circle at 70% 60%, color-mix(in srgb, var(--text) 35%, transparent) 0 1px, transparent 1.5px) 0 0 / 17% 19%,
    var(--orbit-bg, color-mix(in oklab, var(--_fg) 5%, var(--bg)));
  border: var(--orbit-border-width, 1px) solid var(--orbit-border, var(--border));
  border-radius: var(--orbit-radius, var(--radius));
  font-family: var(--orbit-font, var(--font-mono));
}
.orbit::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  inline-size: calc(var(--orbit-body, 0.22) * 100cqi);
  aspect-ratio: 1;
  translate: -50% -50%;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, var(--orbit-body-light, color-mix(in oklab, var(--accent) 55%, var(--text))), var(--orbit-body-fill, color-mix(in oklab, var(--accent) 45%, var(--bg))) 60%, var(--orbit-body-dark, var(--bg)));
  box-shadow: 0 0 0 0.25rem color-mix(in srgb, var(--orbit-atmosphere, var(--accent)) 25%, transparent);
}
.orbit-path {
  --_a: calc(var(--a, 0.35) * 100cqi);
  --_b: calc(var(--a, 0.35) * sqrt(1 - var(--e, 0) * var(--e, 0)) * 100cqi);
  --_c: calc(var(--a, 0.35) * var(--e, 0) * 100cqi);
  position: absolute;
  left: calc(50% - var(--_a) - var(--_c));
  top: calc(50% - var(--_b));
  inline-size: calc(2 * var(--_a));
  block-size: calc(2 * var(--_b));
  transform-origin: calc(var(--_a) + var(--_c)) 50%;
  rotate: calc(var(--tilt, 0) * 1deg);
  border: var(--orbit-line-width, 2px) solid var(--orbit-line, var(--_fg));
  border-radius: 50%;
  color: var(--orbit-label-fg, var(--text));
  font-size: max(0.65rem, 3.2cqi);
  line-height: 1;
}
.orbit-path.is-target { border-style: dashed; --orbit-line: var(--orbit-target, var(--success)); }
.orbit-path.is-planned { border-style: dotted; --orbit-line: var(--orbit-planned, var(--warning)); }
/* data-series="1..6" colours a path to match a .legend. */
.orbit-path[data-series] { --orbit-line: var(--series); }
.orbit-mark {
  position: absolute;
  left: calc(50% + cos(var(--at, 0) * 1deg) * 50%);
  top: calc(50% - sin(var(--at, 0) * 1deg) * 50%);
  rotate: calc(var(--tilt, 0) * -1deg);
  display: block;
  inline-size: 0;
  block-size: 0;
  text-indent: 0.7rem;
  white-space: nowrap;
  text-shadow: 0 0 2px var(--bg), 0 0 2px var(--bg);
}
.orbit-mark::before {
  content: "";
  position: absolute;
  inline-size: 0.6rem;
  aspect-ratio: 1;
  left: 0;
  top: 0;
  translate: -50% -50%;
  background: var(--orbit-mark, var(--text));
  rotate: 45deg;
}
/* The text, beside the marker, centred on the point's height. */
.orbit-mark { line-height: 0; }
.orbit-mark.is-craft::before { inline-size: 0.9rem; rotate: 0deg; border-radius: 50%; background: var(--orbit-craft, var(--flare, var(--warning))); box-shadow: 0 0 0.4rem var(--orbit-craft, var(--flare, var(--warning))); }
/* .is-start: the label on the start side of the marker (near the right edge). */
.orbit-mark.is-start { direction: rtl; }
.orbit-mark.is-node::before { background: none; border: 2px solid var(--orbit-mark, var(--text)); }
/* ftl-themes: the app shell.
 *
 * A theme is not only a palette — LCARS and a telemetry HUD are different
 * LAYOUTS, and switching theme has to move the furniture, not just recolor
 * it. This file defines ONE shell markup contract that an app adopts once:
 *
 *   <body class="app">
 *     <header class="app-bar">brand, nav, actions</header>
 *     <aside   class="app-rail" aria-hidden="true"></aside>
 *     <main    class="app-main">…the app…</main>
 *     <footer  class="app-status">status readouts</footer>
 *   </body>
 *
 * Every theme then re-arranges that same markup through the --app-*
 * properties below: a theme can move the bar, open or close the rail,
 * change the elbow radii, and inset the content. The app never learns a
 * theme's name, and no theme needs app-specific markup.
 *
 * The rail is DECORATION, generated entirely in CSS (see each theme's
 * layout block) and `aria-hidden` in the markup above, so a theme that
 * wants a candy-bar sidebar gets one without the app shipping an empty
 * element's worth of content. Themes with no rail collapse it to nothing.
 *
 * Adopting the shell is optional: an app that keeps its own layout still
 * gets every component and token. It just won't re-lay-out per theme.
 */

.app {
  display: grid;
  grid-template-areas: var(--app-areas, "bar bar" "rail main" "status status");
  grid-template-columns: var(--app-columns, 0 minmax(0, 1fr));
  grid-template-rows: var(--app-rows, auto minmax(0, 1fr) auto);
  gap: var(--app-gap, 0);
  min-height: 100vh;
  /* dvh tracks the actually-visible viewport on mobile browsers, where vh
     is the largest possible one and hides content behind the toolbar. */
  min-height: 100dvh;
  padding: var(--app-padding, 0);
  background: var(--app-bg, transparent);
}

/* An app that never adds the rail element at all (not even an empty one)
   has no box for :empty to match, so a theme's --app-columns would
   still reserve a blank gutter next to .app-main. Collapse the column
   track itself in that case. Browsers without :has() just keep the blank
   gutter — a graceful degrade, not a break. */
.app:not(:has(> .app-rail)) {
  grid-template-areas: var(--app-areas-no-rail, "bar" "main" "status");
  grid-template-columns: var(--app-columns-no-rail, minmax(0, 1fr));
}

.app-bar {
  grid-area: bar;
  display: flex;
  /* A bar that doesn't fit wraps its last items onto a second line rather
     than overflowing the screen. */
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem;
  min-width: 0;
  padding: var(--app-bar-padding, 0.6rem 1rem);
  background: var(--app-bar-bg, var(--nav-bg, var(--surface)));
  color: var(--app-bar-fg, inherit);
  border-bottom: var(--app-bar-rule-width, 1px) solid var(--app-bar-rule, var(--border));
  border-radius: var(--app-bar-radius, 0);
  min-height: var(--app-bar-height, auto);
  font-family: var(--app-bar-font, inherit);
}
/* A nav inside the bar sits on the bar: it does not paint --nav-bg (tuned
   for a free-standing nav) as a second box inside it. Nav text tokens are
   already checked against the bar background, not --nav-bg, so this is
   what the contrast lint assumes. A theme that does want a distinct strip
   inside its bar sets --app-bar-nav-bg. */
.app-bar .nav {
  background: var(--app-bar-nav-bg, transparent);
  border-bottom-width: var(--app-bar-nav-rule-width, 0);
  border-radius: var(--app-bar-nav-radius, 0);
}

/* Decorative only. Themes that use it set --app-rail-display and paint
   it with a gradient/pseudo-elements; the default is no rail at all. */
/* An empty rail is always hidden: decorative rails are painted by themes
   that opt in, but no theme may leave a stray gutter for apps that ship an
   empty slot (the L0 graceful-degradation rule). A theme that paints its
   rail purely decoratively opts back in with --app-rail-empty-display. */
.app-rail:empty {
  display: var(--app-rail-empty-display, none);
}
.app-rail {
  grid-area: rail;
  display: var(--app-rail-display, none);
  flex-direction: column;
  gap: var(--app-rail-gap, 0.4rem);
  padding: var(--app-rail-padding, 0);
  background: var(--app-rail-bg, transparent);
  border-radius: var(--app-rail-radius, 0);
  pointer-events: none;
}

.app-main {
  grid-area: main;
  min-width: 0;
  min-height: 0;
  /* The page scrolls, not main: the shell has a minimum height, not a
     fixed one. v4's overflow:auto turned main into an inner scroller the
     moment anything (an open menu near the bottom) stuck out of it, so the
     wheel scrolled main a little and then the page. clip keeps sideways
     spill hidden without making a scroll container, and sticky elements
     inside main now stick to the screen. */
  overflow-x: clip;
  overflow-y: visible;
  padding: var(--app-main-padding, 1rem);
  background: var(--app-main-bg, transparent);
  border-radius: var(--app-main-radius, 0);
}

.app-status {
  grid-area: status;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  min-width: 0;
  padding: var(--app-status-padding, 0.4rem 1rem);
  background: var(--app-status-bg, var(--surface));
  color: var(--app-status-fg, var(--muted));
  border-top: 1px solid var(--app-status-rule, var(--hairline));
  border-radius: var(--app-status-radius, 0);
  font-size: 0.82em;
}

/* --- v5 tiers (PLAN.md §2) ------------------------------------------------
 * The only place tier widths appear. Custom properties can't be used in
 * @media, so these four numbers are written once here; themes set per-tier
 * tokens instead of breakpoints. --tier is also readable by components
 * through container style queries: @container style(--tier: mobile).
 *
 *   mobile   250–480px   touch     single column, bottom status
 *   tablet   481–900px   touch     single column, roomier
 *   desktop  901–1800px  mouse     the full shell (rail, bar, status)
 *   xl       1801px+     mouse     desktop shell capped at --app-max, centred,
 *                                  theme art in the side gutters
 *
 * The v4 `-sm` tokens (--app-areas-sm, --app-rows-sm, --app-rail-display-sm,
 * --app-main-padding-sm) still work: they are the fallback for both the
 * mobile and tablet tokens, so a v4 theme keeps its small-screen layout.
 */
:root { --tier: desktop; }

@media (max-width: 900px) {
  .app {
    grid-template-columns: minmax(0, 1fr);
  }
  /* Bar on touch tiers: brand and actions share the first line; the nav
     takes its own full-width line and scrolls sideways in one row rather
     than wrapping into a stack of rows. */
  .app-bar { flex-wrap: wrap; row-gap: var(--space-2xs); }
  .app-bar > .nav {
    order: 10;
    flex: 1 0 100%;
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
  }
  .app-bar > .nav > * { flex-shrink: 0; }
  /* The brand gives way first: it truncates so the app's actions keep
     their place on the first line. */
  .app-bar > .nav-brand {
    flex: 1 1 0;  /* zero basis: shrink on the first line rather than wrap */
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  /* A free-standing nav bar (no app shell) scrolls sideways the same way. */
  .nav { overflow-x: auto; scrollbar-width: none; }
  .nav > * { flex-shrink: 0; }
  /* A nav-collapse in the bar keeps its own toggle behaviour instead. */
}
@media (max-width: 480px) {
  :root { --tier: mobile; }
  .app {
    grid-template-areas: var(--app-areas-mobile, var(--app-areas-sm, "bar" "main" "status"));
    grid-template-columns: var(--app-columns-mobile, minmax(0, 1fr));
    grid-template-rows: var(--app-rows-mobile, var(--app-rows-sm, auto minmax(0, 1fr) auto));
  }
  .app-rail, .app-rail:empty { display: var(--app-rail-display-mobile, var(--app-rail-display-sm, none)); }
  .app-main { padding: var(--app-main-padding-mobile, var(--app-main-padding-sm, var(--space-s))); }
}
@media (min-width: 481px) and (max-width: 900px) {
  :root { --tier: tablet; }
  .app {
    grid-template-areas: var(--app-areas-tablet, var(--app-areas-sm, "bar" "main" "status"));
    grid-template-columns: var(--app-columns-tablet, minmax(0, 1fr));
    grid-template-rows: var(--app-rows-tablet, var(--app-rows-sm, auto minmax(0, 1fr) auto));
  }
  .app-rail, .app-rail:empty { display: var(--app-rail-display-tablet, var(--app-rail-display-sm, none)); }
  .app-main { padding: var(--app-main-padding-tablet, var(--app-main-padding-sm, var(--space-m))); }
}

/* XL: the shell stops growing at --app-max and centres. The side gutters
   show the theme's --app-gutter-art on a fixed layer masked to the gutters
   only, so it is never behind content and the theme's own page background
   is untouched. The art fades in from the content edge outwards. */
@media (min-width: 1801px) {
  :root { --tier: xl; }
  .app {
    max-inline-size: var(--app-max, 1800px);
    margin-inline: auto;
  }
  html::before {
    content: "";
    position: fixed;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    background: var(--app-gutter-art, none);
    --gutter: calc((100vw - var(--app-max, 1800px)) / 2);
    mask-image: linear-gradient(90deg,
      #000 0, #000 calc(var(--gutter) - var(--app-gutter-fade, 8rem)),
      transparent var(--gutter),
      transparent calc(100vw - var(--gutter)),
      #000 calc(100vw - var(--gutter) + var(--app-gutter-fade, 8rem)), #000 100%);
  }
}

/* Foldable enhancement: segment geometry is only applied to installed PWAs.
   Unsupported browsers and ordinary windows retain the Mobile/Tablet shell.
   The hinge remains an actual grid gap, so controls and panels cannot paint
   across it. Themes may tune these layouts with --app-fold-* properties. */
@media (display-mode: standalone) and (horizontal-viewport-segments: 2) {
  :root { --folding-mode: book; }
  .app {
    grid-template-areas: var(--app-fold-book-areas, "bar main");
    grid-template-columns: env(viewport-segment-width 0 0) env(viewport-segment-width 1 0);
    grid-template-rows: minmax(0, 1fr);
    column-gap: max(0px, calc(env(viewport-segment-left 1 0) - env(viewport-segment-right 0 0)));
  }
  .app-rail { display: none !important; }
  .app-status { position: fixed; inset-inline-start: env(viewport-segment-left 1 0); inset-block-end: 0; inline-size: env(viewport-segment-width 1 0); }
  .app-main { padding-block-end: var(--app-fold-status-space, 3rem); }
  .app:not(:has(> .app-rail)) {
    grid-template-areas: var(--app-fold-book-areas, "bar main");
    grid-template-columns: env(viewport-segment-width 0 0) env(viewport-segment-width 1 0);
  }
}
@media (display-mode: standalone) and (vertical-viewport-segments: 2) {
  :root { --folding-mode: tabletop; }
  .app {
    grid-template-areas: var(--app-fold-tabletop-areas, "bar" "main");
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: env(viewport-segment-height 0 0) env(viewport-segment-height 0 1);
    row-gap: max(0px, calc(env(viewport-segment-top 0 1) - env(viewport-segment-bottom 0 0)));
  }
  .app-rail { display: none !important; }
  .app-status { position: fixed; inset-inline-start: 0; inset-block-end: 0; inline-size: 100%; }
  .app-main { padding-block-end: var(--app-fold-status-space, 3rem); }
  .app:not(:has(> .app-rail)) {
    grid-template-areas: var(--app-fold-tabletop-areas, "bar" "main");
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: env(viewport-segment-height 0 0) env(viewport-segment-height 0 1);
  }
}
/* Optional theme cursor artwork. Every URL has a native CSS fallback. */
:root {
  --cursor-default: default;
  --cursor-text: text;
  --cursor-progress: progress;
  --cursor-grab: grab;
  --cursor-grabbing: grabbing;
  --cursor-not-allowed: not-allowed;
  --cursor-resize-x: col-resize;
  --cursor-resize-y: row-resize;
  --cursor-resize-diagonal: nwse-resize;
  --cursor-resize-diagonal-alt: nesw-resize;
  --cursor-help: help;
}
:where(.is-loading) { cursor: var(--cursor-progress, progress); }
:disabled, [aria-disabled="true"] { cursor: var(--cursor-not-allowed); }
.table td.is-editable, .tag-input-field, .textarea { cursor: var(--cursor-text); }
.table th[draggable="true"], .kanban-card[draggable="true"], .modal[data-drag] > .modal-header { cursor: var(--cursor-grab); }
.table th[draggable="true"]:active, .kanban-card[draggable="true"]:active, .modal[data-drag].is-dragging > .modal-header { cursor: var(--cursor-grabbing); }
.table th.is-resizable { cursor: var(--cursor-resize-x); }
html[data-contrast="high"] { --cursor-default: default !important; --cursor-text: text !important; --cursor-progress: progress !important; --cursor-grab: grab !important; --cursor-grabbing: grabbing !important; --cursor-not-allowed: not-allowed !important; --cursor-resize-x: col-resize !important; --cursor-resize-y: row-resize !important; --cursor-resize-diagonal: nwse-resize !important; --cursor-resize-diagonal-alt: nesw-resize !important; --cursor-help: help !important; }
@media (forced-colors: active) {
  html[data-theme] { --cursor-default: default !important; --cursor-text: text !important; --cursor-progress: progress !important; --cursor-grab: grab !important; --cursor-grabbing: grabbing !important; --cursor-not-allowed: not-allowed !important; --cursor-resize-x: col-resize !important; --cursor-resize-y: row-resize !important; --cursor-resize-diagonal: nwse-resize !important; --cursor-resize-diagonal-alt: nesw-resize !important; --cursor-help: help !important; }
}
/* theme source: blue-future */
/* ftl-themes: Blue Future
 * Theme-Name: Blue Future
 * Description: Deep-space navy HUD with cyan neon glow — the catalog's baseline sci-fi console look.
 *
 * Restored from the original CuTePi default ("Future SciFi") spec; palette
 * is the reconciled telemetry-dashboard set below.
 *
 * Component look is set through the --<component>-* properties at root
 * scope (see CONTRACT.md § "How a theme overrides a component"): declaring
 * background/color on a base component selector instead would outrank
 * core's variant rules and erase them.
 */

html[data-theme="blue-future"] {
  --motion-leave: warp;
  color-scheme: dark;
  --meter-style: vfd; /* metering.css: every meter drawn as a vacuum-fluorescent display */
  /* Palette reconciled against the reference implementation this theme is
     named for — a rack recorder's telemetry dashboard. These are that
     device's actual values, not an approximation of them. */
  --bg: #020509;
  --surface: #0a1526;
  --surface-2: #08192b;
  --border: #0f3a5c;
  --hairline: #0a2942;
  --text: #cfeeff;
  --muted: #6f9cba;
  --accent: #00d9ff;
  --accent-2: #4d7dff;
  --danger: #ff3355;
  --danger-text: #ff5577; /* danger as text on tinted fills: 4.9:1 */
  --success: #2bffb0;
  --warning: #ff8c1a;
  /* Spectrogram scale (metering.css): cyan signal, green low, yellow
     medium, amber high, red peak. */
  --spectro-signal: #22d3ee;
  --spectro-low: #2bd96b;
  --spectro-mid: #f5e04a;
  --spectro-high: #ff9f1a;
  --spectro-peak: #ff3355;
  --on-accent: #001018;
  --on-danger: #1a0007;
  --on-success: #001a10;
  --radius: 0.25rem;
  --font: Consolas, "IBM Plex Mono", "SFMono-Regular", monospace;
  --font-mono: Consolas, "IBM Plex Mono", monospace;
  --flare: #d85cff;
  --density: 1;

  /* Deck metalwork: the reference values, declared so other themes can
     diverge from a documented baseline rather than guessing it. */
  --deck-face: #112842;
  --deck-trim: #1d5c8f;
  --deck-hub: #11304a;
  --deck-well: #0d1c31;
  --deck-spoke: #16345c;
  --deck-tape: #14507e;
  --deck-shadow: #04121f;

  --focus: var(--accent);
  --focus-ring: 0 0 8px rgba(0, 217, 255, 0.35);
  --link: var(--accent);

  /* HUD corner brackets: 14px L-shapes, 2px stroke, 0.55 opacity, top-left
     and bottom-right. Painted as background layers rather than positioned
     ::before/::after: the surface needs no `position` of its own, so popovers,
     dropdowns and context menus inside it keep anchoring to their real
     offset parent, and the marks stay put when a .modal scrolls. */
  --bracket: rgba(0, 217, 255, 0.55);
  --brackets:
    linear-gradient(var(--bracket), var(--bracket)) top left / 14px 2px no-repeat,
    linear-gradient(var(--bracket), var(--bracket)) top left / 2px 14px no-repeat,
    linear-gradient(var(--bracket), var(--bracket)) bottom right / 14px 2px no-repeat,
    linear-gradient(var(--bracket), var(--bracket)) bottom right / 2px 14px no-repeat;
  --panel-bg: var(--brackets), var(--surface);
  --modal-bg: var(--brackets), var(--surface);
  --panel-shadow: 0 0 20px rgba(0, 180, 255, 0.08), inset 0 0 30px rgba(0, 180, 255, 0.03);
  --panel-radius: 0.25rem;

  /* Buttons: dark cell, tracked label, glow on hover. */
  /* Buttons are tinted: text and border in the button's colour over a 25%
     fill of it (40% on hover, with a glow). Plain buttons use --text; the
     variants re-point --bf-tone below. */
  --btn-fg: var(--text);
  --btn-border: var(--text);
  --btn-bg: color-mix(in srgb, var(--text) 25%, transparent);
  --btn-bg-hover: color-mix(in srgb, var(--text) 40%, transparent);
  --btn-border-hover: var(--text);
  --btn-tracking: 0.05em;
  --btn-shadow-hover: 0 0 0.6rem color-mix(in srgb, var(--text) 45%, transparent);

  /* Inputs */
  --input-bg: #08192b;
  --input-border: var(--border);

  /* Tables: dim tracked headers, hairline rules, accent left select bar. */
  --table-head-fg: var(--muted);
  --table-head-tracking: 0.08em;
  --table-head-rule: var(--hairline);
  --table-rule: var(--hairline);
  --row-hover-bg: rgba(0, 217, 255, 0.05);
  --row-selected-bg: rgba(0, 217, 255, 0.1);
  --row-active-marker: inset 0.35rem 0 0 var(--accent);

  /* Glow range: 4px gradient track, near-square thumb. */
  --slider-height: 4px;
  --slider-radius: 2px;
  --slider-border-width: 0;
  --slider-bg: linear-gradient(90deg, #0e3a5c, var(--accent));
  --slider-thumb-size: 14px;
  --slider-thumb-radius: 2px;
  --slider-thumb-bg: #02050a;
  --slider-thumb-shadow: 0 0 8px rgba(0, 217, 255, 0.5);

  --overlay-bg: rgba(2, 6, 10, 0.75);
  --label-tracking: 0.08em;
  --label-transform: uppercase;
  --indicator-fg: var(--accent);

  /* Instrument primitives: a recorder's meters and counters are the
     content, so they get the brightest colors while the chrome stays dim. */
  --meter-track: #020509;
  --meter-border: #0f3a5c;
  --meter-radius: 2px;
  --meter-low: #0aff9d;
  --meter-mid: #ffe400;
  --meter-high: #ff2a2a;
  --meter-peak-fg: #ffffff;
  --readout-fg: var(--accent);
  --readout-glow: 0 0 10px rgba(0, 217, 255, 0.55);
  --readout-tracking: 0.06em;
  --transport-bg: var(--brackets), #0a1526;
  --transport-border: #0f3a5c;
  --transport-radius: 0.25rem;
  --transport-shadow: 0 0 20px rgba(0, 180, 255, 0.08), inset 0 0 30px rgba(0, 180, 255, 0.03);
  --go-shadow-hover: 0 0 0 3px rgba(0, 217, 255, 0.3), 0 0 22px 4px rgba(0, 217, 255, 0.5);
  --go-bg-hover: rgba(0, 217, 255, 0.1);
  --lamp-radius: 50%;
  --lamp-off: #0d2b4a;
  --lamp-on: var(--accent);
  --lamp-glow: 0 0 0.5rem rgba(0, 217, 255, 0.7);
}

/* Backdrop: a fixed radial vignette plus a faint 2rem cyan grid, so the
   panel sits in a space instead of on flat color. */
html[data-theme="blue-future"] {
  background-color: var(--bg);
  background-attachment: fixed;
  background-image:
    linear-gradient(rgba(47, 214, 255, 0.04) 1px, transparent 1px),
    linear-gradient(90deg, rgba(47, 214, 255, 0.04) 1px, transparent 1px),
    radial-gradient(ellipse at top, #0a1a2e, #020509 70%);
  background-size: 2rem 2rem, 2rem 2rem, auto;
}

/* Headings are labels on a panel: small, tracked, dim. Content is bright. */
html[data-theme="blue-future"] h1,
html[data-theme="blue-future"] h2,
html[data-theme="blue-future"] h3,
html[data-theme="blue-future"] h4 {
  font-size: 0.8em;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--muted);
}

/* Buttons */
/* Button hierarchy: primary is a solid 80% accent with black text and a
   larger label; secondary is the tinted style (accent text and border over
   a 25% fill); tertiary is the accent outline with no fill until hover.
   Danger and success are tinted in their own colours. */
html[data-theme="blue-future"] :is(.btn-secondary, .btn-tertiary, .btn-danger, .btn-success) {
  --btn-fg: var(--bf-ink, var(--bf-tone));
  --btn-border: var(--bf-tone);
  --btn-bg: color-mix(in srgb, var(--bf-tone) 25%, transparent);
  --btn-bg-hover: color-mix(in srgb, var(--bf-tone) 40%, transparent);
  --btn-border-hover: var(--bf-tone);
  --btn-shadow-hover: 0 0 0.7rem color-mix(in srgb, var(--bf-tone) 55%, transparent);
}
html[data-theme="blue-future"] .btn-primary {
  --btn-bg: color-mix(in srgb, var(--accent) 80%, transparent);
  --btn-bg-hover: var(--accent);
  --btn-fg: #000;
  --btn-border: var(--accent);
  --btn-border-hover: var(--accent);
  --btn-font-size: 1.1em;
  --btn-shadow: 0 0 0.7rem rgba(47, 214, 255, 0.3);
  --btn-shadow-hover: 0 0 1rem rgba(0, 217, 255, 0.55);
}
html[data-theme="blue-future"] .btn-secondary { --bf-tone: var(--accent); }
html[data-theme="blue-future"] .btn-tertiary { --bf-tone: var(--accent); --btn-bg: transparent; }
/* Red text is lifted a little so it holds 4.5:1 on tinted bars too. */
html[data-theme="blue-future"] .btn-danger { --bf-tone: var(--danger); --bf-ink: #ff7a8f; }
html[data-theme="blue-future"] .btn-success { --bf-tone: var(--success); }
html[data-theme="blue-future"] .btn-ghost { --btn-shadow-hover: none; }
html[data-theme="blue-future"] .btn:disabled,
html[data-theme="blue-future"] .btn[aria-disabled="true"] {
  opacity: 0.35;
  box-shadow: none;
}

/* Table select bar: selected rows carry it too, not just the active row. */
html[data-theme="blue-future"] .table tr.is-selected td:first-child {
  box-shadow: inset 0.35rem 0 0 var(--accent);
}

/* Slider thumb: dark cap, glow border. Core draws a 2px --bg ring here. */
html[data-theme="blue-future"] .slider::-webkit-slider-thumb { border: 1px solid var(--accent); }
html[data-theme="blue-future"] .slider::-moz-range-thumb { border: 1px solid var(--accent); }

/* Status helpers: ok (success), err (danger), rec (bold danger + glow),
   idle (success: a ready recorder is green). */
html[data-theme="blue-future"] .status-idle { --state-fg: var(--success-text, var(--success)); }
html[data-theme="blue-future"] .status-rec,
html[data-theme="blue-future"] .status-error.is-rec {
  --state-fg: var(--danger-text, var(--danger));
  color: var(--state-fg);
  font-weight: 700;
  text-shadow: 0 0 8px rgba(255, 51, 85, 0.6);
}

/* .sci-switch: the square HUD rail switch. Theme-scoped extra class; other
   themes never see it. Markup:
   <label class="sci-switch" data-on="ONLINE" data-off="OFFLINE">
     <input type="checkbox"><span></span></label>
   The empty span is optional; the thumb and readout are pseudo-elements. */
html[data-theme="blue-future"] .sci-switch {
  --sci-led-off: rgba(255, 255, 255, 0.35);
  position: relative;
  display: inline-block;
  vertical-align: middle;
  width: 4.4em;
  height: 1.9em;
  background: #02050a;
  border: 1px solid var(--border);
  border-radius: 3px;
  font-family: var(--font-mono);
}
html[data-theme="blue-future"] .sci-switch > input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}
/* thumb + status LED */
html[data-theme="blue-future"] .sci-switch::before {
  content: "";
  position: absolute;
  top: 0.15em;
  left: 0.15em;
  width: 1.5em;
  height: 1.5em;
  box-sizing: border-box;
  border: 1px solid var(--border);
  border-radius: 2px;
  background:
    radial-gradient(circle, var(--sci-led-off) 0 0.17em, transparent 0.19em),
    #0d2035;
}
/* readout: dim and recessed when off */
html[data-theme="blue-future"] .sci-switch::after {
  content: attr(data-off);
  position: absolute;
  top: 50%;
  right: 0.5em;
  transform: translateY(-50%);
  font-size: 0.55em;
  letter-spacing: 0.05em;
  color: var(--muted);
  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.8);
  pointer-events: none;
}
html[data-theme="blue-future"] .sci-switch:has(> input:checked),
html[data-theme="blue-future"] .sci-switch.is-on {
  border-color: var(--accent);
  box-shadow: 0 0 8px rgba(0, 217, 255, 0.35), inset 0 0 8px rgba(0, 217, 255, 0.12);
}
html[data-theme="blue-future"] .sci-switch:has(> input:checked)::before,
html[data-theme="blue-future"] .sci-switch.is-on::before {
  left: calc(100% - 1.65em);
  border-color: var(--accent);
  box-shadow: 0 0 8px rgba(0, 217, 255, 0.5);
  background:
    radial-gradient(circle, var(--accent) 0 0.17em, rgba(0, 217, 255, 0.35) 0.2em, transparent 0.7em),
    #0d2035;
}
html[data-theme="blue-future"] .sci-switch:has(> input:checked)::after,
html[data-theme="blue-future"] .sci-switch.is-on::after {
  content: attr(data-on);
  right: auto;
  left: 0.5em;
  color: var(--accent);
  text-shadow: 0 0 8px rgba(0, 217, 255, 0.7);
}
html[data-theme="blue-future"] .sci-switch:has(> input:focus-visible) {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}
html[data-theme="blue-future"] .sci-switch:has(> input:disabled) {
  opacity: 0.35;
  cursor: not-allowed;
}
@media (prefers-reduced-motion: no-preference) {
  html[data-theme="blue-future"] .sci-switch,
  html[data-theme="blue-future"] .sci-switch::before,
  html[data-theme="blue-future"] .sci-switch::after {
    transition: left 0.15s ease, right 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease, color 0.15s ease;
  }
}

  html[data-motion="always"][data-theme="blue-future"] .sci-switch,html[data-motion="always"][data-theme="blue-future"] .sci-switch::before,html[data-motion="always"][data-theme="blue-future"] .sci-switch::after {
    transition: left 0.15s ease, right 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease, color 0.15s ease;
  }


/* --- Layout ------------------------------------------------------------ *
 * A flat instrument panel: full-bleed dark bar across the top, NO rail, and
 * a status strip along the bottom for telemetry. Content runs edge to edge
 * because screen area belongs to data, not to chrome. The rail is switched
 * off and its grid track removed so an app that ships the empty <aside>
 * per the shell contract leaves no gutter. */
html[data-theme="blue-future"] {
  --app-padding: 0;
  --app-gap: 0;
  --app-rail-display: none;
  --app-columns: minmax(0, 1fr);
  --app-areas: "bar" "main" "status";
  --app-bar-bg: var(--surface);
  --app-bar-rule: var(--border);
  --app-bar-height: 3rem;
  --app-main-padding: 1.2rem;
  --app-status-bg: rgba(3, 8, 15, 0.94);
  --app-status-rule: var(--border);
}

/* XL gutters (v5, PLAN.md §3): deep space beside the console. A sparse
   starfield and the telemetry grid in --hairline, both within ~1.3:1 of
   --bg so the eye stays on the content. The grid drifts one cell every two
   minutes, only with motion allowed. */
html[data-theme="blue-future"] {
  --app-gutter-art:
    radial-gradient(1px 1px at 22px 34px, #24506f 50%, transparent 55%) 0 0 / 140px 160px,
    radial-gradient(1px 1px at 96px 118px, #1a4260 50%, transparent 55%) 0 0 / 140px 160px,
    radial-gradient(1.5px 1.5px at 61px 77px, #2d6286 50%, transparent 55%) 0 0 / 230px 210px,
    linear-gradient(var(--hairline) 1px, transparent 1px) 0 0 / 100% 96px,
    linear-gradient(90deg, var(--hairline) 1px, transparent 1px) 0 0 / 96px 100%;
}
@media (min-width: 1801px) and (prefers-reduced-motion: no-preference) {
  html[data-theme="blue-future"]:not([data-motion="reduced"])::before {
    animation: blue-future-drift 120s linear infinite;
  }
}
@media (min-width: 1801px) {
  html[data-motion="always"][data-theme="blue-future"]:not([data-motion="reduced"])::before {
    animation: blue-future-drift 120s linear infinite;
  }
}
@keyframes blue-future-drift {
  to { background-position: 0 160px, 0 160px, 0 210px, 0 96px, 96px 0; }
}

/* Print: the grid, vignette, glows and brackets are screen-only. Core
   already drops panel/modal/transport backgrounds and box-shadows. */
@media print {
  html[data-theme="blue-future"] {
    background-image: none;
  }
  html[data-theme="blue-future"] .sci-switch,
  html[data-theme="blue-future"] .sci-switch::before,
  html[data-theme="blue-future"] .sci-switch::after,
  html[data-theme="blue-future"] .status-rec {
    box-shadow: none !important;
    text-shadow: none !important;
  }
}

}
