/* =============================================================
   Lindela Lite — Operations Console
   OKLCH dark-first design system
   ============================================================= */

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

/* --- Reset --------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

html { height: 100%; -webkit-text-size-adjust: 100%; }

body {
  min-height: 100vh;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: var(--text-body);
  line-height: 1.5;
  display: flex;
  flex-direction: column;
}

/* The scroll container every page needs.
   This rule did not exist and `body` carried `overflow: hidden`, so the two
   surfaces that link this sheet but have no inner scroller — parametric and
   scenarios — clipped their own tables: the parametric Disbursement History
   lost its last column off the right edge with no scrollbar and no way to
   reach it. Every other page escaped this only by not linking the sheet. */
main {
  flex: 1 1 auto;
  min-height: 0;
  min-width: 0;
  overflow-y: auto;
  overflow-x: hidden;
  padding: var(--sp-5);
}

h1, h2, h3 {
  text-wrap: balance;
  line-height: 1.2;
}

h1 { font-size: var(--text-h1); font-weight: 700; letter-spacing: -0.03em; }
h2 { font-size: var(--text-h2); font-weight: 600; letter-spacing: -0.01em; }
h3 { font-size: var(--text-xs); font-weight: 600; color: var(--ink-muted); letter-spacing: 0; }

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

/* Visually hidden, still announced.
   Used three times in co/index.html and defined nowhere, so the labels it was
   written for — "Quarter", "Year", "Locale" — rendered as visible text beside
   their selects. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Skip to content.
   There was no skip link and no <main> in the console, so a keyboard user
   tabbed through roughly thirty map filters, simulations and route inputs
   before reaching the rail that holds the actual alerts. */
.skip-link {
  position: absolute;
  left: var(--sp-2);
  top: -100%;
  z-index: var(--z-toast);
  padding: var(--sp-2) var(--sp-3);
  background: var(--brand);
  color: var(--on-brand);
  border-radius: var(--r);
  font-weight: 600;
  transition: top var(--dur-fast) var(--ease);
}
.skip-link:focus { top: var(--sp-2); text-decoration: none; }

/* --- Focus -----------------------------------------------------
   There were no :focus-visible rules anywhere in the product. The only focus
   styling was `input:focus, select:focus, textarea:focus`, which fires on mouse
   click — the exact behaviour :focus-visible exists to avoid — and covered no
   interactive component at all: not .btn, .chip, .rail-tab, a, or summary. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: var(--focus-ring-width) solid var(--focus-ring);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}

/* Field rings sit inside the control so a dense filter bar does not shift. */
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
  outline: var(--focus-ring-width) solid var(--focus-ring);
  outline-offset: -1px;
  border-color: var(--brand);
}

/* Keyboard users must always be able to see where they are, including on the
   surfaces that were previously outline-less. */
[tabindex="-1"]:focus-visible { outline-offset: -2px; }

code {
  font-family: var(--font-mono);
  font-size: 0.875em;
  background: var(--surface);
  border: 1px solid var(--stroke);
  border-radius: var(--r-sm);
  padding: 0.1em 0.3em;
}

kbd {
  font-family: var(--font-mono);
  font-size: 0.8125em;
  color: var(--ink-muted);
  background: var(--surface);
  border: 1px solid var(--stroke-strong);
  border-radius: var(--r-sm);
  padding: 0.1em 0.4em;
}

/* --- Offline banner ------------------------------------------
   `display: flex` below is what shipped the bug this rule exists to prevent: at
   (0,1,0) it outranked the user agent's `[hidden] { display: none }` at the same
   specificity, so the amber bar — the loudest thing on any page, fixed at
   z-index 1000 above the navbar — rendered on every load, online or not. It was
   not a rare edge case and it was not obvious: the markup carries `hidden`, and
   every element inspector agrees `hidden` is set. components.css now closes the
   whole class with one `!important`; this rule stays because the layout is
   still wanted, and the guard below keeps it honest if that rule is ever
   dropped. Unhiding is done by removing the attribute, never by writing
   `style.display` — see components.css. */
.offline-banner {
  position: fixed;
  inset: 0 0 auto;
  /* Above the shared navbar, not below it.
     The navbar is fixed at z-index 999 and sits in the same 46px band, so the
     offline banner rendered underneath it and was invisible on every page that
     carries both. Proved by hiding the navbar: the banner appeared immediately.
     The field team was told nothing at exactly the moment they most needed
     telling. */
  z-index: 1000;
  background: var(--warn);
  /* --on-warn, not a literal. --warn is a light amber in the dark theme and a
     dark brown in the light one, so ink that reads on the first is invisible on
     the second — axe measured 2.73:1 here in light, which is the offline
     warning the field team most needs to read. The token already flips. */
  color: var(--on-warn);
  font-size: var(--text-xs);
  font-weight: 600;
  text-align: center;
  padding: var(--sp-2) var(--sp-4);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-3);
}

.offline-banner[hidden] { display: none; }

/* The auth variant of the same banner. It reuses --warn because the operator's
   next action is the same in both cases — open Settings — but it must not read
   as "your work is saved and will sync", which is what the offline wording says.
   A dashed inset rule distinguishes it without introducing a second palette
   token, and the wording carries the difference. */
.offline-banner.banner-auth {
  background: var(--warn);
  box-shadow: inset 0 -2px 0 0 var(--ink);
}

.offline-banner.banner-auth strong { font-weight: 700; }

.queued-badge {
  background: oklch(14% 0.02 90 / 0.2);
  border-radius: var(--r-sm);
  padding: 1px 6px;
}

/* --- Topbar -------------------------------------------------- */
.topbar {
  position: sticky;
  top: 0;
  z-index: var(--z-topbar);
  height: 48px;
  flex-shrink: 0;
  background: var(--bg-elevated);
  border-bottom: 1px solid var(--stroke);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  padding: 0 var(--sp-5);
}

.topbar-brand {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

.brand-icon {
  width: 22px;
  height: 22px;
  color: var(--brand);
  flex-shrink: 0;
}

.brand-name {
  font-size: 0.9375rem;
  font-weight: 700;
  color: var(--ink);
  letter-spacing: -0.02em;
}

.topbar-controls {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

/* --- Badges -------------------------------------------------- */
.badge {
  font-size: var(--text-xs);
  font-weight: 500;
  font-family: var(--font-mono);
  padding: 2px 7px;
  border-radius: var(--r-sm);
  white-space: nowrap;
}

.badge-storage {
  background: var(--surface);
  color: var(--ink-muted);
  border: 1px solid var(--stroke);
}

.badge-connection {
  background: color-mix(in oklch, var(--ok) 14%, transparent);
  color: var(--ok);
}

.badge-connection.offline {
  background: color-mix(in oklch, var(--danger) 14%, transparent);
  color: var(--danger);
}

/* --- Buttons ------------------------------------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-1);
  padding: 0.35rem 0.7rem;
  font-size: var(--text-sm);
  font-family: var(--font-sans);
  font-weight: 500;
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--stroke-strong);
  border-radius: var(--r);
  cursor: pointer;
  white-space: nowrap;
  text-decoration: none;
  transition:
    background var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease),
    color var(--dur-fast) var(--ease);
}

.btn:hover  { background: var(--surface-hover); }
.btn:active { background: var(--bg-elevated); }
.btn:disabled { opacity: 0.38; cursor: not-allowed; pointer-events: none; }

.btn-primary {
  background: var(--brand);
  color: var(--on-brand);
  border-color: var(--brand);
  font-weight: 600;
}

.btn-primary:hover { background: var(--brand-hover); border-color: var(--brand-hover); }

/* `.btn-secondary` was used in five places and defined nowhere, so those
   controls rendered as bare UA-default buttons. It is the quiet counterpart to
   .btn-primary: a visible boundary, no fill, no lift. */
.btn-secondary {
  background: transparent;
  color: var(--ink-muted);
  border-color: var(--stroke-strong);
  font-weight: 500;
}
.btn-secondary:hover { background: var(--surface); color: var(--ink); }

/* A variant used without `.btn` never received padding, border or cursor, so
   the UA default `border: 2px outset ButtonBorder` showed through. Give the
   colour-only variants a full standalone appearance as well, so a component
   author who writes `class="btn-primary"` alone gets a real button. */
.btn-primary, .btn-secondary, .btn-approve, .btn-reject, .btn-send {
  padding: 0.35rem 0.7rem;
  font-size: var(--text-sm);
  font-family: var(--font-sans);
  border-width: 1px;
  border-style: solid;
  border-radius: var(--r);
  cursor: pointer;
  transition:
    background var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease),
    color var(--dur-fast) var(--ease);
}

.btn-icon {
  padding: 0.35rem;
  background: transparent;
  border-color: transparent;
  color: var(--ink-muted);
}

.btn-icon:hover { background: var(--surface); color: var(--ink); border-color: transparent; }

.btn-sm  { padding: 0.2rem 0.5rem; font-size: var(--text-xs); }
.btn-xs  { padding: 0.1rem 0.4rem; font-size: calc(var(--text-xs) * 0.9); border-radius: var(--r-sm); }

.btn-approve { color: var(--ok);     border-color: color-mix(in oklch, var(--ok) 35%, transparent); }
.btn-approve:hover { background: color-mix(in oklch, var(--ok) 10%, transparent); }

.btn-reject  { color: var(--danger); border-color: color-mix(in oklch, var(--danger) 35%, transparent); }
.btn-reject:hover { background: color-mix(in oklch, var(--danger) 10%, transparent); }

.btn-send    { color: var(--brand);  border-color: color-mix(in oklch, var(--brand) 35%, transparent); }
.btn-send:hover { background: color-mix(in oklch, var(--brand) 10%, transparent); }

/* Locale select */
#locale-select {
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--stroke-strong);
  border-radius: var(--r);
  padding: 0.2rem 0.4rem;
  font-size: var(--text-xs);
  font-family: var(--font-sans);
  cursor: pointer;
  /* No fixed min-height here: an ID-scoped one silently outranked the shared
     tap-target floor and pinned the language picker to 28px on every surface. */
  min-height: var(--size-control);
}

