/* base.css — shared components for every Paul project.
 *
 * Zero colour literals (enforced by design_check.py). Every value comes from
 * tokens.css. Lifted from nexus/web/app.css and
 * shapescan/ui/static/draw_pattern.html, behaviour preserved.
 *
 * Load AFTER tokens.css:
 *   <link rel="stylesheet" href="/static/tokens.css">
 *   <link rel="stylesheet" href="/static/base.css">
 */

/* ── Reset + page ─────────────────────────────────────────────── */
* { box-sizing: border-box; }

body {
  margin: 0;
  padding: var(--s9) var(--s10) var(--s12);
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-data);
  font-size: var(--fs-body);
  line-height: 1.5;
}

/* Opt a container out of mono for prose. Data stays mono by default. */
.prose { font-family: var(--font-prose); }

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

code {
  background: var(--panel-2);
  padding: var(--s0) var(--s3);
  border-radius: var(--r-hair);
  font-family: var(--font-data);
}

/* ── Headings ─────────────────────────────────────────────────── */
header { display: flex; align-items: baseline; gap: var(--s9); flex-wrap: wrap; margin-bottom: var(--s8); }

h1 {
  font-size: var(--fs-h1);
  margin: 0;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
h2 {
  font-size: var(--fs-body);
  margin: var(--s11) 0 var(--s6);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink);
  border-bottom: 1px solid var(--line);
  padding-bottom: var(--s4);
}
h2 .muted { text-transform: none; letter-spacing: 0; }

/* ── Text modifiers ───────────────────────────────────────────── */
.meta  { color: var(--muted); font-size: var(--fs-meta); }
.muted { color: var(--muted); }
.warn  { color: var(--warn); }
.bad   { color: var(--alert); }
.ok    { color: var(--ok); }

/* ── Status dot ───────────────────────────────────────────────── */
.dot {
  display: inline-block;
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--muted);
}
.dot.ok    { background: var(--ok); }
.dot.warn  { background: var(--warn); }
.dot.alert { background: var(--alert); }

/* ── Tile / KPI card ──────────────────────────────────────────── */
.tiles { display: flex; gap: var(--s7); flex-wrap: wrap; }

.tile {
  display: flex; flex-direction: column; gap: var(--s1);
  min-width: 150px; flex: 1 1 150px;
  padding: var(--s7) var(--s8);
  background: var(--panel);
  border: 1px solid var(--line);
  border-left: 3px solid var(--muted);
  border-radius: var(--r-card);
  color: inherit;
}
.tile:hover { background: var(--panel-2); text-decoration: none; }

.tile .val     { font-size: var(--fs-big); line-height: 1.1; font-weight: 600; }
.tile .lbl     { font-size: var(--fs-label); text-transform: uppercase; letter-spacing: 0.09em; color: var(--muted); }
.tile .verdict { font-size: var(--fs-label); color: var(--muted); }

.tile.ok    { border-left-color: var(--ok); }
.tile.warn  { border-left-color: var(--warn); }
.tile.alert { border-left-color: var(--alert); }
.tile.alert .val { color: var(--alert); }

/* ── Banner ───────────────────────────────────────────────────── */
.banner {
  margin: var(--s8) 0;
  padding: var(--s7) var(--s8);
  border: 1px solid var(--alert);
  border-left-width: 3px;
  border-radius: var(--r-card);
  background: var(--alert-bg);
}
.banner ul { margin: var(--s5) 0 0; padding-left: var(--s10); color: var(--alert-ink); }

/* ── Data table ───────────────────────────────────────────────── */
table.data { width: 100%; border-collapse: collapse; }

table.data th {
  text-align: left;
  font-size: var(--fs-label);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
  border-bottom: 1px solid var(--line);
  padding: var(--s3) var(--s6);
  font-weight: 500;
}
table.data td {
  vertical-align: top;
  padding: var(--s7) var(--s6);
  border-bottom: 1px solid var(--line);
}
td.name { width: 20%; }
.pname  { font-weight: 600; }
.path   { color: var(--muted); font-size: var(--fs-label); word-break: break-all; }
.rowlinks { margin-top: var(--s2); display: flex; gap: var(--s3); flex-wrap: wrap; }

