/* =============================================================
   Lindela Lite — shared component layer

   Loaded by every surface after tokens.css. This exists because seven of the
   eight surfaces carried 236–326 lines of inline <style> re-declaring the
   reset, body, header, footer, buttons, tables, inputs and dialogs. Two pages
   had ended up with three different spellings of danger red and one with a
   light-theme palette on a dark page.

   These are page-level composites built on the primitives in styles.css: the
   things a page needs to look like a page rather than like a document.
   ============================================================= */

@import url('/tokens.css');

/* --- The one rule that outlives every surface -------------------
   `[hidden]` is a UA-stylesheet declaration at specificity (0,1,0) — a single
   attribute selector. Any author rule naming a `display` value on the same
   element outranks it on specificity alone, silently, with no error anywhere.
   That is not hypothetical: this class shipped three times before this rule
   existed. `.offline-banner { display: flex }` rendered the amber offline bar on
   every load, online or not. `.rail-panel { display: flex }` did the same to
   four inactive tab panels, leaving 58 keyboard-focusable controls in
   `opacity: 0` at coordinates identical to the live panel's — reachable by Tab,
   invisible on screen, no focus ring. `.statusbar-metric { display: inline }`
   made the status bar print three labels with nothing after each colon, and the
   audit read it as a panel reporting nothing.

   The defect is invisible in review because the markup is correct: `hidden` is
   present, `display: none` is in the UA sheet, and nothing is wrong *except*
   that a competing author rule won. So it is closed here rather than at each
   site. `!important` is the right escalation for this one property: it is the
   only declaration in the codebase whose entire job is to lose to nothing, and
   it is scoped to the exact attribute whose meaning is being protected.

   Two consequences, both load-bearing:
     - Code must unhide by *removing* the attribute, never by writing
       `style.display`. An author cannot win against this rule, so a
       `.style.display = 'block'` un-hide silently does nothing. `el.hidden =
       false` is the only working un-hide.
     - `@media print` must therefore re-assert on a *more specific* selector
       than `[hidden]` alone, not on `.rail-panel`. See styles.css's print
       block, which is why it reads `.rail-panel[hidden]`. */
[hidden] { display: none !important; }

/* --- Page frame --------------------------------------------- */
.page-main {
  width: 100%;
  max-width: 68rem;
  margin-inline: auto;
  padding: var(--sp-5);
}

.page-title {
  font-size: var(--text-h2);
  font-weight: 700;
}

.page-title-muted { font-weight: 400; color: var(--ink-muted); }

.section-title {
  margin: var(--sp-6) 0 var(--sp-3);
  padding-bottom: var(--sp-1);
  border-bottom: 1px solid var(--stroke);
  font-size: var(--text-body);
  font-weight: 600;
  color: var(--ink);
}

/* --- Severity chip -----------------------------------------
   HX-07. Four surfaces spelled the same concept four ways, and the audit
   measured the result: `/districts/` rendered `CRITICAL` in a 3px grey-on-tint
   square, `/focal-point/` rendered `HIGH` in a 12px pill, the console rendered
   `low` in a 3px pill with a different tint strength, and `shared/fmt.js`'s own
   `sevChip()` handed out a fifth spelling. A user hopping between the district
   list and the console re-learned what severity looked like on every hop.

   One box, one case, one tint, one marker. The word is lowercase because that
   is what the console — the densest and most-used surface — already did and the
   two others were shouting over it; the glyph and the hatch below carry the
   signal that the shouting used to.

   The foreground is body ink on a hue tint, not white on the raw hue. Measured,
   the raw hue at this size lands at ~3.2:1 for critical and ~2.2:1 for low —
   three of four below the 4.5:1 AA floor, on the most operationally urgent
   attribute in the product. The word is always present beside the tint, so
   meaning never depends on hue alone.

   Every selector below is prefixed with `body` deliberately. Two surfaces carry
   their own copy of these rules in an inline <style> that loads after this file
   and wins a specificity tie; `body` outranks them so the one chip declared
   here is the one that renders. The copies are deleted as each of those files
   is next opened — the selectors here are the record of what they must
   converge to. */

body .sev-chip,
body .severity-chip,
body .chip-critical,
body .chip-high,
body .chip-medium,
body .chip-low,
body .chip-unknown,
body .chip-neutral,
body .chip-ok {
  display: inline-flex;
  align-items: center;
  padding: 1px 6px;
  font-size: var(--text-xs);
  font-weight: 700;
  border-radius: var(--r-sm);
  border: 0;
  line-height: 1.5;
  text-transform: none;
  color: var(--ink);
  white-space: nowrap;
  flex-shrink: 0;
}