/* --- Console layout ------------------------------------------ */
/* The console is a fixed app shell: map beside rail, rail scrolls, page does
   not. Opted into per-page rather than assumed, because the other surfaces that
   link this sheet need the page itself to scroll. */
body.app-shell {
  height: 100vh;
  overflow: hidden;
}

.console-layout {
  display: grid;
  grid-template-columns: 1fr 360px;
  flex: 1;
  min-height: 0;
  overflow: hidden;
}

/* --- Action rail ------------------------------------------------
   The panel that answers "what did the platform do about it", which the map
   cannot. Sits below the map rather than in the right-hand rail because it is
   read alongside the map's own record strip: an operator comparing an execution
   against the hazard that caused it needs both in one glance, and the rail is a
   column of tabs far to the right. */
.action-rail {
  border-top: 1px solid var(--stroke);
  padding: var(--sp-4) var(--sp-5);
  background: var(--bg);
}

.action-rail-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  flex-wrap: wrap;
  margin-bottom: var(--sp-3);
}

.action-rail-chips {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
}

.rail-chip {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  padding: 2px var(--sp-2);
  border-radius: var(--r-sm);
  background: var(--surface);
  border: 1px solid var(--stroke);
  color: var(--ink-muted);
  /* Tabular figures because these are counts, and a count that re-flows as its
     digits change is harder to read than one that does not. */
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* A chip carrying a refusal wears the warning ground rather than the neutral
   one. "No figure" and "no number" are different states, and the difference is
   the whole point of the tile. */
.rail-chip.is-refused {
  background: var(--warn-wash);
  color: var(--warn-ink);
  border-color: var(--warn);
}

.action-rail-body {
  display: grid;
  grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
  gap: var(--sp-5);
  align-items: start;
}

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

.rail-table th {
  text-align: left;
  font-weight: 600;
  color: var(--ink-faint);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  padding: var(--sp-1) var(--sp-2);
  border-bottom: 1px solid var(--stroke);
}

.rail-table td {
  padding: var(--sp-2);
  border-bottom: 1px solid var(--stroke);
  vertical-align: top;
}

.rail-table td.is-mono {
  font-family: var(--font-mono);
  color: var(--ink-muted);
}

.rail-feed {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

.rail-feed li {
  font-size: var(--text-xs);
  padding: var(--sp-2);
  border: 1px solid var(--stroke);
  border-radius: var(--r-sm);
  background: var(--surface);
}

.rail-feed-meta {
  color: var(--ink-muted);
  font-family: var(--font-mono);
}

/* Status is carried by a word as well as a colour. A badge that only changes
   hue is unreadable to a colour-blind operator and invisible to a screen reader,
   and this panel's entire content is status. */
.rail-status-badge { font-weight: 700; }
.rail-status-badge[data-status='executed'] { color: var(--ok); }
.rail-status-badge[data-status='partial']  { color: var(--warn-ink); }
.rail-status-badge[data-status='refused']  { color: var(--danger); }
.rail-status-badge[data-status='shadow']   { color: var(--ink-muted); }

.rail-action-chip {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  padding: 1px var(--sp-2);
  margin-left: var(--sp-1);
  border-radius: var(--r-sm);
  border: 1px solid var(--stroke);
  color: var(--ink-muted);
  display: inline-block;
}

/* A refused step keeps its reason on hover AND as the chip's accessible name.
   The title attribute alone would hide the sentence from a keyboard or screen
   reader user, which is the exact audience that cannot see the colour. */
.rail-action-chip[data-status='refused'] {
  background: var(--warn-wash);
  color: var(--warn-ink);
  border-color: var(--warn);
  cursor: help;
}

.action-rail-status {
  margin: var(--sp-3) 0 0;
  font-size: var(--text-xs);
  color: var(--ink-muted);
  min-height: 1.2em;
}

@media (max-width: 900px) {
  .action-rail-body { grid-template-columns: minmax(0, 1fr); }
}

/* --- Map section --------------------------------------------- */
.map-section {
  position: relative;
  z-index: var(--z-map);
  display: flex;
  flex-direction: column;
  min-height: 0;
  /* Scroll rather than clip: the map now has a floor, so on a short window the
     panels below it can exceed the row. Clipped silently is how the routing
     panel disappeared on small screens. */
  overflow-y: auto;
  background: var(--bg);
  border-right: 1px solid var(--stroke);
}

.map-container {
  flex: 1;
  position: relative;
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
}

.situation-map {
  /*
   * min-height is load-bearing, not cosmetic. .map-container is a flex column
   * that also holds the seasonal strip, the filters, the flood controls and the
   * routing panel. With only `flex: 1` the map took whatever those left over:
   * on a 1440x900 laptop that was 122px, so an 800x500 viewBox scaled to 0.24
   * and rendered the map at 195x122 with 82% of the width wasted and legend text
   * at 2.4px. Every DOM check passed, because the cells were present in the
   * document — they were simply invisible.
   */
  flex: 1;
  min-height: 480px;
  width: 100%;
  height: 100%;
  cursor: grab;
  user-select: none;
  touch-action: none;
  display: block;
}

.situation-map:active { cursor: grabbing; }

/* Map SVG internals */
.graticule line {
  stroke: var(--stroke-strong);
  stroke-width: 0.5;
  opacity: 0.4;
}

.graticule text {
  fill: var(--ink-faint);
  font-size: 9px;
  font-family: var(--font-mono);
}

.hazard-marker {
  fill-opacity: 0.75;
  stroke-width: 1.5;
  cursor: pointer;
  transition: fill-opacity var(--dur-fast) var(--ease), r 80ms var(--ease);
}

.hazard-marker:hover { fill-opacity: 1; }

.hazard-flood    { fill: oklch(60% 0.20 220); stroke: oklch(72% 0.20 220); }
.hazard-landslide { fill: oklch(52% 0.16 55);  stroke: oklch(66% 0.16 55); }
.hazard-storm    { fill: oklch(65% 0.18 195); stroke: oklch(75% 0.18 195); }
.hazard-fire     { fill: var(--sev-critical); stroke: var(--sev-high); }
.hazard-disaster { fill: var(--sev-high);     stroke: var(--sev-medium); }
.hazard-conflict { fill: oklch(62% 0.17 300); stroke: oklch(72% 0.17 300); }
.hazard-default  { fill: oklch(62% 0.12 260); stroke: oklch(72% 0.12 260); }

.asset-marker {
  fill-opacity: 0.88;
  stroke-width: 1;
  cursor: pointer;
}

.asset-health    { fill: var(--ok);           stroke: oklch(60% 0.16 155); }
.asset-water     { fill: oklch(62% 0.20 220); stroke: oklch(52% 0.20 220); }
.asset-education { fill: var(--sev-medium);   stroke: var(--warn); }
.asset-default   { fill: var(--ink-muted);    stroke: var(--stroke-strong); }

.risk-blob { pointer-events: none; }

/* Flood depth shading. Sequential blues, deliberately not a rainbow: depth is
   ordinal, and a hue ramp would imply a meaningful "red" that is just
   "deeper". Deeper water is darker and more saturated.

   Opacity is raised when a simulation is active because the risk gradients
   underneath are wide and saturated; at 0.30–0.78 the shallow bands vanished
   into them entirely. */
.flood-layer { pointer-events: none; }

.flood-cell {
  stroke: none;
  shape-rendering: crispEdges;
}

.flood-d0 { fill: oklch(72% 0.10 230); fill-opacity: 0.45; }
.flood-d1 { fill: oklch(64% 0.14 232); fill-opacity: 0.58; }
.flood-d2 { fill: oklch(55% 0.18 236); fill-opacity: 0.70; }
.flood-d3 { fill: oklch(46% 0.20 242); fill-opacity: 0.80; }
.flood-d4 { fill: oklch(34% 0.22 250); fill-opacity: 0.90; }

/*
 * IPC area boxes. Bands of the *published* Phase 3+ fraction — presentation
 * thresholds, not a re-classification of IPC's phases. Dashed edges and flat
 * fills because a rectangle includes neighbouring ground the classification
 * does not cover; solid footprints are reserved for geometry the source drew.
 */
.food-cell {
  fill-opacity: 0.22;
  stroke-opacity: 0.55;
  stroke-width: 1.2;
  stroke-dasharray: 5 3;
}
.food-layer { pointer-events: auto; }

.food-low     { fill: oklch(80% 0.11 145); stroke: oklch(58% 0.14 145); }
.food-medium  { fill: oklch(82% 0.13 85);  stroke: oklch(60% 0.14 85); }
.food-high    { fill: oklch(75% 0.15 55);  stroke: oklch(56% 0.16 55); }
.food-severe  { fill: oklch(58% 0.20 25);  stroke: oklch(45% 0.20 25); }
.food-unclassified { fill: none; stroke: oklch(55% 0.02 250); stroke-opacity: 0.35; }

/*
 * Weather glyphs. The rain band is the sequential blue ramp the flood layer
 * already taught (same variable, same direction: wetter is darker), and the
 * size and dash pattern come from the severity vocabulary — colour is never
 * the only channel, per the WEB-04/WEB-10 note above the marker styles.
 * `ungraded` is a district whose total was not reported, drawn hollow rather
 * than in the dry band's fill, because "no reading" is not "no rain".
 */
.weather-marker {
  stroke-width: 1.5;
  fill-opacity: 0.8;
}

.weather-dry      { fill: none; stroke: var(--ink-muted); }
.weather-light    { fill: oklch(72% 0.10 230); stroke: oklch(60% 0.12 230); }
.weather-moderate { fill: oklch(64% 0.14 232); stroke: oklch(52% 0.16 232); }
.weather-heavy    { fill: oklch(55% 0.18 236); stroke: oklch(42% 0.20 238); }
.weather-extreme  { fill: oklch(42% 0.20 244); stroke: oklch(30% 0.22 248); }
.weather-ungraded { fill: none; stroke: var(--ink-muted); stroke-opacity: 0.6; }

/* River discharge markers reuse the severity radius/dash vocabulary; colour is
   the band, with the same low→critical ramp so a high or extreme discharge
   pops against the basemap without inventing a new visual language. */
.discharge-marker {
  stroke-width: 1.5;
  fill-opacity: 0.85;
}

.discharge-marker:hover { fill-opacity: 1; }

.discharge-low      { fill: oklch(70% 0.12 145); stroke: oklch(58% 0.14 145); }
.discharge-moderate { fill: oklch(64% 0.14 232); stroke: oklch(52% 0.16 232); }
.discharge-high     { fill: oklch(55% 0.18 85);  stroke: oklch(42% 0.20 85); }
.discharge-extreme  { fill: var(--sev-critical); stroke: oklch(30% 0.22 25); }
.discharge-unknown  { fill: none; stroke: var(--ink-muted); stroke-opacity: 0.6; }

/* Disease-outbreak markers (ADR-014). One shape and hue per disease in the
   vocabulary; national aggregates are dashed *and* washed out so the
   country-centroid placement cannot pass for district evidence. */
.disease-marker { stroke-width: 1.5; fill-opacity: 0.85; }
.disease-marker:hover { fill-opacity: 1; }
.disease-cholera      { fill: oklch(65% 0.18 145); stroke: oklch(50% 0.20 145); }
.disease-measles      { fill: oklch(65% 0.18 30);  stroke: oklch(50% 0.20 30); }
.disease-meningitis   { fill: oklch(65% 0.18 285); stroke: oklch(50% 0.20 285); }
.disease-yellow-fever { fill: oklch(65% 0.18 85);  stroke: oklch(50% 0.20 85); }
.disease-plague       { fill: oklch(65% 0.18 320); stroke: oklch(50% 0.20 320); }
.disease-other        { fill: oklch(60% 0.05 0);   stroke: var(--ink-muted); }
.disease-national     { fill-opacity: 0.4; }
.disease-hit { cursor: pointer; }

/* --- Sensor pins ------------------------------------------------
   Filled shapes on a white halo rather than outlines: a sensor is a small
   instrument, not a hazard, and an outline at this size reads as an empty circle
   the operator cannot tell from a map feature. The halo is what keeps a pin
   legible over a flood extent. */
.sensor-marker {
  stroke: var(--bg);
  stroke-width: 2;
  fill-opacity: 0.95;
}
.sensor-cold_chain  { fill: oklch(60% 0.19 30); }
.sensor-humidity    { fill: oklch(62% 0.14 220); }
.sensor-flood_gauge { fill: oklch(58% 0.16 245); }
.sensor-heat        { fill: oklch(65% 0.18 45); }
.sensor-water_point { fill: oklch(58% 0.13 195); }
.sensor-other       { fill: oklch(55% 0.05 0); }

/* A sensor under 20% battery is drawn hollow. It is the one state here where a
   missing reading is the finding — the instrument has stopped reporting, and an
   operator reading a stale "8°C" needs to know before they act on it. The state
   is also named in the pin's label and title, so it never rests on the fill. */
.sensor-low-battery {
  fill-opacity: 0.25;
  stroke-dasharray: 2 1.5;
  stroke: oklch(50% 0.19 30);
}
.sensor-hit { cursor: pointer; }

/*
 * Constant-size point markers. The map zooms by scaling `#mapTransform`, which
 * would balloon markers at district zoom and shrink them to nothing at the
 * Africa zoom-out. Point markers and their labels are not geography — like
 * every mainstream slippy map they keep a constant screen size — so each is
 * counter-scaled by the inverse of the live zoom scale (`--map-scale`, set on
 * #situationMap by applyMapTransform in app.js). Geographic layers (tiles,
 * country rings, district rings, risk blobs, flood cells, roads) still scale:
 * they represent extent, not points. transform-box/origin make each marker
 * shrink around its own centre so its anchor stays glued to the geography.
 * Rules are scoped to #mapTransform so the legend swatches, which reuse these
 * marker classes but live outside the transform, keep their designed size.
 */
#mapTransform .hazard-marker,
#mapTransform .hazard-hit,
#mapTransform .asset-marker,
#mapTransform .asset-hit,
#mapTransform .discharge-marker,
#mapTransform .discharge-hit,
#mapTransform .disease-marker,
#mapTransform .disease-hit,
#mapTransform .sensor-marker,
#mapTransform .sensor-hit,
#mapTransform .districts-layer text,
#mapTransform .graticule text {
  transform-box: fill-box;
  transform-origin: center;
  transform: scale(calc(1 / var(--map-scale, 1)));
}

/*
 * Grouped glyphs whose label must stay attached — the weather dot + its
 * temperature, and the temporary click pin. Counter-scaling each element
 * around its own box would let an offset label drift away from its dot as the
 * zoom changes, so the whole group counter-scales around the anchor point
 * (--pin-x/--pin-y, set where the group is built).
 */
#mapTransform .map-pin {
  transform-box: view-box;
  transform-origin: var(--pin-x, 0px) var(--pin-y, 0px);
  transform: scale(calc(1 / var(--map-scale, 1)));
}