/* ── Chips, tags, metric pills ────────────────────────────────── */
.chip, .tag {
  display: inline-block;
  padding: var(--s0) var(--s5);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  font-size: var(--fs-label);
  color: var(--muted);
}
.chip.small { font-size: var(--fs-micro); padding: 0 var(--s4); }
.chips { display: flex; gap: var(--s5); flex-wrap: wrap; margin: var(--s5) 0 var(--s7); }
.chips b { color: var(--ink); }

.pmetric {
  display: inline-block;
  padding: var(--s0) var(--s5);
  border: 1px solid var(--line);
  border-radius: var(--r-inline);
  font-size: var(--fs-label);
  color: var(--muted);
}
.pmetric b { color: var(--ink); font-weight: 600; }
.pmetric i { font-style: normal; opacity: 0.7; }
.pmetric.bad  { border-color: var(--alert); color: var(--alert); }
.pmetric.bad b { color: var(--alert); }
.pmetric.warn { border-color: var(--warn); }

.pmetrics { display: flex; flex-wrap: wrap; gap: var(--s3); }
.phead    { font-size: var(--fs-label); margin-bottom: var(--s2); }

/* ── Collapsible group ────────────────────────────────────────── */
details.group {
  margin: var(--s5) 0;
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  background: var(--panel);
}
details.group > summary {
  cursor: pointer;
  padding: var(--s5) var(--s7);
  font-size: var(--fs-meta);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
details.group > summary b { color: var(--ink); }
details.group ul { margin: 0; padding: var(--s5) var(--s7) var(--s7) var(--s11); }
details.group li { margin: var(--s2) 0; }

ul.plain { list-style: none; margin: var(--s5) 0; padding: 0; }
ul.plain li { margin: var(--s2) 0; }
.tid    { color: var(--muted); font-size: var(--fs-label); }
.t-open { color: var(--ok); }

/* ── Controls ─────────────────────────────────────────────────── */
button, label.btn {
  background: var(--panel-2);
  color: var(--ink);
  border: 1px solid var(--line);
  padding: var(--s5) var(--s8);
  border-radius: var(--r-inline);
  cursor: pointer;
  font-size: var(--fs-body);
  font-family: inherit;
}
button:hover, label.btn:hover { background: var(--hover); }

button.primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--bg);
}
button.primary:hover { opacity: 0.88; }

button.danger {
  background: var(--alert-bg);
  border-color: var(--alert-line);
  color: var(--alert-ink);
}

input[type="number"], input[type="text"] {
  background: var(--panel-2);
  color: var(--ink);
  border: 1px solid var(--line);
  padding: var(--s3) var(--s6);
  border-radius: var(--r-inline);
  font-size: var(--fs-body);
  font-family: var(--font-data);
}
input[type="file"] { display: none; }

/* ── Canvas + status bar ──────────────────────────────────────── */
/* A canvas is a card only when it IS the plotting surface. A chart library stacks canvases per pane
   (lightweight-charts, Vela and KLineChart all do), so this rule's opaque background lands on the
   TOP layer and paints over the one carrying the series: the pane reads blank while every DOM read
   and the canvas bitmap itself say it is drawing. A canvas with a sibling is a layer, not a card. */
canvas {
  background: var(--canvas);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  display: block;
  width: 100%;
}

canvas:not(:only-child) {
  background: none;
  border: 0;
  border-radius: 0;
  width: auto;
}

.legend {
  padding: var(--s6) var(--s8);
  font-size: var(--fs-meta);
  color: var(--muted);
  background: var(--panel);
  border-top: 1px solid var(--line);
}
.legend b { color: var(--ink); }

.status { margin-left: auto; font-size: var(--fs-meta); color: var(--muted); }