body .sev-unknown,
body .chip-unknown,
body .chip-neutral { background: var(--surface); color: var(--ink-muted); }

body .sev-critical,
body .chip-critical {
  background: color-mix(in oklch, var(--sev-critical) 22%, transparent);
  box-shadow: inset 0 0 0 1px var(--sev-critical);
}
body .sev-high,
body .chip-high {
  background: color-mix(in oklch, var(--sev-high) 22%, transparent);
  box-shadow: inset 0 0 0 1px var(--sev-high);
}
body .sev-medium,
body .chip-medium {
  background: color-mix(in oklch, var(--sev-medium) 22%, transparent);
  box-shadow: inset 0 0 0 1px var(--sev-medium);
}
body .sev-low,
body .chip-low {
  background: color-mix(in oklch, var(--sev-low) 22%, transparent);
  box-shadow: inset 0 0 0 1px var(--sev-low);
}
/* Sentiment's positive pole. Not a severity class, so it stays out of the
   severity ramp — but it is the same chip, and leaving it at a different tint
   strength and with a border would make it the fifth spelling. */
body .chip-ok {
  background: color-mix(in oklch, var(--ok) 22%, transparent);
  box-shadow: inset 0 0 0 1px var(--ok);
}

/* Shape and texture: the redundancy for anyone who cannot see the tint. Two
   channels because they fail independently — the glyph is a shape (survives any
   colour vision deficiency and greyscale printing), the hatch is a texture
   (survives a projector with no colour left to project). */

body .sev-critical::before,
body .severity-critical::before,
body .chip-critical::before { content: var(--sev-marker-critical) ' '; }
body .sev-high::before,
body .severity-high::before,
body .chip-high::before { content: var(--sev-marker-high) ' '; }
body .sev-medium::before,
body .severity-medium::before,
body .chip-medium::before { content: var(--sev-marker-medium) ' '; }
body .sev-low::before,
body .severity-low::before,
body .chip-low::before { content: var(--sev-marker-low) ' '; }
body .sev-unknown::before,
body .severity-unknown::before,
body .chip-unknown::before,
body .chip-neutral::before { content: var(--sev-marker-unknown) ' '; }
/* `.chip-ok` gets no marker on purpose. Sentiment is not on the severity ramp,
   and borrowing low's '●' for it would put two unrelated meanings on one
   glyph — the same collision the ramp exists to prevent. */

/* 0.85em, so a 12px chip widens by two characters rather than four.
   `.sev-chip` is flex-shrink:0 in a nowrap row: every character here is one the
   row cannot give up at 320px, and check-a11y.mjs's reflow probe measures it. */
body .sev-chip::before,
body .severity-chip::before,
body .chip-critical::before,
body .chip-high::before,
body .chip-medium::before,
body .chip-low::before,
body .chip-unknown::before,
body .chip-neutral::before { font-size: 0.85em; line-height: 1; }

/* Densest hatch is the worst class; `low` gets none at all, because a texture
   meaning "slightly less bad" is worse than one meaning "nothing here". */
body .sev-critical,
body .chip-critical {
  background-image: repeating-linear-gradient(
    45deg,
    color-mix(in oklch, var(--sev-critical) 28%, transparent) 0 2px,
    transparent 2px var(--sev-hatch-critical));
}
body .sev-high,
body .chip-high {
  background-image: repeating-linear-gradient(
    45deg,
    color-mix(in oklch, var(--sev-high) 28%, transparent) 0 2px,
    transparent 2px var(--sev-hatch-high));
}
body .sev-medium,
body .chip-medium {
  background-image: repeating-linear-gradient(
    90deg,
    color-mix(in oklch, var(--sev-medium) 28%, transparent) 0 2px,
    transparent 2px var(--sev-hatch-medium));
}

/* --- Text --------------------------------------------------- */
.empty-note,
.empty-cell {
  color: var(--ink-muted);
  font-size: var(--text-sm);
}
.empty-cell { text-align: center; padding: var(--sp-5) var(--sp-3); }

.muted-sm { color: var(--ink-muted); font-size: var(--text-sm); }
.mono-sm  { font-family: var(--font-mono); font-size: var(--text-sm); }
.nowrap  { white-space: nowrap; }
.num-cell { text-align: right; font-variant-numeric: tabular-nums; }

.field-note {
  margin-top: var(--sp-1);
  color: var(--ink-muted);
  font-size: var(--text-sm);
}