.weather-temp {
  font-size: 9px;
  font-family: var(--font-mono);
  fill: var(--ink-muted);
}

/* Heat stress is the one temperature an operator routes around, so it is the
   only band the label raises its voice for. */
.weather-temp-hot {
  fill: var(--sev-critical);
  font-weight: 600;
}

/* Road status: impassable must read instantly at map scale. */
.roads-layer { pointer-events: none; }

.road-marker {
  fill: var(--ok);
  fill-opacity: 0.9;
  stroke: none;
}

.road-restricted { fill: var(--sev-medium); }
.road-impassable {
  fill: var(--sev-critical);
  stroke: oklch(88% 0.10 25);
  stroke-width: 1;
}

.map-sim {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 8px;
  padding: 10px 12px;
  border: 1px solid var(--stroke);
  border-radius: 8px;
  background: var(--bg-elevated);
}

.map-sim-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 8px;
}

.map-sim-status {
  margin: 0;
  font-size: 11px;
  line-height: 1.45;
  color: var(--ink-muted);
}

.flood-legend {
  border-top: 1px solid var(--stroke);
  padding-top: 6px;
}

.flood-legend-list {
  display: grid;
  gap: 2px;
  margin: 4px 0;
  padding: 0;
  list-style: none;
}

.flood-legend-list li {
  display: grid;
  grid-template-columns: 14px 76px 1fr;
  align-items: center;
  gap: 6px;
  font-size: 10px;
  color: var(--ink-muted);
}

.flood-swatch {
  width: 14px;
  height: 10px;
  border-radius: 2px;
  display: inline-block;
}

.flood-band  { font-family: var(--font-mono); color: var(--ink); }
.flood-note  { color: var(--ink-faint); }
.legend-title {
  margin: 0;
  font-size: 11px;
  font-weight: 600;
  color: var(--ink);
}
.legend-note {
  margin: 2px 0 0;
  font-size: 9px;
  line-height: 1.4;
  color: var(--ink-faint);
}

@keyframes marker-pulse {
  0%, 100% { stroke-width: 2; stroke-opacity: 0.9; }
  50%       { stroke-width: 7; stroke-opacity: 0.3; }
}

.marker-selected { animation: marker-pulse 1.8s var(--ease) infinite; }

.map-legend { pointer-events: none; }

.legend-bg { fill: var(--bg-elevated); fill-opacity: 0.92; }

.legend-label {
  fill: var(--ink-muted);
  font-size: 10px;
  font-family: var(--font-sans);
  dominant-baseline: middle;
}

/* Map filters */
.map-filters {
  display: flex;
  align-items: center;
  /* Wrap rather than scroll. Three selects with labels do not fit a 414px
     viewport, and `nowrap` plus a shrinking flex row squeezed each select to
     14px — the width of its own dropdown arrow. The controls were present,
     focusable and inert. */
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-3);
  padding: var(--sp-2) var(--sp-4);
  background: var(--bg-elevated);
  border-top: 1px solid var(--stroke);
  flex-shrink: 0;
  z-index: var(--z-controls);
}

/* A select that cannot show its current value is worse than a wide one. */
.map-filters select { min-width: 7rem; }
.map-filters .filter-label { flex: 0 1 auto; }

.filter-label {
  display: flex;
  /* The global `label` rule sets flex-direction: column for stacked field
     labels. .filter-label re-declared display and alignment but not direction,
     so every checkbox label inherited the column and rendered the box on its
     own line above the caption. */
  flex-direction: row;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--ink-muted);
  white-space: nowrap;
}

.filter-label select {
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--stroke);
  border-radius: var(--r-sm);
  padding: 2px 6px;
  font-size: var(--text-xs);
  /* Same reason as #locale-select: a hard px floor here outranked the shared
     tap-target rule and left the map filters at 24px on a phone. */
}

.map-zoom-controls button {
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--stroke);
  border-radius: var(--r-sm);
  width: 1.75rem;
  height: 1.5rem;
  cursor: pointer;
  font-weight: 700;
  font-size: var(--text-sm);
}
.map-zoom-controls button:hover { background: var(--surface-2); }
.map-zoom-controls button:active { transform: translateY(1px); }
.map-zoom-controls #zoomResetBtn { font-size: 0.85rem; }
.map-zoom-controls #zoomLevel {
  display: inline-block;
  min-width: 2.75rem;
  text-align: center;
  font-variant-numeric: tabular-nums;
  font-size: var(--text-xs);
}