.mode-badge {
  background: var(--panel-2);
  padding: var(--s1) var(--s6);
  border-radius: var(--r-pill);
  font-size: var(--fs-label);
  font-weight: 600;
}

/* ── Footer ───────────────────────────────────────────────────── */
footer {
  margin-top: var(--s12);
  padding-top: var(--s7);
  border-top: 1px solid var(--line);
  font-size: var(--fs-label);
}

/* ═══════════════════════════════════════════════════════════════════════════
   v1.1 — the components the fleet was already re-inventing

   Not speculative. Every shape below had this many independent local copies in
   ~/Projects before it landed here (counted by the 2026-10-02 surface audit, evidence
   in pipeline/SYSTEM-GAPS.md): generic card 10, table shell ~10, empty state ~9, form
   controls ~8, nav ~8, chip state variants ~8, KPI tile variant ~7, progress bar ~6,
   segmented/tabs ~6, skeleton ~6, dialog/menu/overlay ~5.

   "Add a component when a SECOND project needs it" is still the rule. The second
   project arrived; the class simply had no owner. Everything here is built from
   tokens.css only — zero literals — and every value is on the scale.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── The type steps that lead ───────────────────────────────────────────────
   tokens.css shipped --fs-lead/--fs-title/--fs-page/--fs-display with no consumer, so
   a page that wanted a title and a lede had to re-point h1 element-only and invent a
   class locally. These are the consumers. Use exactly ONE .display or .page-title per
   page: two elements of equal weight means one of them is wrong. */
.page-title {
  font-size: var(--fs-page);
  line-height: var(--lh-display);
  letter-spacing: var(--tracking-tight);
  margin: 0 0 var(--s4);
}
.display {
  font-size: var(--fs-display);
  line-height: var(--lh-display);
  letter-spacing: var(--tracking-tight);
  font-weight: 600;
  margin: 0 0 var(--s6);
}
.section-title { font-size: var(--fs-title); line-height: var(--lh-ui); margin: var(--s11) 0 var(--s6); }
.lede {
  font-size: var(--fs-lead);
  line-height: var(--lh-body);
  color: var(--muted);
  max-inline-size: var(--measure-tight);
  margin: 0 0 var(--s8);
}
/* The readable measure. Long prose lines are the "text heavy" complaint in one number. */
.measure { max-inline-size: var(--measure); }

/* ── Layout utilities ─────────────────────────────────────────────────────── */
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.min0 { min-width: 0; }          /* a flex child that must truncate instead of pushing */
.right { text-align: right; }

/* ── Panel — the neutral surface that did not exist ────────────────────────
   .tile is a KPI tile (flex column, status stripe), details.group is collapsible and
   .banner is alert-coloured, so a table or a form had no plain bordered home and every
   project wrote its own .card. 10 surfaces had one. */
.panel {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
}
.panel-head {
  display: flex;
  align-items: baseline;
  gap: var(--s8);
  padding: var(--s7) var(--s8);
  border-bottom: 1px solid var(--line);
}
.panel-head h2, .panel-head h3 {
  margin: 0; border: 0; padding: 0;
  font-size: var(--fs-ui);
  letter-spacing: 0;
  text-transform: none;
}
.panel-body { padding: var(--s8); }
/* One step roomier, for a panel whose content is prose rather than a table. A modifier, not a new
   token: the value (--s9) already exists. */
.panel-body.roomy { padding: var(--s9); }
.panel-foot {
  padding: var(--s7) var(--s8);
  border-top: 1px solid var(--line);
  color: var(--muted);
  font-size: var(--fs-meta);
}
.panel.flush > .panel-body { padding: 0; }   /* a table wants the full width */

/* ── Table shell ───────────────────────────────────────────────────────────
   10 surfaces ship a table and each wraps it differently. The sticky header needs an
   ancestor that scrolls, which is why .table-wrap is not optional. */