/* --- Clamping long values ------------------------------------
   HX-14. A 500-character rule name rendered a 4301px element inside a 375px
   viewport, and its `.rule-meta` sibling 3371px. The page-level scrollWidth was
   contained only because an ancestor scrolled, so the overflow was invisible to
   every gate while pushing the whole card grid out of alignment.

   `min-width: 0` is not optional in a flex or grid child: without it the item
   sizes to its content's min-content width and the clamp has nothing to clamp
   against. `-webkit-line-clamp` is the only line-clamp with browser support
   that does not require a fixed height, and the value is short enough to stay
   declarative.

   A clamp alone deletes a fact. Every element carrying one must also carry the
   full value in a `title` — the same rule the console already follows on alert
   rule names, where `truncate()` shortens the visible text and `title=` keeps
   the original one hop away. */
.clamp-2,
.clamp-3 {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  overflow: hidden;
  min-width: 0;
  overflow-wrap: anywhere;
}
.clamp-2 { -webkit-line-clamp: 2; }
.clamp-3 { -webkit-line-clamp: 3; }

/* --- Notices ------------------------------------------------ */
/* Replaces hardcoded pastel panels (#f0fdf4, #fffbeb, #fef2f2) that had been
   ported from a light design and rendered as bright rectangles on a dark page. */
.notice {
  margin-top: var(--sp-3);
  padding: var(--sp-3);
  border: 1px solid;
  border-radius: var(--r);
  font-size: var(--text-sm);
  line-height: 1.5;
}
.notice-block { margin-top: 0; }

.notice-ok     { background: color-mix(in oklch, var(--ok) 10%, transparent);     border-color: color-mix(in oklch, var(--ok) 40%, transparent); }
.notice-warn   { background: color-mix(in oklch, var(--warn) 12%, transparent);   border-color: color-mix(in oklch, var(--warn) 45%, transparent); }
.notice-danger { background: color-mix(in oklch, var(--danger) 12%, transparent); border-color: color-mix(in oklch, var(--danger) 45%, transparent); }

.testnet-banner {
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid color-mix(in oklch, var(--warn) 45%, transparent);
  border-radius: var(--r);
  background: color-mix(in oklch, var(--warn) 12%, transparent);
  color: var(--ink);
  font-size: var(--text-sm);
}

/* --- Forms -------------------------------------------------- */
.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: var(--sp-3) var(--sp-4);
  /* start, not end: a field with a long note under it made its own label drift
     up out of line with its neighbours. */
  align-items: start;
}

/* Each cell is a block so a label, control and note stay together however the
   grid distributes columns. */
.form-grid > div { display: flex; flex-direction: column; gap: var(--sp-1); }

/* The checkbox row has to override the column rule above it, so it needs to
   match it on specificity — as a plain `.form-check` it lost to `.form-grid > div`
   and the box floated above its own label again. */
.form-grid > .form-check {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: var(--sp-2);
  /* Aligns the checkbox with the control it labels, not the top of the cell. */
  padding-block: calc(var(--sp-2) + var(--sp-1));
}

.form-submit { margin-top: var(--sp-4); }

.form-error {
  margin-top: var(--sp-2);
  color: var(--danger);
  font-size: var(--text-sm);
}

.disclosure {
  margin-top: var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--stroke);
  border-radius: var(--r);
  background: var(--bg-elevated);
}

.disclosure > summary {
  cursor: pointer;
  font-weight: 600;
  padding: var(--sp-1) 0;
}
.disclosure > summary::marker { color: var(--ink-muted); }

/* --- Cards -------------------------------------------------- */
.rule-card {
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--stroke);
  border-radius: var(--r);
  background: var(--bg-elevated);
  margin-bottom: var(--sp-2);
}

.rule-head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
}

/* The rule name is the only thing distinguishing one rule card from the next,
   so it is the element that must not be the one that overflows. `flex-basis:
   12rem` rather than `auto` so the name claims a share of the row and the
   badges keep theirs; `min-width: 0` so `.clamp-2` on the name has a width to
   clamp against rather than the content's min-content width. */
.rule-head > .rule-name {
  flex: 1 1 12rem;
  min-width: 0;
}

/* The metadata line was a run-on string with pipes. A definition grid reads as
   three facts rather than one sentence. */
.rule-meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-1) var(--sp-4);
  margin-top: var(--sp-2);
  color: var(--ink-muted);
  font-size: var(--text-sm);
}

.meta-key { color: var(--ink-faint); }

/* --- Chips -------------------------------------------------- */
.chain-badge {
  display: inline-block;
  padding: 0.1rem 0.4rem;
  border-radius: 999px;
  background: color-mix(in oklch, var(--brand) 18%, transparent);
  color: var(--ink);
  border: 1px solid color-mix(in oklch, var(--brand) 35%, transparent);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  white-space: nowrap;
}