.map-record-count {
  font-size: var(--text-xs);
  color: var(--ink-faint);
  font-family: var(--font-mono);
  margin-left: auto;
  white-space: nowrap;
}

/* --- Rail ---------------------------------------------------- */
.rail {
  z-index: var(--z-rail);
  display: flex;
  flex-direction: column;
  background: var(--bg-elevated);
  min-height: 0;
  overflow: hidden;
}

/* Scrollable, so a rail with more tabs than the width can hold scrolls to them
   instead of hanging off the edge. `flex: 1` below distributes the free space
   between tabs; `flex-shrink: 0` here keeps the labels from being squeezed to
   nothing first, which is what put the overflow outside the viewport. */
.rail-tabs {
  display: flex;
  border-bottom: 1px solid var(--stroke);
  flex-shrink: 0;
  overflow-x: auto;
  scrollbar-width: thin;
}

.rail-tab {
  flex: 1 0 auto;
  white-space: nowrap;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-1);
  padding: var(--sp-3) var(--sp-2);
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--ink-muted);
  background: transparent;
  border: none;
  border-bottom: 2px solid transparent;
  cursor: pointer;
  transition:
    color var(--dur-normal) var(--ease),
    border-color var(--dur-normal) var(--ease),
    background var(--dur-fast) var(--ease);
  white-space: nowrap;
}

.rail-tab:hover { color: var(--ink); background: var(--surface-hover); }

.rail-tab.active,
.rail-tab[aria-selected="true"] {
  color: var(--brand);
  border-bottom-color: var(--brand);
}

.tab-badge {
  /* Measured at 3.2:1 with near-white ink at 10px — under the 4.5:1 AA floor.
     The badge carries an alert COUNT, so the one thing it cannot afford is
     being the least legible text on the page. Deepened fill with body ink. */
  background: color-mix(in oklch, var(--danger) 30%, var(--bg));
  color: var(--ink);
  border: 1px solid var(--danger);
  font-size: var(--text-xs);
  font-weight: 700;
  font-family: var(--font-mono);
  min-width: 15px;
  height: 15px;
  border-radius: 8px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 3px;
}

/* Rail panels */
.rail-content {
  flex: 1;
  min-height: 0;
  overflow: hidden;
  position: relative;
}

/* Rail panels.
   Two visibility mechanisms used to fight each other and both lost. `hidden`
   (set by `selectTab`) is defeated by the `display: flex` below; `.active` is
   the opacity gate, which is (0,2,0) and wins on specificity. So an inactive
   panel computed `display: flex` at `opacity: 0` — laid out, scrollable,
   `position: absolute; inset: 0` on top of the live panel, holding 58
   keyboard-focusable controls at coordinates identical to the visible panel's.
   Tab walked into them and nothing was on screen to indicate it. 58 of the
   console's 99 boot-time controls were in that state.

   `hidden` is now the sole mechanism and the `opacity` pair is decorative: the
   cross-fade still runs on the active panel, but nothing is focusable in a
   panel that is not the selected one. The guard below is redundant with
   components.css's `[hidden] { display: none !important }` and deliberately so
   — this is the second site of the class, and a reader arriving here should not
   have to go looking for the reason `display` is safe. */
.rail-panel {
  position: absolute;
  inset: 0;
  overflow-y: auto;
  overflow-x: hidden;
  display: flex;
  flex-direction: column;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-normal) var(--ease);
}

.rail-panel[hidden] { display: none; }

.rail-panel.active {
  opacity: 1;
  pointer-events: auto;
  position: relative;
}

.panel-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  padding: var(--sp-4) var(--sp-4) var(--sp-2);
  flex-shrink: 0;
  gap: var(--sp-3);
  flex-wrap: wrap;
}

/* Filter chips */
.filter-chips {
  display: flex;
  gap: var(--sp-1);
  flex-wrap: wrap;
}

.chip {
  padding: 2px 8px;
  font-size: var(--text-xs);
  font-weight: 500;
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink-muted);
  border: 1px solid var(--stroke);
  cursor: pointer;
  transition:
    background var(--dur-fast) var(--ease),
    color var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease);
}

.chip:hover  { background: var(--surface-hover); color: var(--ink); }

.chip.active {
  background: color-mix(in oklch, var(--brand) 18%, transparent);
  color: var(--brand);
  border-color: color-mix(in oklch, var(--brand) 38%, transparent);
}

/* List container */
.list-container {
  padding: 0 var(--sp-3) var(--sp-4);
  flex: 1;
}

/* --- Alert items --------------------------------------------- */
@keyframes item-enter {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

.alert-item {
  background: var(--surface);
  border: 1px solid var(--stroke);
  border-radius: var(--r);
  padding: var(--sp-3);
  margin-bottom: var(--sp-2);
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  animation: item-enter var(--dur-slow) var(--ease) both;
  cursor: default;
}

.alert-item:hover {
  border-color: var(--stroke-strong);
  background: var(--surface-hover);
}

.alert-item-row {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-2);
  flex-wrap: wrap;
}

.alert-rule-name {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--ink);
  /* Takes the remaining width on its own line. Sharing the row with a
     nowrap timestamp left ~150px for the title in a 360px rail, so
     "Conflict Proximity Alert" wrapped across three lines and the alert list
     read as a column of fragments. */
  flex: 1 1 100%;
  min-width: 0;
  line-height: 1.35;
}

.alert-item-meta {
  display: flex;
  gap: var(--sp-2);
  align-items: center;
  flex-wrap: wrap;
}

.alert-metric {
  font-size: var(--text-xs);
  color: var(--ink-faint);
  font-family: var(--font-mono);
}

.alert-timestamp {
  font-size: var(--text-xs);
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  margin-inline-start: auto;
}

.item-actions {
  display: flex;
  gap: var(--sp-1);
  flex-wrap: wrap;
  margin-top: var(--sp-1);
}

/* Severity chips live in components.css, in one block covering all four class
   spellings this product accumulated (`.sev-`, `.severity-`, `.chip-`, and the
   one fmt.js invented). They used to be declared here for the console alone,
   which is how the same word ended up a 3px square in one place and a 12px
   pill in another — HX-07. */

/* Status pills */
.status-pill {
  display: inline-flex;
  align-items: center;
  padding: 1px 7px;
  font-size: var(--text-xs);
  font-weight: 500;
  border-radius: 999px;
  white-space: nowrap;
  /* An explicit default, because the class is built from record state and the
     vocabulary keeps growing. Without one, a status with no rule of its own
     inherited from wherever it happened to sit — and four workflow states
     rendered black on a near-black surface at 1.21:1, which is not a low
     contrast ratio but the absence of text. An unmapped status must still be
     legible; that it is not the one we styled is a lesser problem. */
  background: var(--surface);
  color: var(--ink-muted);
}

.status-open         { background: color-mix(in oklch, var(--warn) 14%, transparent);   color: var(--warn); }
.status-approved     { background: color-mix(in oklch, var(--ok) 14%, transparent);     color: var(--ok); }
.status-auto-approved,
.status-auto_approved { background: color-mix(in oklch, var(--brand) 14%, transparent); color: var(--brand); }
.status-acknowledged { background: color-mix(in oklch, var(--ink-muted) 14%, transparent); color: var(--ink-muted); }
.status-rejected     { background: color-mix(in oklch, var(--danger) 14%, transparent); color: var(--danger); }
.status-draft        { background: var(--surface);                                       color: var(--ink-muted); }
.status-ready        { background: color-mix(in oklch, var(--brand) 14%, transparent);  color: var(--brand); }
.status-distributed  { background: color-mix(in oklch, var(--ok) 14%, transparent);     color: var(--ok); }
.status-fresh        { background: color-mix(in oklch, var(--ok) 14%, transparent);     color: var(--ok); }
.status-stale        { background: color-mix(in oklch, var(--warn) 14%, transparent);   color: var(--warn); }
.status-failed       { background: color-mix(in oklch, var(--danger) 14%, transparent); color: var(--danger); }
.status-never-run,
.status-never_run    { background: var(--surface);                                       color: var(--ink-faint); }
.status-active       { background: color-mix(in oklch, var(--ok) 14%, transparent);     color: var(--ok); }
.status-unknown      { background: var(--surface);                                       color: var(--ink-faint); }

/* --- Report items -------------------------------------------- */
.report-item {
  background: var(--surface);
  border: 1px solid var(--stroke);
  border-radius: var(--r);
  padding: var(--sp-3);
  margin-bottom: var(--sp-2);
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

.report-item:hover {
  border-color: var(--stroke-strong);
  background: var(--surface-hover);
}

.report-item-title {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--ink);
}

.report-item-meta {
  font-size: var(--text-xs);
  color: var(--ink-faint);
  font-family: var(--font-mono);
  display: flex;
  gap: var(--sp-3);
  align-items: center;
  flex-wrap: wrap;
}

/* --- Source cards (ingestion) -------------------------------- */
.source-card {
  background: var(--surface);
  border: 1px solid var(--stroke);
  border-radius: var(--r);
  padding: var(--sp-3);
  margin-bottom: var(--sp-2);
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

.source-card-header {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

.health-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex-shrink: 0;
}

.health-dot-fresh    { background: var(--ok); }
.health-dot-stale    { background: var(--warn); }
.health-dot-failed   { background: var(--danger); }
.health-dot-never-run,
.health-dot-never_run { background: var(--ink-faint); }

.source-name {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--ink);
  flex: 1;
  min-width: 0;
}