.table-wrap { overflow-x: auto; overscroll-behavior-x: contain; scrollbar-gutter: stable; }
.table-wrap table.data { min-width: 100%; }
.table-wrap thead th { position: sticky; top: 0; z-index: var(--z-sticky); background: var(--panel); }
table.data td { height: var(--row-h); }
table.data.dense td { height: var(--row-h-dense); padding-top: var(--s3); padding-bottom: var(--s3); }
table.data tbody tr:hover { background: var(--panel-2); }
/* Specificity (0,2,2), not (0,1,1): `table.data th` sets text-align and would win over a
   bare `th.num`. This cost a hop real time to discover, so it is written down here. */
table.data th.num, table.data td.num { text-align: right; font-variant-numeric: tabular-nums; }
table.data th .sort {
  background: none; border: 0; border-radius: 0; padding: 0; cursor: pointer;
  color: inherit; font: inherit; letter-spacing: inherit; text-transform: inherit;
}
table.data th[aria-sort="ascending"] .sort::after { content: " \25B2"; }
table.data th[aria-sort="descending"] .sort::after { content: " \25BC"; }

/* ── Empty state ───────────────────────────────────────────────────────────
   A table with no rows is the most common real state in this fleet (a quiet market is
   the normal case) and it had no component: 9 surfaces hand-wrote a line of text. */
.empty {
  display: flex; flex-direction: column; align-items: center; gap: var(--s5);
  padding: var(--s12); text-align: center; color: var(--muted);
}
.empty-title { font-size: var(--fs-ui); color: var(--ink); }
.empty-desc { font-size: var(--fs-meta); max-inline-size: var(--measure-tight); }
.empty-actions { display: flex; gap: var(--hit-gap); margin-top: var(--s5); }

/* ── Skeleton / loading ────────────────────────────────────────────────────
   A skeleton that matches the final layout, not a centred spinner over a blank pane.
   The animation survives the reduced-motion guard on purpose: a frozen loading
   indicator reads as a hung app, which is worse than the motion it saves. */
.skeleton {
  background: var(--panel-2);
  border-radius: var(--r-inline);
  min-height: var(--s8);
  animation: skeleton-pulse 1400ms var(--ease-in-out) infinite !important;
}
@keyframes skeleton-pulse { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }

/* ── Toolbar ───────────────────────────────────────────────────────────────
   Filters are ghost buttons; the active one is a pressed state, never a solid primary
   button (a solid CTA in a toolbar competes with the page's one action). */
.toolbar { display: flex; align-items: center; gap: var(--s6); flex-wrap: wrap; padding: var(--s6) 0; }
.toolbar .filter { background: transparent; border-color: transparent; color: var(--muted); }
.toolbar .filter[aria-pressed="true"] { background: var(--panel-2); border-color: var(--line); color: var(--ink); }
.toolbar .reset {
  background: none; border: 0; padding: 0; cursor: pointer;
  color: var(--accent); font-size: var(--fs-label);
}
.toolbar .spacer { margin-inline-start: auto; }

/* ── Nav / tabs / segmented ────────────────────────────────────────────────
   Logical properties throughout: the fleet has no RTL surface today, and this is the
   only cheap moment to make one possible. The active state carries aria-current or
   aria-selected — the attribute is the state, the class is not. */
.nav { display: flex; align-items: center; gap: var(--s8); flex-wrap: wrap; }
.nav a { color: var(--muted); padding-block: var(--s5); }
.nav a[aria-current="page"] { color: var(--ink); box-shadow: inset 0 -2px 0 var(--accent); }
.tabs { display: flex; gap: var(--s5); border-bottom: 1px solid var(--line); }
.tab {
  background: none; border: 0; border-bottom: 2px solid transparent; border-radius: 0;
  padding: var(--s5) var(--s6); color: var(--muted);
}
.tab[aria-selected="true"] { color: var(--ink); border-bottom-color: var(--accent); }
.segmented { display: inline-flex; border: 1px solid var(--line); border-radius: var(--r-inline); }
/* NO `overflow: hidden` here, and that is a fix, not an omission: it clipped the :focus-visible ring
   (painted 2-4px outside each segment's border box) to two short vertical slivers at the shared
   edges, so a keyboard user never saw a perimeter. A pipeline review over a built page found it
   with base.css pinned as the owner — the page could not fix it, and the linter cannot see it.
   Round the end segments explicitly instead of clipping the whole group. */