/* --- Tables ------------------------------------------------- */
.table-wrap {
  overflow-x: auto;
  border: 1px solid var(--stroke);
  border-radius: var(--r);
}

.table-wrap table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-sm);
}

.table-wrap th,
.table-wrap td {
  padding: var(--sp-2) var(--sp-3);
  text-align: start;
  border-bottom: 1px solid var(--stroke);
  vertical-align: top;
}

.table-wrap thead th {
  position: sticky;
  top: 0;
  background: var(--surface);
  color: var(--ink-muted);
  font-weight: 600;
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  white-space: nowrap;
  z-index: 1;
}

.table-wrap tbody tr:last-child td { border-bottom: none; }
.table-wrap tbody tr:hover { background: var(--surface); }

/* Amounts and timestamps must not wrap: "3,000 / USD" on two lines reads as two
   separate facts, and a broken timestamp reads as a wrong one. */
.table-wrap td.num-cell { white-space: nowrap; }

/* --- Result panel ------------------------------------------- */
.result-grid {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: var(--sp-1) var(--sp-4);
  margin: var(--sp-3) 0 0;
  font-size: var(--text-sm);
}
.result-grid dt { color: var(--ink-muted); }
.result-grid dd { margin: 0; }

/* --- Loading ------------------------------------------------ */
.skeleton {
  border-radius: var(--r-sm);
  background: linear-gradient(
    90deg,
    var(--surface) 25%,
    var(--surface-hover) 37%,
    var(--surface) 63%
  );
  background-size: 400% 100%;
  animation: skeleton-sweep 1.4s ease-in-out infinite;
  color: transparent;
}

@keyframes skeleton-sweep {
  from { background-position: 100% 50%; }
  to   { background-position: 0 50%; }
}

/* The reduced-motion treatment for .skeleton lives with .spinner further down,
   so one query covers every motion this file declares. */

/* --- Error state -------------------------------------------- */
.error-panel {
  padding: var(--sp-4);
  border: 1px solid color-mix(in oklch, var(--danger) 45%, transparent);
  border-radius: var(--r);
  background: color-mix(in oklch, var(--danger) 10%, transparent);
}
.error-panel strong { display: block; margin-bottom: var(--sp-1); }
.error-panel p { color: var(--ink-muted); font-size: var(--text-sm); }

/* --- Responsive --------------------------------------------- */
@media (max-width: 640px) {
  .page-main { padding: var(--sp-4) var(--sp-3); }
  .result-grid { grid-template-columns: 1fr; }
  .result-grid dt { margin-top: var(--sp-2); }
  .rule-meta { gap: var(--sp-1) var(--sp-3); }
}
/* --- Field grouping ------------------------------------------------------ */
/* A label plus its control plus its note, kept together however the grid
   distributes columns. */
.field { display: flex; flex-direction: column; gap: var(--sp-1); }
.field-label { color: var(--ink-muted); font-size: var(--text-xs); font-weight: 500; }

.fieldset {
  margin: var(--sp-4) 0 0;
  padding: var(--sp-3);
  border: 1px solid var(--stroke);
  border-radius: var(--r);
  background: var(--bg-elevated);
}
.fieldset > legend {
  padding: 0 var(--sp-2);
  color: var(--ink-muted);
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.asset-list {
  max-height: 11rem;
  overflow-y: auto;
  padding: var(--sp-2);
  border: 1px solid var(--stroke);
  border-radius: var(--r-sm);
  background: var(--bg);
}
/* Block-level rows, not inline: inline-flex made the options flow and wrap into
   ragged columns, so a list of assets read as a paragraph. */
.asset-list .check-row {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  width: 100%;
  padding: var(--sp-2);
  border-radius: var(--r-sm);
  font-size: var(--text-sm);
  color: var(--ink);
}
.asset-list .check-row:hover { background: var(--surface); }
.asset-list .check-row span:first-of-type { flex: 1 1 auto; }

.panel {
  padding: var(--sp-4);
  border: 1px solid var(--stroke);
  border-radius: var(--r);
  background: var(--bg-elevated);
}

.page-lede {
  max-width: 60ch;
  margin: var(--sp-2) 0 var(--sp-5);
  color: var(--ink-muted);
  line-height: 1.6;
}

.share-section { margin-top: var(--sp-5); }
.share-section .field-note { margin-top: var(--sp-1); }

.ribbon-sub-name {
  margin-inline-start: var(--sp-2);
  color: var(--ink-muted);
  font-size: var(--text-sm);
  font-weight: 400;
}

.loading-note { padding: var(--sp-4); color: var(--ink-muted); }

.back-link {
  display: inline-block;
  margin-bottom: var(--sp-3);
  color: var(--ink-muted);
  font-size: var(--text-sm);
}
.back-link:hover { color: var(--ink); }

.data-gap {
  margin-inline-start: var(--sp-1);
  padding: 0.05rem 0.3rem;
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink-muted);
  font-size: var(--text-xs);
}