.source-meta {
  font-size: var(--text-xs);
  color: var(--ink-faint);
  font-family: var(--font-mono);
}

/* Source grid checkboxes */
.source-grid {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--sp-2) var(--sp-4);
}

.source-grid label {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--text-sm);
  color: var(--ink-muted);
  cursor: pointer;
  padding: var(--sp-1) var(--sp-2);
  border-radius: var(--r-sm);
}

.source-grid label:hover { background: var(--surface-hover); color: var(--ink); }

/* --- Forms --------------------------------------------------- */
.inline-form {
  padding: var(--sp-3) var(--sp-4);
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  border-top: 1px solid var(--stroke);
  border-bottom: 1px solid var(--stroke);
  background: var(--bg);
}

label {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--ink-muted);
}

input[type="text"],
input[type="number"],
input[type="password"],
input[type="url"],
input[type="search"],
input[type="datetime-local"],
select,
textarea {
  background: var(--bg);
  color: var(--ink);
  border: 1px solid var(--stroke-strong);
  border-radius: var(--r-sm);
  padding: 0.35rem 0.5rem;
  font-size: var(--text-sm);
  font-family: var(--font-sans);
  width: 100%;
  min-height: 30px;
  transition: border-color var(--dur-fast) var(--ease), outline-color var(--dur-fast) var(--ease);
}

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

input:focus, select:focus, textarea:focus {
  outline: 2px solid var(--brand);
  outline-offset: -1px;
  border-color: var(--brand);
}

textarea {
  resize: vertical;
  min-height: 72px;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
}

.form-actions {
  display: flex;
  gap: var(--sp-2);
  flex-wrap: wrap;
}

.ingestion-region {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-4);
}

.ingestion-actions {
  display: flex;
  gap: var(--sp-2);
  flex-wrap: wrap;
  padding: var(--sp-2) var(--sp-4);
}

.service-asset-section {
  padding: var(--sp-4);
  border-top: 1px solid var(--stroke);
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

/* --- Settings ----------------------------------------------- */
.settings-content {
  display: flex;
  flex-direction: column;
  padding-bottom: var(--sp-8);
}

.settings-section {
  padding: var(--sp-4);
  border-top: 1px solid var(--stroke);
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

.settings-note {
  font-size: var(--text-sm);
  color: var(--ink-muted);
  max-width: 65ch;
  line-height: 1.6;
}

.ops-forms {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

details {
  border: 1px solid var(--stroke);
  border-radius: var(--r);
  overflow: hidden;
}

summary {
  padding: var(--sp-2) var(--sp-3);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--ink-muted);
  cursor: pointer;
  user-select: none;
  background: var(--surface);
  list-style: none;
  display: flex;
  align-items: center;
}

summary::-webkit-details-marker { display: none; }

summary::before {
  content: '+';
  display: inline-block;
  width: 1em;
  margin-right: var(--sp-2);
  color: var(--ink-faint);
  font-weight: 400;
  transition: transform var(--dur-fast) var(--ease);
}

details[open] summary::before { content: '-'; }

summary:hover { background: var(--surface-hover); color: var(--ink); }
details[open] summary { border-bottom: 1px solid var(--stroke); color: var(--ink); }

.details-body {
  padding: var(--sp-3);
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  background: var(--bg-elevated);
}

/* --- Statusbar ---------------------------------------------- */
.statusbar {
  position: sticky;
  bottom: 0;
  z-index: var(--z-statusbar);
  height: var(--size-statusbar);
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  padding: 0 var(--sp-5);
  background: var(--bg-elevated);
  border-top: 1px solid var(--stroke);
  /* The status line packs source dots, an update stamp and three metrics into
     one row. At 360px that was 389px of content on a 360px bar, overflowing the
     document. The status text scrolls rather than widening the page. */
  min-width: 0;
  overflow: hidden;
}

.statusbar-live {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  width: 100%;
  min-width: 0;
}

.statusbar-live > * { flex-shrink: 0; }

.statusbar > * { flex-shrink: 0; }

#status {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

@media (max-width: 768px) {
  .statusbar { gap: var(--sp-2); padding: 0 var(--sp-3); }
  /* Secondary telemetry is the first thing to go: "Last signal" and
     "Median lag" were routinely just an em dash. */
  .statusbar-metric { display: none; }
}

.source-dots {
  display: flex;
  gap: 4px;
  align-items: center;
}

.source-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
}

.status-text {
  font-size: var(--text-xs);
  color: var(--ink-faint);
  font-family: var(--font-mono);
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.queued-count {
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--warn);
  font-family: var(--font-mono);
  white-space: nowrap;
}

/* Empty states */
.empty-state {
  padding: var(--sp-8) var(--sp-4);
  text-align: center;
  color: var(--ink-muted);
  font-size: var(--text-sm);
}

.empty-state p { max-width: 45ch; margin: 0 auto; line-height: 1.6; }

/* An empty state that is reporting a failure, not an absence.
   The base rule above is `color: var(--ink-muted)` — quiet grey, centred, no
   title. That is right for "there is nothing here" and wrong for "this was not
   checked": the failed variant has to be visibly *not* the reassuring one, or
   an operator scanning the rail reads both as the same reassurance at different
   greys. The wash and the title are what separate them. */
.empty-state-error {
  color: var(--ink);
  border-inline-start: 3px solid var(--danger);
  text-align: start;
}

.empty-state-error .empty-state-title {
  font-weight: 650;
  color: var(--danger);
  margin-bottom: var(--sp-1);
}

/* The title is a heading in the a11y tree even though it is a `<p>`: the rail
   is a `role="tabpanel"` and the empty state is its entire content, so without
   it a screen-reader user arriving on this tab has no level to attach to. */
.empty-state-error p { max-width: 60ch; margin-inline: 0; }

.empty-state-error .filter-scope-note { color: var(--ink-muted); font-size: var(--text-xs); }

/* --- Dialogs ------------------------------------------------- */
dialog {
  background: var(--bg-elevated);
  color: var(--ink);
  border: 1px solid var(--stroke-strong);
  border-radius: var(--r-lg);
  padding: 0;
  max-width: min(90vw, 560px);
  width: 100%;
  box-shadow:
    0 4px 8px oklch(5% 0.02 260 / 0.4),
    0 16px 48px oklch(5% 0.02 260 / 0.6);
}

dialog::backdrop {
  background: oklch(10% 0.02 260 / 0.65);
}

dialog[open] {
  animation: dialog-enter var(--dur-slow) var(--ease) both;
}

@keyframes dialog-enter {
  from { opacity: 0; transform: scale(0.97) translateY(-4px); }
  to   { opacity: 1; transform: scale(1) translateY(0); }
}

/* The universal `* { margin: 0 }` reset beat the user agent's
   `dialog { margin: auto }`, so every dialog in the console opened pinned to
   the top-left corner instead of centred. A dialog in the corner reads as a
   panel that failed to open. */
dialog { margin: auto; }

.dialog-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--sp-4) var(--sp-5);
  border-bottom: 1px solid var(--stroke);
}

.dialog-header h2 { font-size: var(--text-h2); }

/* `.dialog-body` was used in the console and in focal-point and defined
   nowhere, so dialog content had no padding, no scroll container and no max
   height: a long dialog grew past the viewport with no way to reach its foot. */
.dialog-body {
  padding: var(--sp-4) var(--sp-5);
  max-height: 65vh;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

.dialog-body > :first-child { margin-top: 0; }

/* `.dialog-close` was used on four dialogs and defined nowhere, so the close
   controls fell back to the bare `.btn .btn-icon` box with no spacing from the
   title beside them. */
.dialog-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 1.75rem;
  height: 1.75rem;
  margin-inline-start: var(--sp-3);
  padding: 0;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r);
  color: var(--ink-muted);
  font-size: var(--text-body);
  line-height: 1;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.dialog-close:hover { background: var(--surface-hover); color: var(--ink); }

.dialog-actions {
  display: flex;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-5);
  border-top: 1px solid var(--stroke);
  flex-wrap: wrap;
}

.detail-body {
  padding: var(--sp-4) var(--sp-5);
  max-height: 60vh;
  overflow-y: auto;
  font-size: var(--text-sm);
  line-height: 1.6;
}

.detail-body dl {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: var(--sp-1) var(--sp-4);
}

.detail-body dt {
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--ink-muted);
  white-space: nowrap;
  padding: 2px 0;
}

.detail-body dd {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--ink);
  word-break: break-all;
  padding: 2px 0;
}

/* Command palette */
.command-palette {
  top: 18vh;
  max-width: min(92vw, 640px);
  border-radius: var(--r-lg);
  overflow: hidden;
}

.palette-header {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--stroke);
}

/* The command-palette search field carried `outline: none` on both its base and
   :focus rules, so the one control a user is guaranteed to be typing into had
   no focus indicator at all — and then cancelled the generic input ring that
   would otherwise have supplied one. The palette opens focused, so this was
   invisible to anyone using a mouse and absent for anyone using a keyboard. */
.palette-header input[type="search"] {
  flex: 1;
  background: transparent;
  border: none;
  font-size: var(--text-body);
  color: var(--ink);
  padding: var(--sp-2) var(--sp-1);
  min-height: unset;
  width: auto;
}

/* The ring lives on the header rather than the input, so it frames the whole
   search affordance instead of drawing a box inside a box. */
.palette-header:has(input[type="search"]:focus-visible) {
  outline: var(--focus-ring-width) solid var(--focus-ring);
  outline-offset: 1px;
  border-radius: var(--r-sm);
}

.palette-results {
  list-style: none;
  max-height: 52vh;
  overflow-y: auto;
}

.palette-result {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  cursor: pointer;
  font-size: var(--text-sm);
  color: var(--ink-muted);
  transition:
    background var(--dur-fast) var(--ease),
    color var(--dur-fast) var(--ease);
  border-bottom: 1px solid var(--stroke);
}