.segmented button { border: 0; border-radius: 0; background: transparent; color: var(--muted); }
.segmented button[aria-pressed="true"] { background: var(--panel-2); color: var(--ink); }
.segmented button + button { border-inline-start: 1px solid var(--line); }
.segmented > button:first-child { border-start-start-radius: var(--r-inline); border-end-start-radius: var(--r-inline); }
.segmented > button:last-child { border-start-end-radius: var(--r-inline); border-end-end-radius: var(--r-inline); }
/* The ring draws INSIDE a segment: siblings abut by design (this is one control, not separate
   targets), so an outset ring would sit on top of the neighbouring segment. */
.segmented > button:focus-visible { outline-offset: calc(-1 * var(--s1)); }

/* ── Chip state variants ───────────────────────────────────────────────────
   .chip existed with no state variants, so 8 surfaces invented .pill/.tier/.flag/
   .badge/.score to say the same three things. State, not decoration. */
.chip.ok, .chip.warn, .chip.alert { background: var(--panel-2); }
.chip.ok    { border-color: var(--ok-line);    color: var(--ok-ink);    background: var(--ok-bg); }
.chip.warn  { border-color: var(--warn-line);  color: var(--warn-ink);  background: var(--warn-bg); }
.chip.alert { border-color: var(--alert-line); color: var(--alert-ink); background: var(--alert-bg); }

/* ── Banner gains ok/warn ──────────────────────────────────────────────────
   Banners had an alert colour only, so an "all clear" or a degraded notice borrowed
   the destructive fill. */
.banner.ok   { border-color: var(--ok);   background: var(--ok-bg); }
.banner.warn { border-color: var(--warn); background: var(--warn-bg); }
.banner.ok   ul { color: var(--ok-ink); }
.banner.warn ul { color: var(--warn-ink); }

/* ── Progress / usage bar ─────────────────────────────────────────────────── */
.bar { height: var(--s5); background: var(--panel-2); border-radius: var(--r-pill); overflow: hidden; }
.bar > span { display: block; height: 100%; background: var(--accent); }
.bar.ok > span    { background: var(--ok); }
.bar.warn > span  { background: var(--warn); }
.bar.alert > span { background: var(--alert); }

/* ── KPI tile: the one primary, and a neutral change line ──────────────────
   Four identical cards is the template tell, so one tile may be .primary. The delta is
   NEUTRAL by default: gains-green/losses-red is a trading convention, and it applies
   only where the domain demands it (.delta.up/.delta.down on a trading surface). */
.tile .delta { font-size: var(--fs-label); color: var(--muted); font-variant-numeric: tabular-nums; }
.tile.primary { background: var(--panel-2); border-color: var(--accent); }
.tile.primary .val { font-size: var(--fs-display); }
.delta.up   { color: var(--ok); }
.delta.down { color: var(--alert); }

/* ── Series swatches ───────────────────────────────────────────────────────
   A canvas chart cannot read a token, but its legend is HTML and can. These are the
   only sanctioned consumers of the series palette, and they keep the legend and the
   plot from disagreeing. */
.series-swatch { display: inline-block; width: var(--s6); height: var(--s6); border-radius: var(--r-hair); }
.series-swatch.s1 { background: var(--series-1); }
.series-swatch.s2 { background: var(--series-2); }
.series-swatch.s3 { background: var(--series-3); }
.series-swatch.s4 { background: var(--series-4); }
.series-swatch.s5 { background: var(--series-5); }
.series-swatch.s6 { background: var(--series-6); }
.series-swatch.s7 { background: var(--series-7); }