/* --- Range input ----------------------------------------------------- */
/* Unstyled, this was a full-bleed default-blue track, the only control in the
   product still wearing the browser's own look. */
input[type="range"] {
  appearance: none;
  width: 100%;
  height: 1.5rem;
  background: transparent;
  cursor: pointer;
}

input[type="range"]::-webkit-slider-runnable-track {
  height: 0.35rem;
  border-radius: 999px;
  background: var(--stroke-strong);
}

input[type="range"]::-webkit-slider-thumb {
  appearance: none;
  width: 1.15rem;
  height: 1.15rem;
  margin-top: -0.4rem;
  border-radius: 50%;
  background: var(--brand);
  border: 2px solid var(--bg);
}

input[type="range"]::-moz-range-track {
  height: 0.35rem;
  border-radius: 999px;
  background: var(--stroke-strong);
}

input[type="range"]::-moz-range-thumb {
  width: 1.15rem;
  height: 1.15rem;
  border-radius: 50%;
  background: var(--brand);
  border: 2px solid var(--bg);
}

input[type="range"]:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 3px var(--focus-ring); }
input[type="range"]:focus-visible::-moz-range-thumb     { box-shadow: 0 0 0 3px var(--focus-ring); }

.builder-run { width: 100%; margin-top: var(--sp-4); min-height: var(--tap-target-min); }

/* ===================================================================
   Primitives that did not exist until now
   =================================================================== */

/* --- Spinner ----------------------------------------------------
   styles.css has three @keyframes and none of them is a loader: the map
   marker pulse, the item enter, the dialog enter. A page that is waiting on
   the network had nothing to show but an empty panel, so "loading" and "there
   is nothing here" looked identical.

   Built from a conic gradient rather than a border-top spinner so the ring
   stays one colour and reads at 14px in a table cell. `.spinner-label` is the
   accessible text — the ring is aria-hidden decoration and carries no meaning
   on its own. */