.palette-result:last-child { border-bottom: none; }

.palette-result:hover,
.palette-result.selected {
  background: var(--surface-hover);
  color: var(--ink);
}

.palette-result-icon {
  font-size: 0.9em;
  flex-shrink: 0;
  width: 18px;
  text-align: center;
  color: var(--ink-faint);
  font-family: var(--font-mono);
}

.palette-result-label { flex: 1; }

.palette-result-category {
  font-size: var(--text-xs);
  color: var(--ink-faint);
}

/* Shortcut dialog */
.shortcut-dialog { max-width: 420px; }

.shortcut-table {
  width: 100%;
  border-collapse: collapse;
  display: table;
  margin: 0;
}

.shortcut-table td {
  padding: var(--sp-2) var(--sp-4);
  font-size: var(--text-sm);
  color: var(--ink-muted);
  border-bottom: 1px solid var(--stroke);
  vertical-align: middle;
}

.shortcut-table tr:last-child td { border-bottom: none; }

.shortcut-table td:first-child { width: 90px; }

/* Detail dialog */
.detail-dialog { max-width: min(90vw, 520px); }

/* --- Workflow ribbon (horizontal above the console) ------- */
.workflow-ribbon {
  background: var(--bg-elevated);
  border-bottom: 1px solid var(--stroke);
  overflow-x: auto;
  overflow-y: hidden;
  /* A flex child defaults to min-width:auto, so the ribbon sized itself to the
     widest of eight cards and pushed the whole document 29px past a 360px
     viewport. The scroll container below could not help, because its parent
     was already too wide. */
  min-width: 0;
  max-width: 100%;
}

.workflow-tray {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(140px, 1fr);
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  /* The tray holds eight workflow cards. Below ~900px they cannot all fit, and
     without a scroll container the widest card pushed the whole document 29px
     past the viewport — so the console had a horizontal scrollbar and every
     card to the right of the fold. */
  min-width: 0;
  overflow-x: auto;
  scroll-snap-type: x proximity;
}

.workflow-tray > * { scroll-snap-align: start; }

@media (max-width: 900px) {
  .workflow-tray {
    grid-auto-columns: 140px;
  }
}

.workflow-metric {
  background: var(--surface);
  border: 1px solid var(--stroke);
  border-radius: var(--r);
  padding: var(--sp-2) var(--sp-3);
  cursor: pointer;
  transition: background var(--dur-normal) var(--ease), border-color var(--dur-normal) var(--ease);
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: var(--sp-2);
  min-height: 44px;
}

.workflow-metric:hover {
  background: var(--surface-hover);
  border-color: var(--brand);
}

.workflow-metric-name {
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--ink-muted);
  text-transform: capitalize;
  line-height: 1.2;
}

.workflow-metric-count {
  font-size: 1.125rem;
  font-weight: 700;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.workflow-metric-meta {
  grid-column: 1 / -1;
  font-size: var(--text-xs);
  color: var(--ink-faint);
  line-height: 1.2;
}

/* --- Equity table ------------------------------------------ */
.equity-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-sm);
}

.equity-table thead {
  background: var(--surface);
  border-bottom: 1px solid var(--stroke-strong);
}

.equity-table th {
  padding: var(--sp-2) var(--sp-3);
  text-align: left;
  font-weight: 600;
  color: var(--ink-muted);
  font-size: var(--text-xs);
}

.equity-table td {
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--stroke);
  color: var(--ink);
}

.equity-table tbody tr:hover {
  background: var(--surface);
}

/*
 * The equity table lives in the 360px rail and was overflowing it by 29px with
 * overflow-x: hidden on the wrapper, which silently cut off the last column:
 * "Not acknowledged" — the one that says who was not reached, and the reason
 * the surface exists. The numbers were in the DOM and unreachable on screen.
 *
 * Tightened here rather than given a scrollbar: a horizontally scrolling table
 * in a side rail hides the fact that there is more to read, which is the same
 * failure in a different shape. If a column is ever added that genuinely does
 * not fit, this needs a real redesign, not tighter padding.
 */
.equity-table th,
.equity-table td {
  padding: var(--sp-2) var(--sp-1);
}

/* Headers wrap; data cells do not. Wrapping the headers is what lets the table
   fit the rail at all — nowrap on both was pinning it at its content width,
   which is 29px wider than the rail and invisible behind overflow-x: hidden. */
.equity-table td {
  white-space: nowrap;
}

.equity-table th:first-child,
.equity-table td:first-child {
  padding-left: var(--sp-2);
}

.equity-table th:not(:first-child),
.equity-table td:not(:first-child) {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* --- Dispatch gate dialog ---------------------------------- */
.dispatch-gate-dialog {
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--stroke);
  border-radius: var(--r);
  padding: 1.25rem;
  max-width: 420px;
}

.dispatch-gate-dialog::backdrop {
  background: oklch(0% 0 0 / 0.6);
}

.dispatch-gate-dialog label {
  display: flex;
  flex-direction: column;
  margin: var(--sp-3) 0;
  font-size: var(--text-sm);
}

.dispatch-gate-dialog label span:first-child {
  font-weight: 500;
  color: var(--ink-muted);
  margin-bottom: var(--sp-1);
}

.dispatch-gate-dialog select {
  background: var(--bg);
  color: var(--ink);
  border: 1px solid var(--stroke);
  border-radius: var(--r-sm);
  padding: var(--sp-2);
  font-size: var(--text-sm);
  font-family: var(--font-sans);
}

.dispatch-gate-dialog .warning {
  background: color-mix(in oklch, var(--warn) 14%, transparent);
  color: var(--ink);
  padding: var(--sp-2);
  border-radius: var(--r-sm);
  font-size: var(--text-sm);
  margin: var(--sp-3) 0;
}

/* --- Statusbar metrics ------------------------------------ */
.statusbar-metrics {
  display: flex;
  gap: var(--sp-4);
  align-items: center;
  margin-left: auto;
  font-size: var(--text-xs);
}

/* inline-flex with a gap turned every text node into a flex item, so the
   status bar read "Last signal : —" with a space on each side of every colon.
   These are label/value pairs, not toolbars: they flow inline, and the only
   gap is the one that belongs between metrics. */
/* The `hidden` attribute is set on all three of these in the markup, and it did
   nothing: this rule sets `display: inline`, which beats the user agent's
   `[hidden] { display: none }` on specificity. So the status bar rendered three
   labels with an empty span after each colon — "Last signal:", "Median lag:" —
   and the audit read them as a panel reporting nothing. An author rule naming a
   display value silently cancels `hidden` for that element; the only way back
   is to name the hidden state explicitly. */
.statusbar-metric {
  display: inline;
  color: var(--ink-muted);
  white-space: nowrap;
}

.statusbar-metric[hidden] { display: none; }

.statusbar-metric + .statusbar-metric { margin-inline-start: var(--sp-4); }

.statusbar-metric span:first-child {
  font-weight: 500;
}

.dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex-shrink: 0;
}

.dot-ok {
  background: var(--ok);
}

.dot-warn {
  background: var(--warn);
}

.dot-danger {
  background: var(--danger);
}

/* --- Checkboxes and radios ---------------------------------- */
/* Checkboxes were styled by a single ID, #coldChainToggle, and nothing else.
   Every other checkbox in the product rendered as a raw UA control, and on the
   dashboard, scenarios and parametric pages the box visibly detached from its
   label — the operator was looking at a floating white square above the text. */
input[type="checkbox"],
input[type="radio"] {
  appearance: none;
  flex: 0 0 auto;
  width: 1.05rem;
  height: 1.05rem;
  margin: 0;
  border: 1px solid var(--stroke-strong);
  background: var(--surface);
  cursor: pointer;
  vertical-align: middle;
  transition:
    background var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease);
}

input[type="checkbox"] { border-radius: var(--r-sm); }
input[type="radio"]    { border-radius: 50%; }

input[type="checkbox"]:hover,
input[type="radio"]:hover {
  background: var(--surface-hover);
  border-color: var(--brand);
}

input[type="checkbox"]:checked,
input[type="radio"]:checked {
  background: var(--brand);
  border-color: var(--brand);
}

/* The tick is drawn with a mask rather than a background image so it keeps its
   contrast against the brand fill at both themes and both forced-colours modes. */
input[type="checkbox"]:checked {
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3 8l3 3 7-7' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: center;
  background-size: 0.8rem;
}

input[type="radio"]:checked {
  box-shadow: inset 0 0 0 3px var(--surface);
}

/* Keep the box and its label on one line with a real gap, whatever the
   surrounding layout does. `flex-direction: row` is the load-bearing part: the
   stacked-field `label` rule above is what pushed the box onto its own line. */
label:has(> input[type="checkbox"]),
label:has(> input[type="radio"]) {
  display: inline-flex;
  flex-direction: row;
  align-items: center;
  gap: var(--sp-2);
  cursor: pointer;
}

/* Filter rows put the box first, then the caption, inline. */
.check-row {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  cursor: pointer;
  user-select: none;
}

/* --- Cold chain toggle ------------------------------------ */
#coldChainToggle { margin: 0; }

/* --- RTL ---------------------------------------------------- */
[dir="rtl"] .topbar             { flex-direction: row-reverse; }
[dir="rtl"] .topbar-brand       { flex-direction: row-reverse; }
[dir="rtl"] .topbar-controls    { flex-direction: row-reverse; }
[dir="rtl"] .map-section        { border-right: none; border-left: 1px solid var(--stroke); }
[dir="rtl"] .alert-item-row     { flex-direction: row-reverse; }
[dir="rtl"] .alert-timestamp    { margin-left: 0; margin-right: auto; }
[dir="rtl"] .source-card-header { flex-direction: row-reverse; }
[dir="rtl"] .panel-header       { flex-direction: row-reverse; }
[dir="rtl"] summary::before     { margin-right: 0; margin-left: var(--sp-2); }