/* ── States and accessibility ─────────────────────────────────────
   The sheet covered components and hover, but nothing for keyboard focus, a
   disabled control, or a screen reader. A dark dashboard with no focus state is
   unusable without a mouse, so this is a gap, not a nicety.
   The ring reuses --accent (7.49:1 on --bg) — no new colour was invented. */

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: var(--s1);
  /* No border-radius here: `outline` already follows the element's own radius, and
     setting one flattened every rounded component (a .tile went 6px -> 2px) on focus. */
}

button:active, label.btn:active { transform: translateY(1px); }

/* The :hover selectors are repeated here on purpose: button.primary:hover is (0,2,1)
   and would otherwise beat this (0,1,1) rule, so a disabled primary button brightened
   to 0.88 opacity on hover - looking healthier than when untouched. */
button:disabled, label.btn:disabled, [aria-disabled="true"],
button:disabled:hover, label.btn:disabled:hover {
  opacity: 0.55;
  cursor: not-allowed;
  border-color: var(--line);
}
button:disabled:hover, label.btn:disabled:hover { background: var(--panel-2); }

input::placeholder, textarea::placeholder { color: var(--muted); }

::selection { background: var(--accent); color: var(--bg); }

/* Reachable by a screen reader, invisible on screen — for icon-only controls
   and for labelling an input that has no visible label. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ── Form field ────────────────────────────────────────────────────────────
   The fleet had NO validation pattern, so an error was a red border somebody remembered to
   add. Anatomy: label above the control, hint above it, error BELOW the control, all three
   wired by id so a screen reader reads them (aria-describedby), and the error carries an
   icon and text — never colour alone. Timing is a rule, not a prop: format checks on blur,
   everything else on submit, never on mount. */
.field { display: flex; flex-direction: column; gap: var(--s3); margin-bottom: var(--s8); }
.field-label { font-size: var(--fs-label); color: var(--muted); }
.field-hint { font-size: var(--fs-meta); color: var(--muted); }
.field-error { display: flex; gap: var(--s5); align-items: baseline; font-size: var(--fs-meta); color: var(--alert); }
.field-error::before { content: "\26A0"; }
.field input, .field select, .field textarea, select, textarea {
  background: var(--panel-2); color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--r-inline);
  min-height: var(--h-control); padding: var(--s3) var(--s6);
  font-size: var(--fs-ui); font-family: var(--font-data);
}
.field input:focus-visible, .field select:focus-visible, .field textarea:focus-visible { border-color: var(--accent); }
.field[data-invalid] .field-label { color: var(--alert); }
.field[data-invalid] input, .field[data-invalid] select, .field[data-invalid] textarea { border-color: var(--alert); }
.field-row { display: flex; gap: var(--s8); flex-wrap: wrap; }
.field-row > .field { flex: 1 1 160px; }
input[type="checkbox"], input[type="radio"] { min-height: 0; accent-color: var(--accent); }

/* ── Layers ────────────────────────────────────────────────────────────────
   Five surfaces hand-built overlays, each with its own z-index and its own backup rgba.
   These use the layer ladder and the one scrim. Focus trapping and focus return are JS
   jobs — CSS cannot do them and does not pretend to. A popover scales from its trigger
   (--menu-origin, set from JS); a modal stays centred. */