.spinner {
  display: inline-block;
  inline-size: 1.25rem;
  block-size: 1.25rem;
  border-radius: 50%;
  /* The gap is what makes the rotation legible; a full ring reads as a static
     disc at small sizes. */
  background: conic-gradient(from 0turn, transparent 0 25%, var(--brand) 92%);
  /* Safari still wants the prefixed form; the product ships to iOS field teams. */
  -webkit-mask: radial-gradient(closest-side, transparent 62%, #000 64%);
  mask: radial-gradient(closest-side, transparent 62%, #000 64%);
  animation: spinner-turn 0.9s linear infinite;
  vertical-align: -0.25em;
}

@keyframes spinner-turn {
  to { transform: rotate(1turn); }
}

.spinner-lg { inline-size: 2rem; block-size: 2rem; }

.spinner-label {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  color: var(--ink-muted);
  font-size: var(--text-sm);
}

/* styles.css already has a global reduced-motion kill-switch that clamps every
   animation to 0.01ms and one iteration. It stops the rotation, but it leaves
   the ring painted — and at 0.01ms the conic gradient stops at whatever angle
   the first frame lands on, so the loader reads as a static disc, which is the
   one thing a loader must never look like. Dimming it keeps the meaning without
   asking the compositor for a frame. */
@media (prefers-reduced-motion: reduce) {
  .spinner { animation: none; opacity: 0.55; }
  .skeleton { animation: none; background: var(--surface); }
}

/* --- Toast ------------------------------------------------------
   chw/index.html carries a hand-rolled `.toast`: fixed, bottom-centred,
   max-width 420px, `display:none` until `.show`. The shape below is that one,
   expressed in tokens so the colour comes from the same palette as the rest of
   the product rather than from a hardcoded #10b981 that read as a different
   green on a dark page.

   State is driven by `data-kind` because that is what chw's app.js already
   sets (`showToast(msg, 'info' | 'ok' | 'error')`), so adoption is a delete of
   the inline block and nothing else. The `.toast-*` aliases are for callers
   that would rather set a class.

   `role="status"` on the element is required, not optional: a toast that only
   appears visually is invisible to a screen reader, which is the single most
   common way a notification surface fails. */
.toast {
  position: fixed;
  left: var(--sp-4);
  right: var(--sp-4);
  bottom: var(--sp-4);
  z-index: var(--z-toast);
  max-width: 26rem;
  margin-inline: auto;
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--stroke);
  border-radius: var(--r);
  background: var(--bg-elevated);
  box-shadow: var(--shadow-lg);
  color: var(--ink);
  font-size: var(--text-sm);
  font-weight: 600;
  text-align: center;
  /* Hidden rather than display:none, so a toast added while the node is
     already in the tree can still be announced. */
  opacity: 0;
  visibility: hidden;
  transform: translateY(0.75rem);
  transition: opacity var(--dur-normal) var(--ease), transform var(--dur-normal) var(--ease);
}

.toast.show {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

/* The tinted state is a left bar, not a full-colour fill: a solid #10b981
   block with white text measured 2.6:1 in the field, below the 4.5:1 AA floor
   for 15px text. Tinting the surface and keeping --ink on it holds. */
.toast[data-kind="ok"],
.toast-ok {
  border-color: color-mix(in oklch, var(--ok) 45%, transparent);
  background: color-mix(in oklch, var(--ok) 14%, var(--bg-elevated));
}

.toast[data-kind="error"],
.toast-error {
  border-color: color-mix(in oklch, var(--danger) 50%, transparent);
  background: color-mix(in oklch, var(--danger) 14%, var(--bg-elevated));
}

.toast[data-kind="info"],
.toast-info {
  border-color: color-mix(in oklch, var(--brand) 45%, transparent);
  background: color-mix(in oklch, var(--brand) 12%, var(--bg-elevated));
}

@media (prefers-reduced-motion: reduce) {
  .toast { transition: none; transform: none; }
  .toast.show { transform: none; }
}

/* --- Tooltip ----------------------------------------------------
   Hover alone is not an interaction: it is unreachable by keyboard, invisible
   to a screen reader and unrecorded by a screen reader user on a touch device.
   So the trigger is a real <button> (focusable, announced) and the bubble opens
   on :focus-visible as well as :hover.

   The bubble is a positioned sibling rather than a pseudo-element because the
   content has to be in the DOM for it to be readable, and because
   ::after cannot be truncated or measured.

   Requires `aria-describedby` pointing at the bubble id — without it a screen
   reader user gets the abbreviation "FEWS NET" and never the expansion. */
.tooltip-anchor {
  position: relative;
  display: inline-flex;
  align-items: baseline;
  gap: var(--sp-1);
  /* The dotted underline is the affordance; without a visible cue the button is
     indistinguishable from the text around it. */
  border: 0;
  padding: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: start;
  cursor: help;
  border-bottom: 1px dotted var(--ink-faint);
}

.tooltip-anchor:hover,
.tooltip-anchor:focus-visible { border-bottom-style: solid; }

.tooltip {
  position: absolute;
  z-index: var(--z-toast);
  inset-inline-start: 0;
  bottom: calc(100% + var(--sp-2));
  inline-size: max-content;
  max-inline-size: 20rem;
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--stroke-strong);
  border-radius: var(--r);
  background: var(--bg-elevated);
  box-shadow: var(--shadow-md);
  color: var(--ink);
  font-size: var(--text-sm);
  font-weight: 400;
  line-height: 1.4;
  text-align: start;
  white-space: normal;
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--dur-fast) var(--ease);
}

.tooltip-anchor:hover .tooltip,
.tooltip-anchor:focus-visible .tooltip,
.tooltip-anchor:focus-within .tooltip {
  opacity: 1;
  visibility: visible;
}

/* Escape has to dismiss it — otherwise a keyboard user opens a bubble that
   covers the next control and cannot close it without tabbing through it. */
.tooltip-anchor:focus-visible .tooltip { pointer-events: auto; }

@media (prefers-reduced-motion: reduce) {
  .tooltip { transition: none; }
}

/* --- Pagination -------------------------------------------------
   Every interactive element is 44px on the coarse-pointer floor declared in
   styles.css, not the 28px control height: this is a pager, which in the field
   is the control a gloved thumb hits. The current position is a labelled
   <output>-ish span, not a bare highlighted number — "3 of 12" is the fact;
   "3" alone is an orphan. */
.pagination {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
  margin-top: var(--sp-3);
}

.pagination-list {
  display: flex;
  align-items: center;
  gap: var(--sp-1);
  margin: 0;
  padding: 0;
  list-style: none;
}

.pagination-item { display: inline-flex; }