/* --- Reduced motion ----------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* --- Basemap layers ------------------------------------------ */
/* Themed, so the landmass reads as land in every theme rather than as one dark
   grey block over a white page. */
.ocean-layer path { fill: var(--map-ocean); stroke: var(--map-ocean-edge); stroke-width: 0.5; }
.land-layer path  { fill: var(--map-land); stroke: var(--map-land-edge); stroke-width: 0.8; pointer-events: none; }
.districts-layer circle { fill: none; stroke: var(--brand); stroke-width: 1.2; stroke-dasharray: 4 3; opacity: 0.75; }
.districts-layer text { fill: var(--ink-muted); font-size: 10px; font-family: var(--font-sans); pointer-events: none; }

/* The RTL column swap belongs to the two-column layout and nowhere else.

   It was declared unconditionally, so `[dir="rtl"] .console-layout` (0,2,0) beat
   `.console-layout` inside the 800px breakpoint (0,1,0) — media queries add no
   specificity of their own. An Arabic phone therefore kept the desktop grid: a
   360px rail beside a map that got the remaining 15px, inside a container with
   `overflow: hidden`. 66 controls — every tab, every chip — sat past the left
   edge, clipped and unscrollable, and `documentElement.scrollWidth` stayed equal
   to `clientWidth` so no overflow check could see it. Same reason the toolbar
   fix was invisible. */
@media (min-width: 801px) {
  [dir="rtl"] .console-layout { grid-template-columns: 360px 1fr; }
}

/* --- Mobile -------------------------------------------------- */
@media (max-width: 800px) {
  body.app-shell { height: auto; overflow: auto; }

  .console-layout {
    grid-template-columns: 1fr;
    grid-template-rows: 50vh 1fr;
    height: auto;
  }

  .map-section {
    border-right: none;
    border-bottom: 1px solid var(--stroke);
  }

  .brand-name { display: none; }

  .topbar-controls { gap: var(--sp-1); }
}

/* Route planning overlay.
   Roads are points, not lines, so no polyline is drawn: a straight line
   between hops would invent road geometry the router does not know. The
   sequence is carried by the numbered hop list and the numbered rings. */
.route-layer { pointer-events: none; }

.route-halo {
  fill: none;
  stroke: oklch(80% 0.16 195);
  stroke-width: 2;
  stroke-opacity: 0.9;
}

.route-hop {
  fill: oklch(80% 0.16 195);
  stroke: oklch(20% 0.04 195);
  stroke-width: 1.5;
}

.route-hop-restricted {
  fill: oklch(78% 0.15 75);
}

.route-hop-order {
  fill: oklch(20% 0.04 195);
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 700;
}

.route-hops {
  display: grid;
  gap: 2px;
  margin: 6px 0 0;
  padding: 0;
  list-style: none;
  counter-reset: none;
}

.route-hop-row {
  display: grid;
  grid-template-columns: 16px 1fr auto auto;
  align-items: center;
  gap: 8px;
  padding: 3px 6px;
  border-radius: 4px;
  background: color-mix(in oklab, var(--bg-elevated) 70%, transparent);
  font-size: 11px;
  color: var(--ink-muted);
}

.route-hop-row.is-restricted {
  background: color-mix(in oklab, oklch(78% 0.15 75) 18%, transparent);
}

.route-hop-index {
  font-family: var(--font-mono);
  font-weight: 700;
  color: oklch(80% 0.16 195);
}

.route-hop-name { color: var(--ink); }
.route-hop-class,
.route-hop-status {
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--ink-faint);
}

/* Seasonal context strip.
   Colour encodes warm/cool, but the phase label always carries "advisory":
   a warm month is not a declared ENSO event, and a red badge reading
   "El Niño" would assert something the connector deliberately does not. */
.seasonal-strip {
  display: grid;
  gap: 4px;
  margin: 0 0 8px;
  padding: 8px 12px;
  border: 1px solid var(--stroke);
  border-left: 3px solid oklch(55% 0.04 220);
  border-radius: 6px;
  background: var(--bg-elevated);
}

.seasonal-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex-wrap: wrap;
}

.seasonal-label {
  font-size: 9px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.seasonal-index {
  font-size: 11px;
  color: var(--ink-muted);
}

.seasonal-phase {
  margin-left: auto;
  padding: 1px 7px;
  border-radius: 10px;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 600;
  /* Tokenised because the text on this chip is a token. `background:
     oklch(35% …)` beside `color: var(--ink-muted)` is a dark-theme pairing
     wearing the appearance of a themed one: in the light theme the muted ink is
     34% L and this chip is still 30%, which measured 1.47:1 on the console's own
     "not ingested" badge — the badge whose whole job is to say the data is not
     there. */
  background: var(--phase-bg);
  color: var(--ink-muted);
}

.seasonal-phase-el_nino_advisory {
  background: oklch(48% 0.14 35);
  color: oklch(92% 0.05 35);
}

.seasonal-phase-la_nina_advisory {
  background: oklch(48% 0.12 230);
  color: oklch(92% 0.04 230);
}

.seasonal-phase-neutral { background: var(--phase-bg); }
/* A border, because "we have never ingested this" is the one badge that has to
   be findable in a dark room and is the one that carries no colour of its own. */
.seasonal-phase-unknown { background: var(--phase-bg-unknown); color: var(--ink-faint); box-shadow: inset 0 0 0 1px var(--stroke-strong); }

.seasonal-value {
  display: flex;
  align-items: baseline;
  gap: 8px;
}

.seasonal-anomaly {
  font-family: var(--font-mono);
  font-size: 14px;
  font-weight: 600;
  color: var(--ink);
}

.seasonal-anomaly.is-warm { color: var(--warm); }
.seasonal-anomaly.is-cold { color: oklch(70% 0.11 230); }

.seasonal-period {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--ink-faint);
}

.seasonal-seasonbar {
  display: flex;
  align-items: center;
  gap: 8px;
}

.seasonal-seasonlabel {
  font-size: 10px;
  color: var(--ink-faint);
}

.seasonal-pips {
  display: inline-flex;
  gap: 3px;
}

.seasonal-pip {
  width: 14px;
  height: 5px;
  border-radius: 2px;
  background: oklch(30% 0.02 220);
}

.seasonal-pip.is-met { background: oklch(62% 0.13 200); }

.seasonal-seasonstext {
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--ink-muted);
}

.seasonal-note {
  margin: 0;
  font-size: 10px;
  line-height: 1.5;
  color: var(--ink-faint);
}

.map-scale { font-family: var(--font-mono); font-size: 8px; color: var(--ink-faint); }
.map-legend-text { font-size: 10px; color: var(--ink-muted); padding: 4px 0; line-height: 1.4; }
.map-legend-text p { margin: 0; font-weight: 600; }

.seasonal-enhanced { font-size: 0.8125rem; padding: 0.5rem; background: var(--surface); border: 1px solid var(--stroke); border-radius: var(--r); line-height: 1.5; color: var(--ink); }
.seasonal-enhanced strong { font-weight: 700; color: var(--ink); }
.seasonal-enhanced p { margin: 0; }

/* The calendar is the one chart in the strip, and it is allowed to be wider
   than the strip's text column: twelve months cannot be read at 10px in a
   240px box. It scrolls rather than shrinking, because twelve columns of 4px
   is a texture, not a chart. */
.seasonal-calendar {
  margin: 8px 0 0;
}

.seasonal-calendar-caption {
  font-size: 10px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: 4px;
}

.seasonal-calendar #seasonalCalendar {
  overflow-x: auto;
}

.seasonal-calendar .chart-cell-missing {
  /* A gap needs to read as absent even against a dense field of colour, and a
     2px dashed hairline disappears entirely at the bottom of a diverging ramp.
     The surface fill is what separates it: the cell is the background showing
     through, not a colour. */
  fill: var(--surface);
}

/* An unavailable source is not a broken one. Dimmed card plus the reason, so
   "never_run" never reads as an unexplained failure. */
.source-card.is-unavailable { opacity: 0.72; }

.source-idle {
  font-style: italic;
  color: var(--ink-faint);
}

.source-reason {
  margin-top: 3px;
  font-size: 10px;
  line-height: 1.45;
  color: oklch(72% 0.11 70);
}

.panel-note {
  margin: 8px 0 0;
  font-size: 10px;
  line-height: 1.5;
  color: var(--ink-faint);
  max-width: 62ch;
}

/* Hazard footprint: a source that reports only a bounding box. Drawn as the
   area the source claims, never as a dot at the box centre, because the centre
   is not where the event is. */
.hazard-footprint {
  fill-opacity: 0.10;
  stroke-opacity: 0.55;
  stroke-width: 1.25;
  stroke-dasharray: 4 3;
  cursor: pointer;
}

.hazard-footprint:hover { fill-opacity: 0.18; stroke-opacity: 0.9; }

.hazard-footprint-critical { stroke-opacity: 0.8; fill-opacity: 0.14; }
.hazard-footprint-low { stroke-opacity: 0.35; fill-opacity: 0.06; }

/* --- Tap targets ------------------------------------------------
   Measured across the eight surfaces with scripts/check-responsive.mjs: 171
   controls rendered below the 44px floor this product's own --tap-target-min
   token declares — 24px selects in the map filter bar, 16px range sliders, 20px
   checkboxes. On a desk those are merely tight. For the health worker this app
   exists to serve, on a low-end Android, they are mis-taps. The floor is
   enforced here rather than left to each surface to remember. */