.overlay { position: fixed; inset: 0; background: var(--scrim); z-index: var(--z-overlay); }
.dialog {
  position: fixed; z-index: var(--z-overlay);
  inset-inline: 0; margin-inline: auto; inset-block-start: var(--s12);
  max-inline-size: var(--measure-tight);
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-pill);
  box-shadow: var(--shadow-3); padding: var(--s10);
  transform-origin: center;
  transition: opacity var(--dur-overlay) var(--ease-out), transform var(--dur-overlay) var(--ease-out);
}
.dialog-title { font-size: var(--fs-title); line-height: var(--lh-ui); margin: 0 0 var(--s5); }
.dialog-desc { font-size: var(--fs-meta); color: var(--muted); margin: 0 0 var(--s8); }
.dialog-foot { display: flex; gap: var(--s6); justify-content: flex-end; margin-top: var(--s10); }
.sheet {
  position: fixed; inset-block: 0; inset-inline-end: 0; z-index: var(--z-overlay);
  inline-size: min(var(--measure-tight), 100%);
  background: var(--panel); border-inline-start: 1px solid var(--line); padding: var(--s10);
  transition: transform var(--dur-sheet) var(--ease-drawer);
}
.menu {
  z-index: var(--z-dropdown); background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-inline); box-shadow: var(--shadow-2); padding: var(--s3); min-inline-size: 160px;
  transform-origin: var(--menu-origin);
  transition: opacity var(--dur-menu) var(--ease-out);
}
.menu-item {
  display: block; inline-size: 100%; text-align: start; background: none; border: 0;
  border-radius: var(--r-hair); padding: var(--s5) var(--s6); color: var(--ink); cursor: pointer;
}
.menu-item:hover, .menu-item[data-highlighted] { background: var(--hover); }
.menu-sep { height: 1px; background: var(--line); margin: var(--s3) 0; border: 0; }
.toast {
  z-index: var(--z-toast); background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-inline); box-shadow: var(--shadow-2);
  padding: var(--s7) var(--s8); font-size: var(--fs-meta);
  transition: opacity var(--dur-overlay) var(--ease-out);
}
.toast-stack {
  position: fixed; inset-block-end: var(--s10); inset-inline-end: var(--s10);
  z-index: var(--z-toast);          /* without this the stack sits under a dialog (--z-overlay 30) */
  display: flex; flex-direction: column; gap: var(--s6);
}
.tooltip {
  z-index: var(--z-tooltip); background: var(--panel-2); border: 1px solid var(--line);
  border-radius: var(--r-inline); padding: var(--s4) var(--s6);
  font-size: var(--fs-meta); color: var(--ink);
  transition: opacity var(--dur-tip) var(--ease-out);
}

/* ── Motion: the only two things a product UI may animate ───────────────────
   A press is transform-only and short; a hover exists only where there IS a pointer,
   because on touch a false :hover fires on tap. Nothing animates layout metrics — a
   font-weight or padding change on hover reflows and cannot be composited. */
button, .tab, .segmented button, .menu-item, .chip {
  transition: background-color var(--dur-press) var(--ease-out),
              border-color var(--dur-press) var(--ease-out),
              color var(--dur-press) var(--ease-out),
              transform var(--dur-press) var(--ease-out);
}
/* Overrides the older translateY(1px) press: one press language, not two. */
button:active, label.btn:active, .menu-item:active { transform: scale(0.97); }
@media (hover: hover) and (pointer: fine) {
  .tile:hover, .menu-item:hover { transform: translateY(-1px); }
}
/* Touch floors: 24px is the AA minimum, 44px the comfortable one. An icon-only control is
   the usual offender, so it takes the comfortable size wherever the pointer is coarse. */
button.icon { min-inline-size: var(--tap-min); min-block-size: var(--tap-min); padding: var(--s5); }
@media (pointer: coarse) {
  button.icon { min-inline-size: var(--tap-comfortable); min-block-size: var(--tap-comfortable); }
}
/* A sticky bar must never cover the element you just jumped or focused to. */
[id] { scroll-margin-block-start: var(--s12); }
.skip-link {
  position: absolute; inset-block-start: -100%; inset-inline-start: var(--s8);
  background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--r-inline);
  padding: var(--s5) var(--s6); z-index: var(--z-sticky);
}
.skip-link:focus { inset-block-start: var(--s8); }

/* Nothing above animates on its own, but this is the guard that stops the next
   transition someone adds from ignoring the OS setting. --motion-kill is 0.01ms and not 0:
   at 0 the transitionend/animationend events stop firing and JS awaiting them expires.
   .skeleton keeps its animation on purpose — a frozen loading state reads as a hung app. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: var(--motion-kill) !important;
    animation-iteration-count: 1 !important;
    transition-duration: var(--motion-kill) !important;
    scroll-behavior: auto !important;
  }
}