.pagination-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: var(--tap-target-min);
  min-block-size: var(--tap-target-min);
  padding: 0 var(--sp-2);
  border: 1px solid var(--stroke);
  border-radius: var(--r);
  background: var(--bg-elevated);
  color: var(--ink);
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}

.pagination-link:hover { background: var(--surface); }
.pagination-link:focus-visible { outline: var(--focus-ring-width) solid var(--focus-ring); outline-offset: 2px; }

.pagination-link[aria-current="page"] {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--on-brand);
  font-weight: 700;
}

/* aria-disabled rather than `disabled`: a native disabled button is removed
   from the tab order, so a keyboard user tabbing past the pager never learns
   that there are no more pages. Kept focusable and announced as unavailable. */
.pagination-link[aria-disabled="true"],
.pagination-link:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  pointer-events: none;
}

.pagination-status {
  margin-inline-start: auto;
  color: var(--ink-muted);
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
}

@media (max-width: 640px) {
  .pagination-status { margin-inline-start: 0; inline-size: 100%; }
}

/* --- Card -------------------------------------------------------
   Six surfaces each spelled a card: .kpi-tile (co, districts), .delta-card
   (portal), .rule-card, .panel, .card-* in focal-point. Same box, different
   padding and border-radius. This is the shared base they can be expressed in
   terms of; the call sites are left alone because their files are owned
   elsewhere and a half-migrated class list is worse than a duplicated one.

   .rule-card and .panel keep their own rules deliberately — migrating them is a
   one-line class swap once their surfaces are open. */
.card {
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--stroke);
  border-radius: var(--r);
  background: var(--bg-elevated);
}

.card-interactive { cursor: pointer; }
.card-interactive:hover { background: var(--surface); }
.card-interactive:focus-within { border-color: var(--brand); }

/* --- KPI tile ----------------------------------------------------
   `.kpi-tile` was defined inline in co/ and districts/ with the same box and
   different padding, so the same figure looked like it belonged to two
   products. `.card` above is the shared base; these are the metric-specific
   parts, which were duplicated verbatim between the two surfaces and are
   promoted here because both of those surfaces are now maintained in this
   partition.

   `min-height: 44px` on the interactive retry button is the SC 2.5.8 target
   floor, and it is the reason this rule exists as a component rather than a
   style: a button that is 4px too short fails on every surface it is
   duplicated onto. */
.kpi-tile {
  background: var(--bg-elevated);
  border: 1px solid var(--stroke);
  border-radius: var(--r);
  padding: var(--sp-3) var(--sp-4);
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
}

.kpi-tile-label,
.kpi-tile-unit {
  font-size: var(--text-xs);
  color: var(--ink-muted);
}

.kpi-tile-value {
  font-size: var(--text-h2);
  font-weight: 700;
  color: var(--ink);
  line-height: 1.1;
}

.kpi-tile-annotation {
  font-size: var(--text-xs);
  color: var(--brand);
  margin-top: var(--sp-1);
}

.retry-btn {
  margin-top: var(--sp-4);
  padding: var(--sp-2) var(--sp-4);
  font: inherit;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--ink-muted);
  border-radius: 6px;
  cursor: pointer;
  /* WCAG 2.2 SC 2.5.8. This button was duplicated into two surfaces, each
     carrying the floor, which is exactly the arrangement that produces a
     third copy that forgets it. */
  min-height: 44px;
}

.retry-btn:hover { background: var(--surface-hover); }

.card-title {
  margin: 0;
  font-size: var(--text-body);
  font-weight: 600;
  color: var(--ink);
}

.card-value {
  display: block;
  font-size: var(--text-h2);
  font-weight: 700;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.card-meta {
  margin-top: var(--sp-1);
  color: var(--ink-muted);
  font-size: var(--text-sm);
}

/* --- Data table -------------------------------------------------
   The base .table-wrap table styles, named. Kept alongside the descendant
   selectors rather than replacing them, so a <table> that predates this class
   keeps its layout and a new one can opt in by name.

   Row striping is left out: a zebra pattern is a second visual channel for the
   same information, and on the 9:1 dark surface it is the first thing to cost
   contrast. The sticky header already carries the structure. */
.data-table {
  inline-size: 100%;
  border-collapse: collapse;
  font-size: var(--text-sm);
}

.data-table caption {
  padding: var(--sp-2) var(--sp-3);
  color: var(--ink-muted);
  font-size: var(--text-xs);
  text-align: start;
}

.data-table th,
.data-table td {
  padding: var(--sp-2) var(--sp-3);
  text-align: start;
  border-bottom: 1px solid var(--stroke);
  vertical-align: top;
}

.data-table thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--surface);
  color: var(--ink-muted);
  font-weight: 600;
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  white-space: nowrap;
}