@media (pointer: coarse), (max-width: 768px) {
  .btn,
  button,
  input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="hidden"]),
  select,
  textarea {
    min-height: var(--tap-target-min);
  }

  /* The visible track stays thin; the hit area does not. */
  input[type="range"] { height: var(--tap-target-min); background: transparent; }

  /* 24px is WCAG 2.2 SC 2.5.8 (AA). The label carries the full tap area. */
  input[type="checkbox"],
  input[type="radio"] { width: 1.5rem; height: 1.5rem; }

  .btn-icon { min-width: var(--tap-target-min); }
}

/* --- Console toolbar -------------------------------------------
   The console's own controls. The injected navbar carries the brand and the
   cross-application links, so this row deliberately has no name of its own —
   repeating it is what made the console look like it had two headers. */
.console-toolbar {
  display: flex;
  align-items: center;
  /* Not `flex-end`. A flex container aligns its overflow from the START edge,
     so right-aligning a child that cannot fit hung the overflow off the *left*
     — where nothing can scroll to it, and where `documentElement.scrollWidth`
     does not grow, so no overflow check of any kind can see it. At 375px the
     controls sat at x = -69 inside a 375px row: Language, Copy link, Severity,
     Source and Cold-chain, all present, all focusable, all invisible.
     `.console-toolbar-controls` right-aligns itself with `margin-inline-start`
     below, which is what the `flex-end` was imitating badly. */
  justify-content: flex-start;
  gap: var(--sp-2);
  flex-wrap: wrap;
  padding: var(--sp-1) var(--sp-4);
  background: var(--bg);
  border-bottom: 1px solid var(--stroke);
}

.console-toolbar-controls {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
  margin-inline-start: auto;
  /* A flex item's automatic minimum is its min-content width, which for the
     controls group is 428px. Without this it cannot shrink into a 375px row at
     all, and the row overflows in whichever direction the alignment chose. */
  min-width: 0;
}

/* The three declarations below are load-bearing, not tidying. Without them,
   dropping `flex-end` converts a silent left overflow into a *detectable* right
   overflow — 444px at a 360px viewport — because the nowrap labels and the
   nowrap record count hold the group's min-content width open. */
@media (max-width: 480px) {
  .console-toolbar-controls .filter-label {
    white-space: normal;
    min-width: 0;
    flex: 1 1 100%;
  }
  .console-toolbar-controls .filter-label select {
    min-width: 0;
    max-width: 100%;
    flex: 1 1 8rem;
  }
  /* Physical, to match the `margin-left` it overrides, so RTL stays correct. */
  .console-toolbar-controls .map-record-count {
    white-space: normal;
    margin-left: 0;
  }
}

/* `select { width: 100% }` is right in a form column and wrong in a toolbar,
   where it stretched the language picker across the whole row. */
.console-toolbar-controls select {
  width: auto;
  min-width: 8rem;
}

/* --- Workflow summary --------------------------------------------
   The ribbon showed eight tiles labelled with alert-rule names, each carrying
   an open count. They read as eight KPIs when they are one number split eight
   ways, and the totals appeared nowhere. The summary leads; the tiles are
   explicitly a breakdown. */
.workflow-summary {
  display: flex;
  align-items: baseline;
  gap: var(--sp-1) var(--sp-2);
  flex-wrap: wrap;
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--stroke);
  font-size: var(--text-sm);
  color: var(--ink-muted);
}

.workflow-summary-count {
  font-size: var(--text-h2);
  font-weight: 700;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}

.workflow-summary-sep { color: var(--ink-faint); }
.workflow-summary-rejected { color: var(--warn); }
.workflow-summary-breakdown {
  margin-inline-start: auto;
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--ink-faint);
}

.workflow-metric[tabindex] { cursor: pointer; }
.workflow-metric.active {
  border-color: var(--brand);
  box-shadow: inset 0 0 0 1px var(--brand);
}

.workflow-empty { padding: var(--sp-3); color: var(--ink-muted); font-size: var(--text-sm); }

/* --- Print ----------------------------------------------------
   There was no print stylesheet anywhere in the product. For a tool where a
   dispatch decision or an equity audit gets printed and filed, that means
   printing a black rectangle: the dark background, the map chrome and the
   filter controls all come out, and the alert list comes out on whatever paper
   happens to be in the tray. */
@media print {
  /* The theme blocks are attribute selectors and this one is a pseudo-class,
     so `:root` alone loses to `[data-theme='light']` no matter where it sits —
     the print palette has to out-specify the theme, not merely precede it. */
  :root,
  :root[data-theme] {
    /* Force light. The token layer is dark-only, so every surface is inverted
       at the token level rather than by overriding hundreds of rules. */
    --bg: #fff;
    --bg-elevated: #fff;
    --surface: #f4f4f5;
    --surface-hover: #e9e9ec;
    --ink: #111;
    --ink-muted: #444;
    --ink-faint: #555;
    --stroke: #c9c9cf;
    --stroke-strong: #9a9aa2;
    --brand: #1a5f7a;
    --brand-hover: #1a5f7a;
    --on-brand: #fff;
    --focus-ring: #1a5f7a;
    --ok: #1f7a4d;
    --warn: #8a5a00;
    --danger: #a11;
    --sev-critical: #a11;
    --sev-high: #8a5a00;
    --sev-medium: #6b6b00;
    --sev-low: #1f7a4d;
  }

  /* Nothing that only makes sense on screen. */
  .topbar, .l-navbar, .map-filters, .map-sim, .rail-tabs, .item-actions,
  .filter-label, .statusbar, .offline-banner, .skip-link, .item-actions,
  .workflow-tray, .item-actions, .toast, dialog, .cmd-palette-button,
  #refreshButton, #cmdPaletteButton {
    display: none !important;
  }

  html, body {
    height: auto;
    overflow: visible;
    background: #fff;
    color: #000;
    font-size: 10.5pt;
  }

  body.app-shell { height: auto; overflow: visible; }

  .console-layout {
    display: block;
    height: auto;
    overflow: visible;
  }

  main, .console-layout, .rail-content, .rail-panel {
    overflow: visible !important;
    height: auto !important;
    max-height: none !important;
    padding: 0;
  }

  /* Print every rail panel, not just the selected tab: the tab strip is gone,
     so leaving the others hidden would silently drop two thirds of the page.
     Written `.rail-panel[hidden]`, not `.rail-panel`, and that attribute
     selector is load-bearing rather than decorative: components.css closes the
     `[hidden]` defect class with `[hidden] { display: none !important }`, and
     an `!important` declaration only loses to another `!important` one of
     equal-or-higher specificity. Plain `.rail-panel` is also (0,1,0), so
     source order decides — and components.css loads after this file, so print
     would have silently stopped printing three of the five tabs. The attribute
     raises this to (0,2,0) and states the intent instead of depending on load
     order. */
  .rail-panel[hidden] { display: block !important; page-break-inside: avoid; }
  .rail-panel + .rail-panel { page-break-before: always; }

  a[href]::after { content: " (" attr(href) ")"; font-size: 8pt; color: #555; }

  table { page-break-inside: auto; }
  thead { display: table-header-group; }  /* repeat headers across pages */
  tr    { page-break-inside: avoid; }
  th, td { border-bottom: 1px solid #ccc; }

  .alert-item, .kpi-tile, .workflow-card, .workflow-metric {
    page-break-inside: avoid;
    border: 1px solid #ccc;
  }

  /* The map is a picture of where something is. In monochrome it is a grey
     rectangle, so print what it was describing instead. */
  .map-section { page-break-after: always; }

  .seasonal-strip, .statusbar-metric { page-break-inside: avoid; }

  .table-wrap { overflow: visible !important; border: 1px solid #ccc; }
}

/* --- Map as a list ------------------------------------------------
   The map's text alternative. Nothing in the product listed hazards, assets or
   road state as text, so on the officer-facing surfaces the primary decision
   surface was a picture with no non-visual form. */
.map-list-toggle { flex-shrink: 0; }

.map-record-list {
  max-height: 22rem;
  overflow-y: auto;
  border: 1px solid var(--stroke);
  border-radius: var(--r-sm);
  background: var(--bg-elevated);
}

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

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

.map-record-list thead th {
  position: sticky;
  top: 0;
  background: var(--surface);
  color: var(--ink-muted);
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

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

.situation-map:focus-visible {
  outline: var(--focus-ring-width) solid var(--focus-ring);
  outline-offset: 2px;
}

/* Two notes the alert rail started carrying in the UX audit pass.

   `.filter-scope-note` says what the filter bar did to *this list*, which it
   previously did not do at all — the bar looked map-only because the map was
   under it, so `Severity: High` silently left `critical` in the list beside it.

   `.alert-blocked-note` says why a greyed-out Send is greyed out. A disabled
   button announces nothing to a screen reader and hovers nothing in most
   browsers, so the reason has to exist as text. */
.filter-scope-note,
.alert-blocked-note {
	margin: 0 0 0.5rem;
	font-size: 0.78rem;
	line-height: 1.45;
	color: var(--text-muted);
}
.alert-blocked-note { margin: 0.35rem 0 0; }
.filter-scope-note strong { color: var(--text); font-weight: 600; }

/* The breakdown lives behind a summary now: eight tiles were a decomposition of
   the number printed directly above them, and they put eight tab stops on the
   screen before the operator had seen a record. */
.workflow-breakdown {
	border-top: 1px solid var(--stroke);
	padding: var(--sp-1) var(--sp-3) 0;
}
.workflow-breakdown > summary {
	cursor: pointer;
	font-size: 0.78rem;
	color: var(--text-muted);
	min-height: 32px;
	display: flex;
	align-items: center;
	gap: var(--sp-1);
}
.workflow-breakdown > summary:hover { color: var(--text); }
.workflow-breakdown[open] > summary { margin-bottom: var(--sp-1); }