.data-table tbody tr:last-child td { border-bottom: none; }
.data-table tbody tr:hover { background: var(--surface); }
.data-table td.num-cell { white-space: nowrap; }

/* --- Charts -----------------------------------------------------
   The eight surfaces had four unrelated graphics implementations between them:
   a <polyline> in co/, a CSS-<div> bar chart in co/, two pixel-height <div>s
   in scenarios/, and 39 svgEl() calls in the situation map. These are the rules
   for the one shared set in public/shared/charts.js.

   The SVG is `aria-hidden` and always ships a `.chart-table` sibling, so nothing
   here is load-bearing for a screen reader. What is load-bearing is contrast:
   --ink-muted on --bg for tick labels, --stroke for grid lines. Grid lines sit
   below the 4.5:1 text floor deliberately — they are a scale reference, not a
   value, and a chart whose grid competes with its data is worse than one
   without. */
.chart {
  display: block;
  inline-size: 100%;
  block-size: auto;
  color: var(--brand);
  overflow: visible;
}

.chart-tick {
  font-family: var(--font-sans);
  font-size: var(--text-xs);
}

.chart-grid line { stroke-linecap: square; }
.chart-band { pointer-events: none; }
.chart-cell { shape-rendering: crispEdges; }

/* A missing cell is a dashed outline. It must not be the lowest colour in the
   ramp, which would say "the driest thing on this map". */
.chart-cell-missing { opacity: 0.7; }

/* The accessible table is not decoration; it is the chart for anyone who cannot
   see the chart. Collapsed to a caption-sized summary by default and expanded
   on focus or hover so it does not push the page around. */
.chart-table {
  margin-block-start: var(--sp-2);
  overflow-x: auto;
}

.chart-table table {
  inline-size: 100%;
  border-collapse: collapse;
  font-size: var(--text-xs);
  color: var(--ink-muted);
}

.chart-table caption {
  text-align: start;
  color: var(--ink);
  font-weight: 600;
  padding-block-end: var(--sp-1);
}

.chart-table th,
.chart-table td {
  text-align: start;
  padding: var(--sp-1) var(--sp-2);
  border-block-end: 1px solid var(--stroke);
  white-space: nowrap;
}

.chart-table .num {
  text-align: end;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}

.chart-grid {
  display: grid;
  grid-template-columns: repeat(var(--chart-cols, 3), minmax(0, 1fr));
  gap: var(--sp-4);
}

/* A facet with no chart is still a facet: "Refused: 40 months of 60" is
   actionable and a blank tile is not, which is why smallMultiples renders the
   panel rather than dropping it. */
.chart-panel {
  margin: 0;
  padding: var(--sp-3);
  background: var(--bg-elevated);
  border: 1px solid var(--stroke);
  border-radius: var(--r);
  min-inline-size: 0;
}

.chart-panel-title {
  font-weight: 600;
  font-size: var(--text-sm);
  margin-block-end: var(--sp-2);
}

.chart-panel-note,
.chart-panel-refused {
  margin: var(--sp-2) 0 0;
  font-size: var(--text-xs);
  color: var(--ink-faint);
}

.chart-panel-refused { color: var(--warn); }

/* A chart is 100% wide but not infinitely tall — a 480x180 viewBox stretched
   across a 1200px rail renders its 10px type at 25px and its bars as slabs. */
@media (min-width: 900px) {
  .chart-grid { grid-template-columns: repeat(var(--chart-cols, 2), minmax(0, 1fr)); }
}

/* --- Print ------------------------------------------------------
   styles.css owns the token inversion and the page-level print block; only the
   component-level rules belong here. Printing a component costs paper and
   communicates nothing: a pager prints as "1 2 3 of page 7", a skeleton prints
   as a grey box the reader waits for, a spinner prints as a third of a ring,
   and a tooltip prints as a detached paragraph of small print. */
@media print {
  .spinner,
  .skeleton,
  .toast,
  .tooltip,
  .pagination { display: none !important; }

  /* Cards lose their lift — a border plus a tint prints, a background wash on
     a 4-colour inkjet does not. */
  .card,
  .data-table thead th { background: none; }

  /* A sticky header on paper prints once per page, floating mid-table. */
  .data-table thead th { position: static; }

  /* Rows must not split across a page boundary: half a district row is worse
     than a slightly longer document. */
  .data-table tr { break-inside: avoid; }
  .data-table thead { display: table-header-group; }
}
