/* VicFire shared component styles — GuardOps theme.
 *
 * theme.css is the only source of colour. Nothing here may introduce a
 * literal hex; every colour resolves to a token from that file.
 *
 * Load order on every page:
 *   theme.js (no defer, sets data-theme before paint)
 *   fonts.css  →  theme.css  →  app.css  →  page-specific <style>
 *
 * Component geometry (padding, radii, weights) is measured from the GuardOps
 * design prototype rather than guessed — see guardops/design-prototype/.
 * Class names are unchanged from the previous theme, so the page markup did
 * not have to move.
 */

/* ── Incident type colours ────────────────────────────────────────────────
 * Six types onto six distinct tokens. Unlike the previous theme these need
 * only ONE set: the basemap now follows the theme (Mapbox dark-v11 in dark,
 * light-v11 in light/hybrid), so a theme-aware colour is legible both on the
 * page and on the map. */
:root {
  --type-fire:    var(--danger);   /* red    #ef4444 */
  --type-rescue:  var(--cat-2);    /* yellow #fbbf24 dark / #b45309 light */
  --type-medical: var(--cat-1);    /* blue   #7dd3fc dark / #0284c7 light */
  --type-alarm:   var(--cat-3);    /* purple #a78bfa dark / #6d28d9 light */
  /* Pink is the last distinct hue standing: red/amber/blue/purple are the
     four types above, grey is "other", and green means "safe" and nothing
     else. --cat-4's only other job is domain identity on /map (FRV
     markers), which never shares a surface with a type badge. */
  --type-vehicle: var(--cat-4);    /* pink   #f472b6 dark / #be185d light */
  /* ⚠ PINNED to literals, NOT var(--fg-3), and that is the whole point.
     "other" was an alias of the muted-label ink, so it silently followed
     --fg-3 wherever the theme took it. When the palette landed (2026-08-14)
     the owner listed the five type colours as explicitly NOT carried over —
     and four of them are safe because they alias tokens that did not move,
     but --fg-3 DID move (#98a1ad -> #a29f9f dark, #5b6878 -> #565656 light).
     Aliasing would have changed an excluded colour as a side effect of a
     chrome change. These are the exact values the site shipped before the
     swap. Do not restore the alias; the two roles only ever coincided. */
  --type-other:   #98a1ad;
  /* Green (--ok) is deliberately absent: it means "safe" and nothing else. */
}
[data-theme="light"] { --type-other: #5b6878; }


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

html { background: var(--bg); }

/* GuardOps sets html{font-size:17px} to shift Tailwind's rem-based text
   utilities up a step. vicfire sizes in px, so copying it would scale nothing
   and only mislead anyone reading this — deliberately not carried over. */
body {
  background: var(--bg);
  color: var(--fg-1);
  font-family: var(--font-sans);
  font-size: 14px;
  line-height: 1.5;
  min-height: 100vh;
  font-feature-settings: "cv11", "ss01";
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

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

/* Numbers are tabular so columns don't jitter as digits change. */
.tabular, .stat-big, .stat-sub, .inc-time, .brig-count, .dist-count,
.type-count, .pager-info, .cond-count {
  font-variant-numeric: tabular-nums;
}

/* JetBrains Mono is the design's mono face — for IDs, times and codes. */
.mono, .inc-time, .brig-name {
  font-family: var(--font-mono);
  font-feature-settings: normal;
}

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: var(--radius-pill); }
::-webkit-scrollbar-thumb:hover { background: var(--fg-3); }

/* ── Focus ────────────────────────────────────────────────────────────── */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}
:focus:not(:focus-visible) { outline: none; }

/* ── Cards ───────────────────────────────────────────────────────────────
 * Filled surface, hairline border, 12px radius — the GuardOps card. */
.card, .card-split, .panel {
  background: var(--surface-1);
  border: 1px solid var(--border-1);
  border-radius: var(--radius-lg);
  padding: 16px;
}
.card-split { display: flex; padding: 0; overflow: hidden; }
.card-half {
  flex: 1; padding: 16px;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center; text-align: center;
}
.card-half + .card-half { border-left: 1px solid var(--border-1); }

/* ── Type ─────────────────────────────────────────────────────────────── */
.card-title, .section-label, .section-title {
  font-size: var(--t-label);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--fg-3);
}
.card-title { margin-bottom: 12px; }
.section-label, .section-title { margin-bottom: 12px; }

.stat-big {
  font-size: 34px;
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -.02em;
  color: var(--fg-1);
}
.stat-sub { font-size: var(--t-label); color: var(--fg-3); margin-top: 4px; }

/* ── Header ───────────────────────────────────────────────────────────── */
/* ── Header geometry ──────────────────────────────────────────────────────
   The weather strip is aligned to start exactly where the hero map starts, so
   the header reads as a continuation of the layout rather than a bar floating
   over it. That position is not a fraction of the viewport — it is driven by
   the card's clamp, so it moves from 74% of the width at 1280 down to 65% at
   2560. Reproducing it needs the same arithmetic, not a percentage:

     map left = rail + layout gap (16) + main-col padding (22) + card + hero gap (16)
              = rail + 54 + var(--hero-card)

   rail + 18 in the track, not + 54: the track START has to land on the map,
   and the header spends its own 20px padding-left and 16px grid gap before
   that point. 54 - 20 - 16 = 18. Verified against .hero-map's measured left
   edge at 1280 / 1600 / 1920 / 2560, expanded and collapsed rail: delta 0px.

   ⚠ GATED AT 1900px. Below that the alignment does not fit: the right-hand
   cluster needs ~595px in its winter form, and the space to the right of the
   map is only 312px at 1280, 456 at 1600, 546 at 1800. Under the gate the
   header is the flex row it always was, with the strip pushed right by
   margin-left:auto.

   THE OLD "break-even is W >= 1909" ARITHMETIC HAS BEEN REMOVED, and the
   reason is worth keeping. It computed the gate from a cluster width of 595px
   — which was the header measured on ONE winter day, with both fire-danger
   badges null. The cluster is not a constant: on a CATASTROPHIC day it is
   ~870px. So the formula was solving for a number that only held in one
   season, and it was ALSO off by nine pixels against its own gate, leaving a
   1900-1908px window where the grid engaged before it fitted.
   The header no longer depends on that arithmetic being right — see the
   overflow guards below .header-right. Do not reintroduce a magic width. */
:root {
  --hero-card: clamp(560px, 55vw, 1410px);
  --rail-w: 190px;
}

/* ── Type scale ───────────────────────────────────────────────────────────
 * One fluid scale, declared here and referenced everywhere, so sizes cannot
 * drift back into per-rule pixel values. Same clamp technique pager.html and
 * map.html already used locally; this lifts it to the shared sheet so every
 * page moves together.
 *
 * WHY THE SIZES WENT UP. The old scale was built around a 14px body with
 * 12px secondary text, which is comfortable on a desk and too small for the
 * way this site is actually read: on a phone, outdoors, at a glance, often by
 * someone about to get on a truck. The secondary text is not decoration here
 * — the raw pager string IS the dispatch, so 12px was the wrong size for the
 * most important content on the page.
 *
 * WHY THE MINIMUMS ARE THE MOBILE SIZES. clamp()'s MIN applies on the
 * narrowest screens, so these numbers are what a 390px phone gets. They are
 * deliberately not smaller than the desktop equivalents were — the hero used
 * to SHRINK from 20px to 17px under 600px, which had it smallest exactly
 * where it needed to be loudest.
 *
 * THE HERO CAP IS MEASURED, NOT CHOSEN. Its comment promises the address
 * fits one line for ~93% of real incidents. Re-measured over 138 live
 * addresses: p90 42 chars, p93 47, max 55. Against the hero's real width the
 * fitting capacity is 1500px→48ch at 26px and 1920px→62ch, so 26px holds the
 * p93 guarantee at every width the cap is reached. Raising it further breaks
 * that promise — check the fit before you do. */
:root {
  --t-hero:      clamp(19px,   1.50vw, 26px);  /* hero job + address       */
  --t-hero-msg:  clamp(14.5px,  .95vw, 19px);  /* raw CAD string in hero   */
  --t-hero-meta: clamp(13px,    .80vw, 16px);  /* hero time, id, map ref   */
  --t-row-addr:  clamp(15px,    .95vw, 18px);  /* incident row headline    */
  --t-row-body:  clamp(13.5px,  .85vw, 16.5px);/* pager text, table cells  */
  --t-label:     clamp(11.5px,  .70vw, 14px);  /* caps labels, th, chips   */
  --t-num:       clamp(14px,    .90vw, 17px);  /* turnout clock numeral    */
  /* Header chrome is NOT content, and is the one thing that must not grow
     with the scale. The header is a fixed 60px bar whose right-hand cluster
     has a hard width budget: the 1900px grid alignment is gated on that
     cluster fitting 595px (see the note above .header), and sizing its
     buttons from --t-label pushed it to 640px and broke the gate — the page
     gained a 20px horizontal scroll at 1920. Chrome stays put; content
     grows. */
  --t-chrome:    clamp(11px,    .58vw, 19px);  /* header buttons + weather; cap raised 13->19 for TV screens (2026-08-18) */
}

.header, header {
  display: flex; align-items: center; gap: 16px;
  height: 60px; padding: 0 20px;
  background: var(--surface-1);
  border-bottom: 1px solid var(--border-1);
}
/* The LOGO is the compressible part. Adding Map + List cost the header ~137px
   it does not have on a 390px phone, and a flex item defaults to
   min-width:auto — it will not shrink below its own text — so the row pushed
   the page sideways instead. min-width:0 all the way down to the span is what
   lets the title ellipsis instead; the buttons and the toggle never shrink,
   because a half-width button is worse than a truncated title. */
.header-left { display: flex; align-items: center; gap: 16px; min-width: 0; flex: 1 1 auto; }
.header-left .logo, .header-left h1 { min-width: 0; }
.header-left .logo span, .header-left h1 { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.header-left .logo-icon { flex-shrink: 0; }
/* flex-shrink:0 on the CLUSTER, but the weather strip inside it may shrink and
   scroll. A half-width Map button is worse than a truncated forecast, so the
   buttons and the toggle keep their size; the strip is the part that gives. */
/* header-right may SHRINK now (the strip inside scrolls via its own
   min-width:0 + overflow-x:auto); the theme toggle keeps its size via
   .header-actions below. Buttons live in header-left since 2026-08-18. */
.header-right { display: flex; align-items: center; gap: 12px; min-width: 0; margin-left: auto; flex-shrink: 1; }
/* Defined HERE, not per page. Every page used to declare this in its own
   <style> and map.html missed it — .header-actions fell back to display:
   block there, and the ::before separator below rendered as an invisible
   inline sliver. A shared child of the shared header belongs in the shared
   sheet; the per-page copies are now redundant but harmless. */
.header-actions { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
/* A rule between the weather strip and the theme toggle (owner, 2026-08-18):
   the toggle is a control, not another weather cell, and the boundary says
   so. Same geometry as .wx-divider so it reads as one more cell edge. The
   adjacent-sibling selector is the gate: /map has no weather widget, and a
   page without the strip must not carry a dangling rule beside the toggle. */
.weather-widget + .header-actions::before {
  content: '';
  width: 1px; height: clamp(18px, 1.1vw, 26px);
  background: var(--border-2); flex-shrink: 0;
}

/* ── The header must never push the page sideways ─────────────────────────
 *
 * THE BUG THIS FIXES. The weather strip is not a fixed width — it grows with
 * the conditions. On a CATASTROPHIC fire danger day it carries a badge for
 * today AND tomorrow plus a gust reading, which took it from ~390px to ~660px.
 * The grid track below is sized off --hero-card so the header aligns with the
 * map, and .header-right is flex-shrink:0, so nothing could absorb the extra
 * 270px: the header overflowed by ~250px and put a horizontal scrollbar on
 * every page. Measured at 1900-2000px. Winter hides it — both badges are null
 * today, which is why a plain load looks fine and why this survived review.
 *
 * It is exactly backwards for a fire-danger badge to break the layout on the
 * day the badge means the most.
 *
 * Two independent guards, because either alone still fails:
 *
 *   1. The strip may SHRINK and scroll inside its track (min-width:0 defeats
 *      a flex item's automatic min-content floor, which is what actually
 *      pinned it open).
 *   2. The header clips at the viewport, so no future addition to this row
 *      can reintroduce the same class of bug.
 *
 * The 1909px break-even that used to be documented here was a measurement of
 * ONE header state — the winter one. Sizing a layout to a number that only
 * holds in one season is what put a 9px overflow at 1900-1908 even with no
 * badges at all. The gate is gone; the header now fits by construction at
 * every width instead of at the widths someone measured. */
/* ⚠ NOT `max-width: 100vw`. That was the fix's own bug, and it caused BOTH
   symptoms the change was meant to remove.

   100vw is the viewport width INCLUDING the classic vertical scrollbar; the
   space content actually gets is documentElement.clientWidth, ~15px less
   whenever the page scrolls vertically (which it always does). So a header
   capped at 100vw is ~15px wider than the page, which:
     - puts a horizontal scrollbar on every page at 100% zoom, and
     - pushes the right-hand end of the header — the theme toggle — underneath
       the vertical scrollbar, where it is clipped and hard to click.

   `width: 100%` is the correct cap: percentages resolve against the CONTAINING
   BLOCK, which already excludes the scrollbar. It needs no viewport unit and
   no magic number, and it is right at every zoom level, which is where a vw
   cap also drifts (100vw is computed in CSS px and the scrollbar is not).

   overflow-x: clip (not hidden) still contains anything inside the bar: clip
   does not create a scroll container, so it cannot turn into a scrollable
   header or make position:sticky children behave differently. */
.header, header { width: 100%; max-width: 100%; overflow-x: clip; }

/* The >=1900px grid that aligned the weather strip to the hero map's left
   edge is GONE (2026-08-18). Two owner changes killed its arithmetic in one
   day: the Map/Pager buttons moved beside the agency picker, and the strip
   grew into single-line metric cells sized for a TV. Inside the aligned
   track the enlarged strip was CLIPPED 229px at 2560 — HUMIDITY and
   TOMORROW silently scrolled out of view on exactly the screen the sizes
   exist for. A plain flex header fits by construction at every width;
   alignment was a nicety, hidden data is a defect. */

/* The strip is the compressible part of the cluster. It scrolls rather than
   wraps: the header is a fixed 60px bar, so a second line would be clipped
   anyway, and a scrollable strip keeps every reading reachable.
   scrollbar-width:none because a scrollbar inside a 60px bar is unusable and
   ugly; the content is still reachable by drag, wheel and keyboard. */
.header-right .weather-widget {
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: none;
}
.header-right .weather-widget::-webkit-scrollbar { display: none; }

/* ── Nav buttons ──────────────────────────────────────────────────────── */
.nav-btn {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 6px 13px; border-radius: var(--radius-sm);
  border: 1px solid var(--border-2); background: var(--surface-2);
  color: var(--fg-2); font-family: var(--font-sans);
  font-size: var(--t-chrome); font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; text-decoration: none; white-space: nowrap;
}
.nav-btn:hover { color: var(--fg-1); border-color: var(--fg-3); text-decoration: none; }
/* The page you are already on. aria-current is the semantic carrier; the
   styling hangs off it so the two cannot disagree. */
.nav-btn[aria-current="page"] {
  background: var(--accent); border-color: var(--accent); color: var(--accent-fg);
}
.logo, .header h1 {
  display: flex; align-items: center; gap: 10px;
  /* Fluid with the scope-picker trigger beside it (owner, 2026-08-18): the
     min is the old fixed 17px so phones and laptops are untouched, and on a
     wall panel the wordmark grows in step with the picker rather than
     shrinking beside it. */
  font-size: clamp(17px, 1.05vw, 27px); font-weight: 700;
  letter-spacing: -.01em;
  color: var(--fg-1);
  text-decoration: none;
}
.logo:hover { text-decoration: none; opacity: .85; }
.logo-icon {
  width: clamp(32px, 2vw, 50px); height: clamp(32px, 2vw, 50px);
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
  /* No background and no clip. The mark is a circle on a transparent field,
     so it needs neither — the previous rounded-square crop existed for the
     earlier artwork, which carried its own red backdrop. */
}
.logo-icon img { width: 100%; height: 100%; object-fit: contain; display: block; }

/* ── Controls ─────────────────────────────────────────────────────────── */
/* Every section button belongs in this list, not just in home.html's
   re-skin. This base rule is where they get border-radius, border and
   padding; home.html only overrides the fill and the ink. The CFA button
   (since removed, 2026-08-18 — the agency picker superseded it and the FRV
   one) was once added there and not here, which gave it square corners
   beside four rounded ones — a shape difference, visible at a glance, that
   no amount of checking the computed COLOUR would have found. */
.btn-primary, .btn-secondary, .pager-btn, .filter-btn, .filter-all,
.btn-live, .btn-emevic, .btn-vbb, .btn-parks {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  font-family: inherit;
  font-size: 13px; font-weight: 600;
  padding: 8px 14px;
  border-radius: var(--radius-md);
  border: 1px solid var(--border-2);
  background: var(--surface-2);
  color: var(--fg-1);
  text-decoration: none;
  cursor: pointer;
  transition: background .15s, border-color .15s, color .15s;
}
/* The section buttons are NOT in this list, though they share the base
   rule above. home.html re-skins them as brand-coloured fills with their own
   ink, and this hover replaced the fill with --surface-3 while leaving that
   ink alone — white on #e7e9ee (1.21:1) for FRV and Big Build under the light
   theme, near-black on #1c232c (1.33:1) for Vic Emergency and Parks under the
   dark one. Every one of them vanished on hover, in one theme or the other.
   A generic hover may only touch what the generic base set. */
.btn-primary:hover, .btn-secondary:hover, .pager-btn:hover:not([disabled]),
.filter-btn:hover, .filter-all:hover {
  background: var(--surface-3);
  border-color: var(--fg-3);
  text-decoration: none;
}
.btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-fg);
}
.btn-primary:hover { background: var(--accent); border-color: var(--accent); filter: brightness(1.08); }
.pager-btn, .filter-btn, .filter-all { padding: 7px 13px; font-size: var(--t-label); }
/* The theme toggle is a BARE ICON, not a boxed button (owner, 2026-08-18) —
   it left the shared chrome lists above when the text label became a sun /
   moon glyph (theme.js owns the SVG). The padding is the hit target: the
   glyph ramps with the header's fluid rule, and the button stays at least
   30px square, which clears the WCAG 2.2 AA 24px floor with room over it. */
.theme-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 6px; border: 0; background: none; cursor: pointer;
  border-radius: var(--radius-md);
  color: var(--fg-2);
  transition: color .15s, background .15s;
}
.theme-toggle svg { width: clamp(18px, 1.1vw, 26px); height: clamp(18px, 1.1vw, 26px); display: block; }
.theme-toggle:hover { color: var(--fg-1); background: var(--surface-2); }
.pager-btn[disabled] { opacity: .4; cursor: default; }
.filter-btn, .filter-all { color: var(--fg-2); }
.filter-btn.active, .filter-all.active {
  color: var(--accent-ink);
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
/* Favicons are square; the FRV and VBB marks are wordmarks, so they are sized
   by height and keep their aspect ratio. */
.btn-emevic img, .btn-parks img { width: 16px; height: 16px; object-fit: contain; flex-shrink: 0; }
.btn-vbb img { height: 18px; width: auto; object-fit: contain; flex-shrink: 0; }

.search-input {
  padding: 8px 12px;
  border-radius: var(--radius-md);
  border: 1px solid var(--border-2);
  background: var(--surface-2);
  color: var(--fg-1);
  font-family: inherit;
  font-size: 13px;
}
.search-input::placeholder { color: var(--fg-3); }

/* ── Badges — pill, tinted from their own colour ──────────────────────────
 * The caller sets `color`; fill and border derive from it via currentColor,
 * so one rule covers every badge on the site. Measured from the prototype:
 * 3px 8px, 11px/600, 14% fill, 30% border. */
.type-badge, .status-badge, .sev-badge, .rating-badge, .tfb-badge,
.wx-fire-badge, .inc-brigade, .district-count {
  display: inline-block;
  font-size: var(--t-label); font-weight: 700;
  letter-spacing: .02em;
  padding: 3px 9px;
  border-radius: var(--radius-pill);
  /* --b is the badge hue, set inline by vfTokens.badgeStyle(). Badges without
     one (the neutral chips) fall back to currentColor and behave as before. */
  color: var(--b, currentColor);
  background: color-mix(in srgb, var(--b, currentColor) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--b, currentColor) 30%, transparent);
  white-space: nowrap;
}
/* The fire danger badge sits in the header next to the weather, and was sized
   for a table cell. Its own size so it reads at a glance. */
.wx-fire-badge { font-size: var(--t-chrome); padding: 4px 9px; letter-spacing: .04em; }

/* Light theme: the tinted fills are washed out against a near-white ground
   in a way they are not against the dark one, so their opacity doubles —
   14% -> 28% fill, 30% -> 60% border. Half as transparent, in other words.
   Dark is left alone; the same values read fine there. */
/* Light theme: badges are SOLID in their own hue with an ink chosen to read
   on it (vfTokens.inkOn picks white or near-black by measured contrast). The
   hue itself is unchanged — only the fill strength and the label ink differ
   from dark, where the same hue is used as a tint with coloured text.
   Neutral chips have no --b and keep the tint via the fallbacks. */
[data-theme="light"] .type-badge,
[data-theme="light"] .status-badge,
[data-theme="light"] .sev-badge,
[data-theme="light"] .rating-badge,
[data-theme="light"] .tfb-badge,
[data-theme="light"] .wx-fire-badge,
[data-theme="light"] .inc-brigade,
[data-theme="light"] .district-count {
  background: var(--b, color-mix(in srgb, currentColor 40%, transparent));
  border-color: var(--b, color-mix(in srgb, currentColor 75%, transparent));
  color: var(--bi, currentColor);
}
[data-theme="light"] .filter-btn.active,
[data-theme="light"] .filter-all.active {
  background: color-mix(in srgb, var(--accent) 34%, transparent);
}
[data-theme="light"] .incident-row.highlighted td  { background: color-mix(in srgb, var(--accent) 24%, transparent); }
[data-theme="light"] .incident-row.fresh td        { background: color-mix(in srgb, var(--danger) 32%, transparent); }
[data-theme="light"] .incident-row.recent td       { background: color-mix(in srgb, var(--warn)   28%, transparent); }

/* Neutral badges carry no semantic colour of their own. */
.inc-brigade, .district-count {
  color: var(--fg-2);
  font-family: var(--font-mono);
  font-size: var(--t-label);
}
.inc-brigade { flex-shrink: 0; text-align: center; }

/* The primary brigade, given the same treatment as the type: the cell IS the
   value, not a pill floating inside one. Larger than the old badge text
   because at 10px in a pill it was the hardest thing in the row to read.
   The full responder list is on the title attribute. */
.inc-primary {
  flex-shrink: 0; width: 78px;
  align-self: stretch;
  display: flex; align-items: center; justify-content: center;
  /* --b / --bi are set inline on a brigade page (green = ours, orange =
     supporting). Elsewhere there is nothing to compare against, so the
     fallbacks give a neutral cell. Solid either way — no tint.
     --badge-bg, not --surface-3: the chip has to read as a filled block off
     the card, and --surface-3 is only one step up from it. */
  background: var(--b, var(--badge-bg));
  color: var(--bi, var(--fg-1));
  font-family: var(--font-mono);
  font-size: var(--t-row-body); font-weight: 700;
  letter-spacing: .02em;
}

/* Under the light theme the neutral cell keeps the DARK theme's surface.
   Districts do not support one another the way brigades do, so on a district
   page and on the district-wide feed this cell is a plain identifier — the
   darker block reads as a label instead of competing with the type band.
   
   Self-scoping through the --b fallback: a brigade page sets --b/--bi inline
   for the green/orange own-vs-supporting states, and those still win. Only
   the cells with no --b — the ones with nothing to compare against — darken.
   
   The values are the dark theme's --badge-bg and --fg-1, written out because
   they must NOT re-resolve when the theme flips and no token means "the other
   theme's surface". A deliberate exception to colours coming from tokens. */
[data-theme="light"] .inc-primary,
[data-theme="light"] .hero-brigade {
  background: var(--b, #2b333e);
  color: var(--bi, #e9edf2);
}

/* ── Loading ──────────────────────────────────────────────────────────── */
.spinner {
  display: inline-block;
  width: 16px; height: 16px;
  border: 2px solid var(--border-2);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin .7s linear infinite;
  vertical-align: middle;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ── Type breakdown rows ──────────────────────────────────────────────── */
.type-row { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.type-row:last-child { margin-bottom: 0; }
.type-dot, .inc-dot, .legend-dot, .sev-dot {
  width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0;
}
.type-label { flex: 1; font-size: var(--t-row-body); color: var(--fg-2); }
.type-count { font-size: var(--t-row-body); font-weight: 600; color: var(--fg-1); }

/* ── Bars ─────────────────────────────────────────────────────────────── */
.brig-row, .dist-row { display: flex; align-items: center; gap: 10px; margin-bottom: 9px; }
.brig-row:last-child, .dist-row:last-child { margin-bottom: 0; }
a.dist-row { border-radius: var(--radius-sm); padding: 3px 6px; margin: -3px -6px; color: inherit; }
a.dist-row:hover { background: var(--surface-2); text-decoration: none; }
.brig-name {
  font-size: var(--t-label); font-weight: 600;
  color: var(--fg-1);
  width: 72px; flex-shrink: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.brig-name:hover { color: var(--accent-ink); text-decoration: none; }
.dist-label { font-size: var(--t-label); color: var(--fg-3); width: 60px; flex-shrink: 0; }
.brig-bar-wrap, .dist-bar-wrap {
  flex: 1; height: 6px;
  background: var(--surface-3);
  border-radius: var(--radius-pill);
  overflow: hidden;
}
.brig-bar, .dist-bar { height: 100%; background: var(--accent); border-radius: var(--radius-pill); }
.brig-count, .dist-count {
  font-size: var(--t-label); font-weight: 600; color: var(--fg-2);
  width: 40px; text-align: right; flex-shrink: 0;
}

/* ── Incident rows ────────────────────────────────────────────────────────
 * .incident-row is a <div> on the list pages and a <tr> on the table pages.
 * The flex layout MUST stay scoped to the div form — display:flex on a <tr>
 * collapses the table's column model so headers stop lining up. */
div.incident-row {
  /* 15px, was 10 (owner, 2026-08-18): the chips ran too close to the
     address/pager text beside them. One value spaces every column, so the
     chips separate from the text without the chips drifting apart from
     each other by a different amount. */
  display: flex; align-items: center; gap: 15px;
  padding: 10px 12px;
  border-radius: var(--radius-md);
  border-bottom: 1px solid var(--border-1);
  /* Anchors the .inc-open overlay that makes the whole row a link to the
     incident's page. Every width, not just mobile — see .inc-open below. */
  position: relative;
}
div.incident-row:last-child { border-bottom: none; }
.incident-row-empty { height: 41px; }

/* ── The row as a link ────────────────────────────────────────────────────
 * Every incident has its own page, and the row is how you get there.
 *
 * The anchor (.inc-open) is invisible and carries no text: the row's existing
 * cells are the label, and wrapping them in the anchor instead would put the
 * badges and the clock inside a link, which reads terribly to a screen reader
 * ("link, 19:33, 16/08, FS87, Other, 2 STUD RD..."). The anchor has its own
 * aria-label naming the incident and its address, so it announces as one
 * meaningful destination.
 *
 * IT STRETCHES OVER THE ROW AT EVERY WIDTH. It was briefly a 0×0 anchor above
 * 600px, on the theory that the cursor and hover wash were affordance enough
 * and a full-row overlay would swallow text selection. That shipped a row that
 * looked clickable and did nothing — verified in a browser: clicking the
 * address on a 1500px viewport stayed on the district page. A pointer needs a
 * real target, so the overlay is unconditional.
 *
 * What sits ABOVE it keeps working: the pin button and the description are
 * lifted with z-index (see the narrow-viewport block), so directions are still
 * one click and the pager text can still be selected. */
.inc-open {
  position: absolute; inset: 0;
  border-radius: var(--radius-md);
  /* No text, so nothing to show — this is a hit area, and the row underneath
     is what the reader sees. */
  z-index: 0;
}
.inc-open:focus-visible {
  outline: 2px solid var(--accent); outline-offset: -2px;
}
/* ONLY the pin sits above the hit area.
 *
 * The pager text used to be lifted too, so it could be selected. That put the
 * biggest element in the row above the link and made most of the row dead:
 * clicking an address on a desktop did nothing, which is exactly the bug the
 * old mobile-only overlay was hiding. Selecting the raw CAD string is a real
 * use (people paste it into reports), but it is a rarer act than opening the
 * incident, and the whole string is still selectable on the incident page
 * itself — which is now one click away. */
.incident-row .inc-directions { position: relative; z-index: 1; }
/* Only rows that actually lead somewhere get the pointer. */
div.incident-row.has-incident-link { cursor: pointer; }
div.incident-row.has-incident-link:hover { background: var(--surface-2); }
/* The turnout clock. Filled and full height while the job is live (green ->
   yellow -> red), a plain two-line timestamp once it is over 20 minutes old
   and the clock stops meaning anything. --b/--bi are set inline by
   vfPaintAge; without them it falls back to the neutral treatment. */
.inc-time {
  width: 88px; flex-shrink: 0;
  align-self: stretch;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: 1px;
  font-size: var(--t-label);
  color: var(--bi, var(--fg-3));
  background: var(--b, transparent);
}
.inc-time .age-main { font-family: var(--font-mono); font-size: var(--t-num); font-weight: 600; }
.inc-time .age-sub  { font-size: var(--t-label); opacity: .85; }
.inc-time .age-sub:empty { display: none; }
/* Live jobs get the bigger numeral — it is the thing being watched. */
.inc-time.age-live .age-main { font-size: calc(var(--t-num) * 1.12); letter-spacing: .02em; }
/* Address first, pager text second.
   The raw CAD string is still the authoritative record and stays visible, but
   it is dense with unit codes, map refs and the incident ID — reading it to
   find the street was the main complaint about this feed. */
.inc-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.inc-address {
  font-size: var(--t-row-addr); font-weight: 600; color: var(--fg-1);
  line-height: 1.3; overflow-wrap: anywhere;
}
.inc-desc { font-size: var(--t-row-body); color: var(--fg-3); line-height: 1.5; overflow-wrap: anywhere; }
/* No confident address parsed — the pager text is all there is, so it gets
   the primary treatment rather than being demoted to nothing. */
.inc-address-missing {
  font-size: var(--t-row-body); color: var(--fg-2); line-height: 1.5; overflow-wrap: anywhere;
}

/* Type badge sits in the row's fixed column so the addresses line up. */
/* The type column IS the swatch: a full-height band of the type colour with
   an ink that reads on it, rather than a rounded pill floating in an empty
   cell. Placed after the badge rules so it wins the specificity tie. */
.type-badge.inc-type {
  flex-shrink: 0; width: 78px;
  align-self: stretch;
  display: flex; align-items: center; justify-content: center; gap: 6px;
  border: none;
  border-radius: 0;
  background: var(--b);
  color: var(--bi);
  letter-spacing: .04em;
}
/* Fire is the one badge that overrides its measured ink. inkOn() picks
   near-black on #ef4444 because it genuinely scores higher (5.20:1 against
   white's 3.76:1), but black on a saturated red reads as muddy rather than as
   an alarm, and this is the badge that most needs to carry across a room. The
   same call is already made for the turnout clock's red band and for
   CATASTROPHIC fire danger — three places now, all red.

   Keyed on data-type, set by the row builder, so the override is a property
   of the TYPE rather than of whatever hue happens to be in --b.

   The .inc-type / .hero-type compounds are named explicitly, and not for
   tidiness: those rules set `color: var(--bi)` at the same specificity as a
   bare `.type-badge[data-type]`, so the winner would be decided by source
   order alone. It already broke that way once — the list rows took the white
   ink and the hero card silently kept the black. Naming them lifts this to
   (0,3,0) and makes it immune to anything moving in this file. */
.type-badge[data-type="fire"],
.type-badge.inc-type[data-type="fire"],
.type-badge.hero-type[data-type="fire"] { color: var(--accent-fg); }

/* A generic escape hatch for the same decision on a badge whose hue is not one
   of the five incident types — the Emergency Victoria page sets it on its red
   and green statuses. Compounds named for the same source-order reason. */
.type-badge[data-ink="light"],
.type-badge.inc-type[data-ink="light"],
.type-badge.hero-type[data-ink="light"] { color: var(--accent-fg); }
[data-theme="light"] .type-badge[data-type="fire"],
[data-theme="light"] .type-badge.inc-type[data-type="fire"],
[data-theme="light"] .type-badge.hero-type[data-type="fire"] { color: var(--accent-fg); }

/* Lucide glyphs, stroked in the badge's own ink. Stroke width is 2.25 rather
   than Lucide's 2 — at this size the default reads as a smudge. */
.type-badge svg, .vf-icon {
  width: 15px; height: 15px; flex-shrink: 0;
  stroke: currentColor; fill: none;
  stroke-width: 2.25; stroke-linecap: round; stroke-linejoin: round;
}
/* nowrap suits short fixed labels ("Directions ↗"). */
/* Not --accent (crimson, the same value as Fire) and no longer --cat-3, which
   Alarm now owns. --cat-4 is the remaining hue no incident type claims, and
   green is off-limits because it means "safe". */
.inc-directions, .frv-link, .vic-link {
  font-size: var(--t-label); color: var(--cat-4); text-decoration: none; white-space: nowrap;
}
/* Attachment links carry long document names — nowrap pushed them to 483px in
   a 390px viewport and made the whole parks page scroll sideways. */
.cond-att-link {
  font-size: var(--t-label); color: var(--accent-ink); text-decoration: none;
  overflow-wrap: anywhere; min-width: 0;
}
.inc-directions:hover, .frv-link:hover, .vic-link:hover, .cond-att-link:hover { text-decoration: underline; }

/* Recency wash, tinted from the status tokens. */
/* Selection only. The age-based washes are gone: the turnout clock in the
   first column says how old a job is, and colouring the whole row for it as
   well meant two things competing to signal the same fact.
   One step off the card it sits on, so it reads as "this row" without
   implying a status. Static per theme, no accent, no age. */
div.incident-row.highlighted { background: var(--surface-2); }
.incident-row.highlighted td { background: color-mix(in srgb, var(--accent) 12%, transparent); }
.incident-row.fresh td       { background: color-mix(in srgb, var(--danger) 16%, transparent); }
.incident-row.recent td      { background: color-mix(in srgb, var(--warn) 14%, transparent); }

/* ── Current-incident hero (district page) ────────────────────────────────
 * From the 2026-08-11 design handoff: the newest turnout is promoted out of
 * the list and given the top of the page, with the map beside it. Geometry
 * (grid tracks, radii, type scale) follows the handoff; every colour resolves
 * to a token, and the elapsed badge keeps OUR turnout clock rather than the
 * handoff's always-red counter. */
.section-label-row {
  display: flex; align-items: center; gap: 9px;
  margin-bottom: 12px;
}
.section-label-row .section-label { margin-bottom: 0; }
/* home.html heads its sections with .section-title rather than
   .section-label; inside the flex row it needs the same margin reset or the
   heading sits 12px below its own baseline and the live readout beside it. */
.section-label-row .section-title { margin-bottom: 0; }
/* Sized to be read from a wall-mounted screen, not from a desk. */
.live-dot {
  width: 14px; height: 14px; border-radius: 50%;
  background: var(--accent); flex-shrink: 0;
  animation: vfblink 1.4s ease-in-out infinite;
}
@keyframes vfblink { 0%, 100% { opacity: 1 } 50% { opacity: .35 } }
.section-caption { font-size: var(--t-label); color: var(--fg-3); }
/* The label itself carries the accent here; elsewhere .section-label is muted. */
.section-label-row .section-label.is-live { color: var(--accent-ink); }
.section-label-row .section-label.is-live,
.section-label-row .section-label.is-recent { font-size: 17px; letter-spacing: .06em; }

/* Past the turnout clock's 20-minute window the job is history, so the header
   stops claiming otherwise: green and still, not red and pulsing. Green is
   reserved site-wide for "nothing to act on", which is exactly the state
   being described — the one other place it appears is the clock's first
   three minutes. */
.live-dot.is-recent { background: var(--ok); animation: none; }
.section-label-row .section-label.is-recent { color: var(--ok-ink); }

/* The map is BOUNDED, not proportional. It used to be
   `minmax(0, 1.25fr) minmax(0, 1fr)`, which held the map at 44.4% of the row
   at every width — so at 2560 it rendered 1020x563px of basemap for a single
   marker, and the hero consumed 628px of a 1000px viewport before one history
   row appeared. Sixty-three percent of the first screen spent on one incident,
   on a page whose job is the list.

   A locator's useful size does not grow with the viewport: past a few hundred
   pixels more basemap adds no information about where the job is. So the map
   takes a clamped track and the CARD takes everything left over — the same
   reasoning the measure caps already apply to prose, which this system bounds
   by content rather than by the page box.

   Measured effect (district/8, rail open):
     1600  map 593 -> 416px (44.4% -> 31.2%), card 741 -> 918
     2560  map 1020 -> 420px (44.4% -> 18.3%), card 1274 -> 1874

   The 260px floor is what keeps it a map rather than a sliver just above the
   900px breakpoint, where the rail is still taking ~230px. Below 900px the
   map is removed from the DOM entirely, so this track never applies there. */
.hero-grid {
  display: grid;
  /* The CARD is the bounded track and the MAP takes everything left over.
     This is the reverse of the previous rule, and deliberately so: the card
     only needs to be as wide as its text measure (108ch on .hero-msg, which
     resolves to ~1020px at 1920 and ~1360px at 2560), and every pixel past
     that was dead space sitting between the raw-data box and the card's right
     border. Giving the surplus to the map removes that space AND makes the map
     usefully bigger, instead of leaving a gap inside the card.
     55vw tracks the measure closely because both scale on the same vw curve —
     card content lands within ~15px of the cap at 1920 and 2560, so the
     raw-data box effectively fills the card and its padding reads even.
     The 560px floor keeps the card readable just above the 900px breakpoint,
     where the rail still takes ~230px; the 1410px ceiling is the measure at
     the point the type stops scaling, so past that the map takes it all. */
  /* The map keeps a 240px MINIMUM. Without it, just above the 900px
     breakpoint — where the rail still takes ~230px — the card claims its
     clamp maximum and leaves the map a 75px sliver. The card track's minimum
     is 0, so it yields here and the map gets its floor. */
  /* The card track is a VARIABLE because the header mirrors it — the weather
     strip is aligned to sit directly above the map, which means the header has
     to know where the map starts. Change it here and the header follows. */
  grid-template-columns: minmax(0, var(--hero-card)) minmax(240px, 1fr);
  gap: 16px; align-items: stretch;
}
.hero-card {
  display: flex; flex-direction: column;
  background: var(--surface-1);
  border: 1px solid var(--border-1);
  border-radius: var(--radius-lg);
  padding: 22px 24px;
  /* 392, not 360. The headline carries two lines now — the nature of the call
     over the address — and holding 360 meant shrinking the raw pager block,
     which was the wrong thing to pay with. At 392 the raw block is back at
     15px/1.65 and a two-line job info still fits without the card growing
     past the map beside it. Measure the rendered card across a few hundred
     real incidents before changing any size in here. */
  min-height: 392px;
}
.hero-badges { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
/* The turnout clock, in badge form. It shares .inc-time so the 15s ticker in
   tokens.js repaints it — the states and thresholds are identical to the feed,
   only the geometry differs. --b/--bi come from vfPaintAge. */
.inc-time.hero-elapsed {
  width: auto; align-self: auto;
  flex-direction: row; gap: 6px;
  padding: 8px 14px;
  border-radius: var(--radius-md);
  background: var(--b, var(--surface-3));
  color: var(--bi, var(--fg-2));
}
.inc-time.hero-elapsed .age-main { font-size: calc(var(--t-num) * 1.15); font-weight: 700; }
.inc-time.hero-elapsed .age-sub { font-size: var(--t-label); }
/* White on the red band, overriding the ink vfPaintAge measured. inkOn()
   picks near-black there because it genuinely scores higher (5.20:1 against
   3.76:1), but on the page's loudest badge white is what reads as an alarm —
   the same call already made for the Fire type badge and for CATASTROPHIC
   fire danger.

   EVERY clock, not just the hero's. A district at a busy moment shows several
   rows past +6 at once, and those were still black-on-red while the hero
   beside them was white.

   The compounds are named explicitly for the same reason the Fire badge names
   its own: .past-section .inc-time also sets `color`, at equal specificity to
   a bare .inc-time[data-age-state], so source order alone would decide the
   winner. That is exactly how the Fire badge broke on its first attempt. */
.inc-time[data-age-state="late"],
.past-section .inc-time[data-age-state="late"],
.inc-time.hero-elapsed[data-age-state="late"] { color: var(--accent-fg); }

/* GREEN TAKES NEAR-BLACK INK, by having no override at all.
   White on green was asked for on 2026-08-12, to match red. It proved hard to
   read in use, which is what the measurement said all along — 2.54:1 against
   near-black's 7.26:1 — and that instruction was retired on 2026-08-16 as
   belonging to the old design. The rule is REMOVED rather than inverted, so
   inkOn() picks the higher-contrast ink on its own; nothing here has to be
   kept in sync with the palette.

   Red keeps its white ink above: white on #ef4444 measures 4.06:1 against
   near-black's 5.17:1, so the two are close and the red band reads as an
   alarm, which is the point of it.

   Yellow was never in this list. White on #fbbf24 is 1.67:1 — effectively
   invisible — so the rolling band has always kept near-black at 11.04:1.

   The primary-brigade chip goes with it: it is the same green and the two
   have to match, which is why they shared one rule. */

/* The same chip in the history list, so the two clocks read as one control.
   Differences from the hero, both deliberate:
   - a FIXED width, because it heads a column and the brigade / type / address
     columns beside it have to line up. The hero is a lone badge and sizes to
     its own text.
   - the width is set for the widest state it has to hold, "19:27 11/08"
     side by side; the live "+7" states then sit centred in it.
   Everything else — radius, padding, gap, row layout, type sizes, the --b/--bi
   fill — is the hero's, so a change there carries across on its own. */
.past-section .inc-time {
  align-self: center;
  flex-direction: row; gap: 6px;
  padding: 8px 14px;
  border-radius: var(--radius-md);
  background: var(--b, var(--surface-3));
  color: var(--bi, var(--fg-2));
}
.past-section .inc-time .age-main { font-size: var(--t-num); font-weight: 700; }
.past-section .inc-time.age-live .age-main { letter-spacing: .02em; }
.past-section .inc-time .age-sub  { font-size: var(--t-label); }
/* 98px, was 116 (owner, 2026-08-18: reclaim chip padding, do NOT shrink
   the numerals). Sized from measurement, not the requested straight 25%:
   the widest face — "19:05 Today", the expired clock — measures 91px of
   content at 1600, so 87px would clip it and 98 leaves ~3.5px air a side.
   The font ramp is untouched; only the slack went. */
@media (min-width: 601px) { .past-section .inc-time { width: 98px; } }
.hero-brigade {
  font-family: var(--font-mono);
  font-size: 14px; font-weight: 700; letter-spacing: .06em;
  padding: 8px 16px;
  border-radius: var(--radius-md);
  background: var(--b, var(--badge-bg));
  color: var(--bi, var(--fg-1));
}
.type-badge.hero-type {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: 14px; font-weight: 700; letter-spacing: .06em;
  padding: 8px 16px;
  border: none; border-radius: var(--radius-md);
  background: var(--b); color: var(--bi);
}
.type-badge.hero-type svg { width: 18px; height: 18px; }
/* Two lines, right-aligned: the time of the call over the incident number. */
.hero-meta {
  margin-left: auto; text-align: right;
  font-size: 13px; color: var(--fg-3);
  line-height: 1.35;
}
.hero-meta-time {
  font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  font-size: 15px; font-weight: 600; color: var(--fg-2);
}
.hero-meta-id { font-family: var(--font-mono); }
/* The headline is two lines now — the nature of the call over the address —
 * because what the job IS matters as much as where it is, and it was only
 * readable by picking it out of the raw CAD string by eye.
 *
 * Both are smaller than the single 34px address line they replace, so the
 * card does not grow: two lines at 34px would have pushed it past its 360px
 * floor on any job with a long description. Still bold, so the pair still
 * reads as the headline rather than as body copy. Measure the rendered card
 * height before changing either size. */
.hero-head { margin-top: 16px; }
/* Equal size, because the two are equally important; the job line is one
   weight step heavier, which is the whole difference between them.
   Sized from --t-hero, whose 26px cap is the largest size at which the
   ADDRESS still fits one line for ~93% of real incidents — re-measured over
   138 live POCSAG addresses (p90 42 chars, p93 47) against the hero's actual
   width. The job line only makes one line 67% of the time at any readable
   size — its 90th percentile is 80 characters against the address's ~30 — so
   it is allowed to wrap rather than be clipped. See the note on .hero-card
   about the height budget, and --t-hero before changing the cap. */
.hero-job {
  font-size: var(--t-hero); font-weight: 800;
  line-height: 1.3; letter-spacing: -.01em;
  color: var(--fg-1);
  overflow-wrap: anywhere;
}
.hero-address {
  font-size: var(--t-hero); font-weight: 700;
  line-height: 1.2; letter-spacing: -.02em;
  color: var(--fg-2);
  overflow-wrap: anywhere;
}
.hero-job + .hero-address { margin-top: 4px; }
.hero-address:empty { display: none; }
/* The raw CAD string stays visible — it is the authoritative record — but is
   set back into the page so the address stays the headline. */
.hero-msg {
  /* --inset, not --bg. Under the old theme the light cards stepped UP from the
     page ground (#ffffff on #f6f7f9), so painting this panel --bg recessed it.
     The palette inverts that — light cards now step DOWN (#e7e7e7 on #F7F7F7)
     — which would have left the "recessed" panel LIGHTER than the card holding
     it. --inset is deeper than the ground in both modes, so the panel reads as
     cut into the card either way. */
  background: var(--inset);
  border: 1px solid var(--border-1);
  /* --radius-md, not the bespoke 10px this used to carry — the only 10px in
     the codebase and the one radius with no reason recorded beside it. This
     panel is nested INSIDE the hero card, which is --radius-lg (12px), so the
     inner corner must be the smaller step or the two curves fight. */
  border-radius: var(--radius-md);
  padding: 16px 18px;
  margin-top: 16px;
  font-size: var(--t-hero-msg); line-height: 1.6;
  color: var(--fg-2);
  overflow-wrap: anywhere;
}
.hero-safety { color: var(--danger-ink); font-weight: 600; }
.hero-actions {
  display: flex; align-items: center; gap: 20px; flex-wrap: wrap;
  margin-top: auto; padding-top: 18px;
}
/* One column, for a page whose hero has no map beside it.
 *
 * index.html dropped its Leaflet map on 2026-08-16: it cost ~150 KB plus tile
 * requests on every load to show one district, which the dedicated /map page
 * already does properly. A modifier rather than a change to .hero-grid,
 * because district.html still has its map and shares that rule. */
.hero-grid-solo { grid-template-columns: 1fr; }

/* With no map beside it, the card has nothing to match heights with, and the
   392px floor below becomes dead space under the raw pager block — clearly
   visible as a large empty gap. The content still sets a sensible minimum on
   its own (two headline lines plus the raw block), so the floor is dropped and
   the reclaimed vertical space goes to the incident list, which is the point
   of removing the map. */
.hero-grid-solo .hero-card { min-height: 0; }

.hero-mapref { margin-left: auto; font-size: var(--t-hero-meta); color: var(--fg-3); }

.hero-map {
  position: relative; min-height: 392px;
  border: 1px solid var(--border-1);
  border-radius: var(--radius-lg);
  background: var(--surface-2);
  overflow: hidden;
}
.hero-map-caption {
  position: absolute; left: 10px; bottom: 10px; z-index: 500;
  background: color-mix(in srgb, var(--surface-1) 92%, transparent);
  border: 1px solid var(--border-1);
  border-radius: var(--radius-md);
  padding: 8px 12px;
  font-size: var(--t-label); font-weight: 600; color: var(--fg-2);
}
.hero-empty {
  display: flex; align-items: center; justify-content: center;
  color: var(--fg-3); font-size: 14px;
}

/* ── History rows: brigade + type as hero-style buttons ───────────────────
 * On the district page these two cells match the hero's chips rather than
 * being full-height bands of colour — same radius, weight and letter-spacing,
 * so one visual language covers the card and the list beneath it.
 *
 * Scoped to .past-section. The full-height band is still what /index and the
 * brigade pages use, where the row is denser and the colour is doing more
 * work; changing those was not asked for.
 *
 * The columns are WIDER than the 78px used elsewhere because the button form
 * costs horizontal room: the type cell carries an icon plus a heavier label,
 * and "Medical" is the one that decides it. Measure before narrowing. */
.past-section .inc-primary,
.past-section .type-badge.inc-type {
  align-self: center;
  border-radius: var(--radius-md);
  padding: 8px 0;
  font-size: var(--t-row-body); font-weight: 700;
  letter-spacing: .06em;
}
/* Widened with the type scale: --t-row-body caps at 16.5px against the 13px
   this was measured at, and "MEDICAL" is still the label that decides it.
   Verified at 390 / 768 / 1500 / 1920 with no clipping. */
/* 62px, was 92 (owner, 2026-08-18, same measured treatment as the clock
   chip): the widest real content is a 6-character pager code — 53px of
   mono at this size — so 30px of every chip was slack. Numerals and codes
   keep their font. */
.past-section .inc-primary { width: 62px; }
/* Uppercase to match the hero chip. It costs width — measure the longest
   label ("MEDICAL") against this column before touching either number.
   (Re-measured 2026-08-18: MEDICAL + icon + gap is 93px at this size, so
   102 is already the fit and stays.) */
.past-section .type-badge.inc-type { width: 102px; gap: 7px; text-transform: uppercase; }
.past-section .type-badge.inc-type svg { width: 16px; height: 16px; }

/* ── Stat strip ───────────────────────────────────────────────────────────
 * Replaces the three stacked panels that used to sit left of the map. Same
 * numbers, one row: totals on the left, top brigades over type counts on the
 * right. Tinted chrome rather than a card, so it reads as page furniture. */
.stat-strip {
  display: flex; align-items: center; justify-content: space-between;
  gap: 32px; flex-wrap: wrap;
  background: var(--surface-2);
  border: 1px solid var(--border-1);
  border-radius: var(--radius-lg);
  padding: 16px 22px;
}
.strip-totals { display: flex; align-items: center; gap: 26px; }
.strip-divider { width: 1px; align-self: stretch; background: var(--border-2); }
.strip-num {
  font-size: 30px; font-weight: 800;
  letter-spacing: -.03em; line-height: 1;
  color: var(--fg-1);
}
.strip-label {
  font-size: var(--t-label); font-weight: 600;
  text-transform: uppercase; letter-spacing: .07em;
  color: var(--fg-3);
  margin-top: 5px;
}
.strip-right { display: flex; flex-direction: column; align-items: flex-end; gap: 9px; }
.strip-brigades { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; justify-content: flex-end; }
.strip-brig { font-size: 14px; font-weight: 700; letter-spacing: .05em; color: var(--fg-2); }
.strip-brig:hover { text-decoration: none; color: var(--accent-ink); }
.strip-brig span { color: var(--fg-3); font-weight: 600; margin-left: 5px; }
.strip-types { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; justify-content: flex-end; }
.strip-type { display: flex; align-items: center; gap: 6px; font-size: 13.5px; color: var(--fg-2); }
.strip-type b { color: var(--fg-1); font-weight: 700; }

/* ── Directions as an icon button ─────────────────────────────────────────
 * Handoff treatment for the history rows: the pin replaces the text link so
 * the message column gets the width back. Still the same anchor, so the
 * stretched-overlay tap target below 600px is unaffected.
 *
 * .hero-nav is the same button on the current-incident card. It is styled
 * here rather than given the .inc-directions class, because that class picks
 * up a `::after { position:absolute; inset:0 }` overlay below 600px which
 * anchors to the nearest positioned ancestor — a row has one, the hero card
 * does not, so the overlay would escape and make most of the page a
 * directions link. */
.inc-directions.inc-directions-icon, .hero-nav {
  width: 44px; height: 44px; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--radius-md);
  background: var(--badge-bg);
  color: var(--fg-1);
}
[data-theme="light"] .inc-directions.inc-directions-icon,
[data-theme="light"] .hero-nav { background: #2b333e; color: #e9edf2; }
.inc-directions.inc-directions-icon:hover, .hero-nav:hover {
  background: var(--accent); color: var(--accent-fg); text-decoration: none;
}
.inc-directions.inc-directions-icon svg, .hero-nav svg { width: 20px; height: 20px; }

/* ── Pager ────────────────────────────────────────────────────────────── */
.pager {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px 16px; flex-wrap: wrap;
  padding: 12px 16px;
  border-top: 1px solid var(--border-1);
}
.pager-info { font-size: var(--t-label); color: var(--fg-3); }
.pager-btns { display: flex; gap: 8px; }

/* ── Tables ───────────────────────────────────────────────────────────── */
.frv-table, .vic-table, .disr-table { width: 100%; border-collapse: collapse; }
.frv-table th, .vic-table th, .disr-table th {
  font-size: var(--t-label); font-weight: 700;
  text-transform: uppercase; letter-spacing: .08em;
  color: var(--fg-3);
  padding: 11px 12px;
  border-bottom: 1px solid var(--border-1);
  text-align: left; white-space: nowrap;
}
.frv-table td, .vic-table td, .disr-table td {
  padding: 12px 12px;
  border-bottom: 1px solid var(--border-1);
  font-size: var(--t-row-body); vertical-align: middle;
  color: var(--fg-2);
}
.frv-table tr:last-child td, .vic-table tr:last-child td, .disr-table tr:last-child td { border-bottom: none; }
.frv-table tbody tr:hover td, .vic-table tbody tr:hover td, .disr-table tbody tr:hover td {
  background: var(--surface-2);
}
/* The cap is on .cell-measure, a child block, NOT on the cell. Under
   table-layout:auto the automatic algorithm treats max-width on a <td> as a
   hint and routinely exceeds it — the 560px that used to be here computed
   correctly and was then ignored, with a cell rendering 1240px against a
   declared 697px. It only ever looked like it worked because the page was
   capped at 1440px; removing that box is what exposed it. */
.desc-cell { white-space: normal; word-break: break-word; }
.vic-name-cell { font-weight: 600; color: var(--fg-1); }
.vic-loc-cell { white-space: normal; word-break: break-word; }
.no-data { color: var(--fg-3); padding: 24px 12px; }

/* ── Weather widget ───────────────────────────────────────────────────── */
/* METRIC CELLS on ONE LINE (owner, 2026-08-18, second pass: "single line
   not multi line" — the first pass stacked label/value/detail like the
   GuardOps wall board and was rejected; the board is a 132px wall header,
   this is a slim nav bar). Each cell reads left to right on one baseline:
   muted icon · small caps label · big value · small detail, with vertical
   rules between cells. The approved TWO-COLOUR scheme is unchanged: --fg-1
   for values, --fg-3 for everything else; the AFDRS badge keeps its
   official colour. Sizes stay the same coupled clamps — only the axis
   changed, so flipping back to stacked is one flex-direction. */
/* ONE type size for the entire strip (owner, 2026-08-18, third pass and
   explicit: label, value and detail are the SAME size, and the icon is
   matched to it — no per-part hierarchy). --wx-fs is the single dial;
   everything below derives from it, so it CANNOT drift apart again. What
   still distinguishes the parts is colour and weight only: values in
   --fg-1 bold, labels and details in --fg-3 — the approved two-colour
   scheme. Min 14px reads on a desktop, cap 22px on a wall display. */
.weather-widget {
  --wx-fs: clamp(14px, .85vw, 22px);
  display: flex; align-items: center; gap: 12px; margin-left: auto;
  font-size: var(--wx-fs);
}
.wx-item { display: flex; align-items: center; white-space: nowrap; }
.wx-cell { gap: 8px; }
.wx-divider { width: 1px; height: calc(var(--wx-fs) * 1.3); background: var(--border-2); flex-shrink: 0; }
.wx-icon { display: inline-flex; line-height: 1; color: var(--fg-3); align-self: center; }
/* Icons at 1.3x the text size — the ratio the theme toggle's glyph has to
   its neighbouring text — applied to EVERY icon, the TOMORROW mini one
   included. .wx-icon-sm is dead but harmless in old markup. */
.wx-icon svg, .wx-icon-sm svg { width: calc(var(--wx-fs) * 1.3); height: calc(var(--wx-fs) * 1.3); display: block; }
.wx-col { display: flex; flex-direction: row; align-items: baseline; gap: 7px; line-height: 1.15; }
.wx-label {
  font-size: var(--wx-fs); font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase; color: var(--fg-3);
}
.wx-value-row {
  display: flex; align-items: baseline; gap: 5px;
  font-size: var(--wx-fs); font-weight: 700;
  color: var(--fg-1); font-variant-numeric: tabular-nums;
}
/* The mini icon in the TOMORROW value row is the one non-text flex item on
   the baseline row; centre it on itself or it hangs from the baseline. */
.wx-value-row .wx-icon { align-self: center; }
.wx-detail { font-size: var(--wx-fs); color: var(--fg-3); font-variant-numeric: tabular-nums; }
@media (max-width: 900px) {
  .weather-widget { display: none; }
  /* The strip is gone, so its boundary goes with it. */
  .weather-widget + .header-actions::before { display: none; }
}

/* ── Legend + filters ─────────────────────────────────────────────────── */
.map-legend { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 10px; }
.legend-item { display: flex; align-items: center; gap: 6px; font-size: var(--t-label); color: var(--fg-3); }
.filter-bar, .type-filters, .list-header { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-bottom: 12px; }
.last-updated { font-size: var(--t-label); color: var(--fg-3); margin-left: auto; }

/* ── Mobile tab bar ───────────────────────────────────────────────────── */
.tab-bar {
  display: flex;
  background: var(--surface-1);
  border-top: 1px solid var(--border-1);
}
.tab-btn {
  flex: 1; padding: 10px 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
  font-family: inherit; font-size: var(--t-label); font-weight: 600;
  background: transparent; border: none;
  color: var(--fg-3); cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.tab-btn.active { color: var(--accent-ink); }

/* ── Leaflet ──────────────────────────────────────────────────────────────
 * No tile filter here: the basemap STYLE follows the theme (Mapbox dark-v11
 * in dark, light-v11 in light/hybrid), so the map matches without any
 * post-processing. */
.leaflet-container { background: var(--surface-2); font-family: var(--font-sans); }

.leaflet-tooltip,
.leaflet-tooltip.frv-tt, .leaflet-tooltip.vic-tt,
.leaflet-tooltip.disr-tt, .leaflet-tooltip.coc-tt {
  background: var(--surface-1) !important;
  border: 1px solid var(--border-2) !important;
  color: var(--fg-1) !important;
  border-radius: var(--radius-md) !important;
  padding: 10px 12px !important;
  font-family: var(--font-sans) !important;
  box-shadow: var(--shadow-modal) !important;
  white-space: normal !important;
}
.leaflet-tooltip { width: 300px !important; }
.leaflet-tooltip.frv-tt, .leaflet-tooltip.vic-tt,
.leaflet-tooltip.disr-tt, .leaflet-tooltip.coc-tt { width: 380px !important; max-width: 380px !important; }
.leaflet-tooltip::before { display: none !important; }

.tt-brigade, .frv-tt-name, .vic-tt-name, .disr-tt-name, .coc-tt-name, .coc-tt-title {
  font-weight: 700; font-size: var(--t-row-addr); margin-bottom: 5px; color: var(--fg-1);
}
.tt-meta { color: var(--fg-3); font-size: var(--t-label); margin-bottom: 4px; }
.tt-desc { font-size: var(--t-row-body); color: var(--fg-2); word-break: break-word; line-height: 1.45; }
.frv-tt-row, .vic-tt-row, .disr-tt-row { display: flex; gap: 8px; margin-bottom: 3px; font-size: var(--t-label); }
.frv-tt-label, .vic-tt-label, .disr-tt-label { color: var(--fg-3); min-width: 60px; flex-shrink: 0; }
.frv-tt-val, .disr-tt-desc, .coc-tt-sub { color: var(--fg-2); }

.leaflet-popup-content-wrapper {
  background: var(--surface-1) !important;
  color: var(--fg-1) !important;
  border: 1px solid var(--border-2) !important;
  border-radius: var(--radius-lg) !important;
  box-shadow: var(--shadow-modal) !important;
}
.leaflet-popup-tip { background: var(--surface-1) !important; }
.leaflet-popup-content { font-family: var(--font-sans); font-size: var(--t-row-body); line-height: 1.5; }

.leaflet-control-zoom a {
  background: var(--surface-1) !important;
  border-color: var(--border-2) !important;
  color: var(--fg-1) !important;
}
.leaflet-control-zoom a:hover { background: var(--surface-2) !important; }
.leaflet-control-attribution {
  background: color-mix(in srgb, var(--surface-1) 85%, transparent) !important;
  color: var(--fg-3) !important;
}
.leaflet-control-attribution a { color: var(--fg-3) !important; }

/* Fallback only: no MAPBOX_TOKEN means OSM raster, which is a light basemap.
   Under the dark theme that reads as a hole in the page, so it gets filtered.
   With a token this class is never set and the real dark-v11 style is used. */
.vf-tiles-dim .leaflet-tile-pane {
  filter: invert(1) hue-rotate(180deg) brightness(.85) contrast(.9) saturate(.55);
}

/* Map containers */
.map-panel, #district-map, #brigade-map, #frv-map, #vic-map,
#disr-map, #coc-map, #emevic-map {
  border: 1px solid var(--border-1);
  border-radius: var(--radius-lg);
  background: var(--surface-2);
}


/* ── Narrow viewports ─────────────────────────────────────────────────────
 * The incident row stacks below 600px. Side by side, its fixed-width children
 * (time 88 + brigades + directions 68, plus gaps) consume nearly the whole
 * row, leaving the description ~27px — which word-break then shredded into
 * one character per line, making rows 1542px tall against 80px on desktop.
 * Two lines instead: a meta line, then the description at full width. */
@media (max-width: 600px) {
  div.incident-row {
    display: grid;
    grid-template-columns: auto auto 1fr;
    grid-template-areas:
      "type  brig  link"
      "time  time  time"
      "body  body  body";
    align-items: center;
    gap: 6px 8px;
    padding: 12px;
  }
  .inc-time       { grid-area: time; width: auto; }
  /* The chip hugs its own text here. Its grid area spans the full row, and a
     stretched flex item filled the lot — a 308px-wide time badge on a 390px
     screen. */
  .past-section .inc-time { justify-self: start; }
  .inc-body       { grid-area: body; }
  /* Stacked, so there is nothing to align to — both cells size to content and
     keep their fill rather than stretching to a phantom column height. */
  .inc-type       { grid-area: type; width: auto; align-self: center; padding: 3px 10px; }
  .inc-primary    { grid-area: brig; width: auto; align-self: center; padding: 3px 10px; }
  .inc-directions { grid-area: link; justify-self: end; }

  /* Tapping anywhere on the incident OPENS THE INCIDENT, and the pin button
     beside it is what starts navigation.

     This used to stretch the DIRECTIONS anchor over the whole row, so a tap
     anywhere handed the address to a maps app. That was the best available
     action when a row led nowhere; now that every incident has a page, the
     row's own destination is that page, and sending a reader to Google Maps
     when they tapped an incident would be a surprise.

     Same technique, applied to the incident link instead: a real anchor
     stretched over the row, so it keeps keyboard focus, Enter activation,
     long-press-to-share and open-in-new-tab. The row is position:relative for
     the overlay to anchor to; the pin sits ABOVE the overlay (z-index) so it
     stays independently tappable, and the description does too so text can
     still be selected. */
  .inc-directions { position: relative; z-index: 1; }

}

/* ── Mobile district picker ───────────────────────────────────────────────
 * Below 900px the sidebar is hidden, which on home.html and district.html
 * left no way at all to change district. A native <select> rather than a tab
 * bar or drawer: one row of height, 20 options without a scroll trap, and
 * keyboard/screen-reader behaviour for free. index.html keeps its Filters
 * tab, which already does brigades as well as districts. */
/* ── Maps are desktop-only ────────────────────────────────────────────────
 * Below 900px the map is dropped rather than shrunk. On a phone it was a
 * 260px band of mostly-empty basemap above the content that added nothing
 * the list doesn't already say, and it cost a screenful of scrolling before
 * the first incident. Hiding it also stops Leaflet fetching tiles over mobile
 * data — see the matching guards in the page scripts, which skip map
 * initialisation entirely at this width. */
@media (max-width: 900px) {
  .map-section,
  .map-col,
  .hero-map,
  #district-map, #brigade-map { display: none; }

  /* The Map tab on index.html has nothing left to show. */
  .tab-btn[data-tab="map"] { display: none; }
}

/* ── Hero on narrow viewports ─────────────────────────────────────────────
 * The handoff specifies no mobile breakpoint — it floors the main column at
 * 1080px and lets the page scroll sideways. That is not carried over: this
 * site fits a 390px viewport on every route and regressing that would cost
 * the phone reading, which is where a pager feed is actually read. The hero
 * collapses to one column instead, and the map goes with every other map. */
@media (max-width: 900px) {
  .hero-grid { grid-template-columns: 1fr; }
  .hero-card { min-height: 0; }
}
@media (max-width: 600px) {
  .hero-card { padding: 16px; }
  .hero-head { margin-top: 12px; }
  /* REMOVED: `.hero-job, .hero-address { font-size: 17px }`.
     This shrank the hero from 20px to 17px under 600px — it made the most
     important text on the site SMALLEST on the screen where it is most often
     read, standing on a driveway at night. The type scale's clamp minimum
     (--t-hero, 19px; 21px inside .main-col) is now the phone size, and the
     hero keeps a size that carries at arm's length. */
  .section-label-row .section-label.is-live,
  .section-label-row .section-label.is-recent { font-size: 14px; }
  .live-dot { width: 11px; height: 11px; }
  /* Was 13.5px. The raw CAD string is the dispatch itself, not a caption,
     so it holds --t-hero-msg's 14.5px floor on a phone too. */
  .hero-msg { padding: 13px 14px; }
  /* Both are margin-left:auto on one line at desktop width; stacked they read
     as orphans pushed to the right edge. */
  .hero-meta, .hero-mapref { margin-left: 0; }
  /* ── Mobile hero badge row tidy-up (2026-08-18) ──
     At phone width the badge row wraps, which exposed two things the desktop
     row hides: a brigade chip with no text renders as a stray empty pill
     (some POCSAG rows resolve no primary brigade), and once the turnout
     clock expires it shows the call time itself — so the meta block's time
     line said the same thing twice, two lines apart. Both fixes are scoped
     here so the desktop row is untouched. */
  .hero-brigade:empty { display: none; }
  .hero-meta { display: flex; align-items: baseline; gap: 10px; flex-basis: 100%; }
  .inc-time.hero-elapsed:not([data-age-state]) ~ .hero-meta .hero-meta-time { display: none; }
  /* The wordmark yields to the scope trigger on a phone; the icon stays.
     Without this the trigger and buttons crushed the logo to "V..". */
  .header .logo span { display: none; }
  .hero-actions { gap: 14px; padding-top: 14px; }
  .stat-strip { gap: 16px; padding: 14px 16px; }
  .strip-right { align-items: flex-start; }
  .strip-brigades, .strip-types { justify-content: flex-start; }
  .strip-num { font-size: 24px; }
}


/* ── Wide screens: scale the type, cap the measure ────────────────────────
 * district.html has no max-width, so on a 4K wall screen the layout fills
 * 3840px. Left alone that is worse than the cap it replaced: a paragraph set
 * across 2000px is a thin line nobody can track back to the start of, and the
 * type stays sized for a desk while the screen is read from across a room.
 *
 * Two mechanisms, neither of which is a breakpoint:
 *
 * 1. FLUID TYPE. clamp(min, vw, max) — the min is exactly today's value, so
 *    nothing changes until the viewport passes ~1750px, and the max stops it
 *    before the type turns into signage. A 4K panel viewed at 3m subtends
 *    about what a 1080p monitor does at 1m, so bigger type on a bigger screen
 *    is the point, not a side effect.
 *
 * 2. MEASURE CAPS. ch units, so they scale WITH the type: whatever the font
 *    size, prose stops at a readable number of characters and the rest of the
 *    row is left empty. This is what actually prevents the thin-line problem;
 *    the fluid type alone would only slow it down.
 *
 * Scoped to .main-col — only district.html has one. The other six pages keep
 * their 1440px cap, and vw units would otherwise scale their text against a
 * viewport their layout does not use.
 *
 * Verify by rendering at 1600 / 1920 / 2560 / 3840 and reading the character
 * count per line, not by eyeballing the font size. */
.main-col .hero-job,
.main-col .hero-address   { font-size: clamp(21px, 1.15vw, 29px); }
/* The cap is on the CHILD, not on .hero-msg. .hero-msg is the bordered panel;
   capping it stopped the panel growing at ~1750px while the card kept going,
   so the card's padding went asymmetric — 24px left against 187px right at
   1920, and 490px right at 2560. The panel now fills the card and the prose
   is bounded inside it. See the matching note in renderPagerText. */
.main-col .hero-msg { font-size: clamp(15px, .85vw, 21px); }
.main-col .hero-msg .cell-measure { max-width: 108ch; }
.main-col .hero-meta-time { font-size: clamp(14px, .78vw, 19px); }
.main-col .hero-brigade,
.main-col .type-badge.hero-type { font-size: clamp(14px, .72vw, 19px); }
.main-col .type-badge.hero-type svg { width: 1.05em; height: 1.05em; }

/* The headline is a single line by design at these widths, so it gets a
   longer measure than the raw block — but still a bounded one. */
.main-col .hero-job,
.main-col .hero-address { max-width: 92ch; }

.main-col .strip-num   { font-size: clamp(30px, 1.55vw, 44px); }
.main-col .strip-label { font-size: clamp(11px, .58vw, 15px); }
.main-col .strip-brig  { font-size: clamp(14px, .72vw, 19px); }
.main-col .strip-type  { font-size: clamp(13.5px, .7vw, 18px); }

.main-col .inc-address { font-size: clamp(16px, .82vw, 20px); max-width: 96ch; }
.main-col .inc-desc    { font-size: clamp(14px, .72vw, 17.5px); max-width: 132ch; }
.main-col .inc-address-missing { font-size: clamp(14px, .72vw, 17.5px); max-width: 132ch; }
/* Both clocks scale together — they are the same chip, so one set of sizes.
   Only the WIDTH differs: the hero sizes to its text, the row cell heads a
   column and is fixed. :not(.hero-elapsed) is load-bearing on that width rule.
   Without it the hero was pinned to the column width, 88px around 81px of
   content, and its declared padding had nowhere to go — with border-box that
   reads as lost padding rather than as a wrong width. */
.main-col .inc-time .age-main { font-size: clamp(15px, .80vw, 21px); }
.main-col .inc-time .age-sub  { font-size: clamp(12.5px, .64vw, 16px); }
.main-col .inc-time { padding: 8px clamp(14px, .8vw, 20px); }
/* Guarded: below 601px the row stacks and the cell is a grid area that sizes
   itself, so a fixed width here would beat the stacked layout's `width: auto`. */
@media (min-width: 601px) {
  /* Mins/maxes follow the 98px base above: expired-face content measures
     116px at 2560, so the 130px cap keeps ~7px air; 5.8vw tracks the type
     ramp between (needs ~107px at 1920, gets 111). */
  .main-col .past-section .inc-time { width: clamp(98px, 5.8vw, 130px); }
}

/* The chips keep their fixed columns so addresses stay aligned; the columns
   themselves grow with the type rather than stranding a 13px label in the
   middle of an oversized row. */
.main-col .past-section .inc-primary,
.main-col .past-section .type-badge.inc-type { font-size: clamp(14px, .70vw, 18px); }
/* Mins and caps sized from the widest REAL content + ~8px air (owner,
   2026-08-18): a 6-char code measures 53px at 1600 and 72px at 2560;
   MEDICAL with its icon measures 93px and 117px. The vw coefficients track
   the chips' own font clamp between those points. All reclaimed width is
   slack — fonts untouched. */
.main-col .past-section .inc-primary         { width: clamp(62px, 3.2vw, 82px); }
.main-col .past-section .type-badge.inc-type { width: clamp(102px, 5.3vw, 126px); }
.main-col .past-section .type-badge.inc-type svg { width: 1.15em; height: 1.15em; }
.main-col .inc-directions.inc-directions-icon {
  width: clamp(44px, 2.6vw, 62px); height: clamp(44px, 2.6vw, 62px);
}
.main-col .inc-directions.inc-directions-icon svg { width: 50%; height: 50%; }
.main-col .hero-nav { width: clamp(44px, 2.6vw, 62px); height: clamp(44px, 2.6vw, 62px); }
.main-col .hero-nav svg { width: 50%; height: 50%; }

/* REMOVED 2026-08-14: `@media (min-width:1900px) { .hero-card, .hero-map {
   min-height: clamp(392px, 22vw, 620px) } }`.
   Its own reason was "the hero card's floor is a fixed 392px, which on a 4K
   panel is a letterbox strip NEXT TO A VERY WIDE MAP" — it grew the whole hero
   so the card kept its proportion beside a map that was 44.4% of the row.
   The map is now bounded to 420px, so that premise is gone, and all the rule
   did was hold empty space open: measured 188px of nothing on /district at
   2560, 120px on /index, 100px at 1920.
   The base 392px floor (see .hero-card) stays — it is still binding on /index,
   whose content measures exactly 392 at 1600, so it is doing real work. */


/* ── Wide screens: the table and home pages ───────────────────────────────
 * The same contract the .main-col block above sets for district.html and
 * index.html, applied to the remaining five so the site behaves the same way
 * everywhere: no box around the page, type that scales with the screen, and a
 * bounded measure so prose never runs the full width of a 4K panel.
 *
 * Two differences from .main-col, both forced by what these pages contain:
 *
 * - The prose caps are on TABLE CELLS, which already carried px caps
 *   (.desc-cell 560px, .vic-loc-cell 420px). Those are re-expressed in ch at
 *   roughly the same width they had at 13px, so they now scale WITH the type
 *   instead of pinning to a size the page no longer uses.
 *
 * - A table spread across 3840px with a capped description column is mostly
 *   gap, so the table itself is bounded rather than only its text. Wide
 *   enough that nothing reflows at ordinary sizes; the cap only bites past
 *   about 2200px. */
.page-content .frv-table th, .page-content .vic-table th, .page-content .disr-table th,
.page-content .frv-table td, .page-content .vic-table td, .page-content .disr-table td {
  font-size: clamp(13px, .62vw, 17px);
}
.page-content .frv-table th, .page-content .vic-table th, .page-content .disr-table th {
  font-size: clamp(11px, .52vw, 14px);
}
.desc-cell .cell-measure    { max-width: 85ch; }
.vic-loc-cell .cell-measure { max-width: 64ch; }
.page-content .frv-table, .page-content .vic-table, .page-content .disr-table {
  max-width: 2200px;
}
/* The section headings and controls above the tables scale with them, or a
   4K page reads as small chrome over large content. */
.page-content .section-label, .page-content .section-title { font-size: clamp(11px, .52vw, 14px); }
.page-content .last-updated, .page-content .legend-item { font-size: clamp(12px, .56vw, 15px); }

/* home.html: the stat column is a fixed track and the cards inside it are
   already narrow, so only the right-hand column can run away. */
.home-content .main { max-width: 2200px; }
.home-content .stat-big { font-size: clamp(34px, 1.7vw, 48px); }
.home-content .type-label, .home-content .type-count,
.home-content .dist-label, .home-content .brig-name, .home-content .brig-count {
  font-size: clamp(12px, .58vw, 16px);
}
/* The fixed label/number columns have to grow with the type or the text wraps
   inside them — scaling .dist-label alone put "District 8" on two lines at
   2560. Measured, not guessed: these are the widest strings each column holds
   ("District 14", a four-letter brigade code, a three-digit count). */
.home-content .dist-label { width: clamp(60px, 3.4vw, 86px); }
.home-content .brig-name  { width: clamp(72px, 4vw, 100px); }
.home-content .brig-count, .home-content .dist-count { width: clamp(40px, 2.4vw, 58px); }


/* ── History date filter ──────────────────────────────────────────────────
 * Lives in the card FOOTER beside the pager, not above the list. Above it, it
 * pushed the whole table down the page for a control most visits never touch.
 * The archive runs to roughly six months, which is 441 pages of Prev/Next on a
 * busy district — First/Last reaches the ends, this reaches the middle.
 *
 * `min` is set from the scope's own first incident and `max` to today, so a
 * date outside the archive cannot be picked. Picking one silently returned an
 * empty page, which is the same "it looks missing" confusion the archive
 * itself caused. */
.history-filter {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 0;
}
/* The note precedes the control and reads as its caption. `order` rather than
   markup order: the label must stay immediately before its input in source
   order for the `for=`/screen-reader association. */
.history-filter .date-note { order: -1; }
.history-filter label {
  font-size: var(--t-label); font-weight: 600;
  text-transform: uppercase; letter-spacing: .08em;
  color: var(--fg-3);
}
.history-filter input[type="date"] {
  padding: 6px 10px;
  border-radius: var(--radius-md);
  border: 1px solid var(--border-2);
  background: var(--surface-2);
  color: var(--fg-1);
  font-family: inherit; font-size: 13px;
  font-variant-numeric: tabular-nums;
  /* Without this the native picker indicator is a black glyph on the dark
     surface — the same class of bug as the black map pin. */
  color-scheme: dark;
}
[data-theme="light"] .history-filter input[type="date"] { color-scheme: light; }
.history-filter .date-note { font-size: var(--t-label); color: var(--fg-3); }
.history-filter .date-note b { color: var(--fg-2); font-weight: 600; }


/* ── Status chips (Emergency Victoria) ────────────────────────────────────
 * That page's chip carries a STATUS, and those labels are far longer than the
 * five CFA type names the column was sized for. Measured at 1600 against the
 * 102px column: Responding needed 116, Contained 107, Under Control 145 and
 * "Not Yet Under Control" 211 — four of the five overflowed, the worst by
 * 109px.
 *
 * Wider, and allowed to wrap. The width fits "Under Control" on one line,
 * which covers everything but the longest status; that one takes two lines
 * rather than forcing a column wide enough for a value that is rare. Scoped
 * to .status-chips so the CFA and FRV rows keep their tighter column.
 *
 * The compound is named past-section-first for the usual reason: the generic
 * width rule sits at equal specificity, so source order alone would decide. */
.main-col .past-section .status-chips .type-badge.inc-type {
  /* NOT uppercase, and this is the whole reason the chip fits.
     Measured at 1600: "UNDER CONTROL" is 103.7px and left 1.2px of clear
     space each side inside the 132px chip — the text visibly touched the
     edge, where the /district chips sit at 11.7-24.6px. Title case gives
     ~30px of that back. Every other lever was too weak to matter: 10px font
     bought 4.7px, .02em tracking 2.8px, and closing the gap to district-like
     air by width alone needed 154px, which is the oversized chip this
     started from.
     Uppercase is right for a one-word category (FIRE, ALARM). A status is a
     phrase, and it is title case on the feed and in the hero chip already. */
  text-transform: none;
  letter-spacing: .02em;
  column-gap: 6px;
  /* 138/12px, from the same sweep: worst case is "Under Control" at 15px of
     clear space per side, against 11.7px for the tightest /district chip. The
     font goes UP a step rather than down — the fit came from the case change,
     so there was room to buy back legibility for a wall display. */
  width: clamp(138px, 7.5vw, 184px);
  font-size: clamp(12px, .62vw, 15px);
  /* pre-line, not normal: the page supplies the break point for the one
     status that needs two lines (see STATUS_WRAP in emevic.html), and this is
     what honours it. Ordinary wrapping still applies to everything else. */
  white-space: pre-line;
  line-height: 1.15;
  text-align: center;
}
/* Sized against the two-line label rather than the type, so the glyph reads as
   the chip's mark instead of a speck beside a block of text. Uniform across
   all five statuses — a per-status icon size would make the column jitter. */
.main-col .past-section .status-chips .type-badge.inc-type svg {
  width: clamp(17px, .95vw, 22px);
  height: clamp(17px, .95vw, 22px);
}

/* ── Accessibility utilities ──────────────────────────────────────────────
 * Added 2026-08-14 after the design review measured 139 tab stops on
 * /district/8 with no way past the rail. District changes are FULL PAGE
 * LOADS here, not SPA transitions, so a keyboard user re-traversed all 20
 * district links plus their expanders on every single navigation. The skip
 * link is the whole fix for that.
 *
 * .sr-only is the standard clip-rect pattern rather than display:none or
 * visibility:hidden, both of which remove the element from the accessibility
 * tree — which would defeat the point of the headings that use it. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Off-screen until focused, then it lands over the header. It is the first
   focusable thing in the document, so it must sit above .header's stacking
   context or it renders behind it. */
.skip-link {
  position: absolute;
  top: 0; left: 0;
  z-index: 200;
  transform: translateY(-120%);
  padding: 10px 16px;
  background: var(--surface-3);
  color: var(--fg-1);
  border: 1px solid var(--border-2);
  border-radius: 0 0 var(--radius-md) 0;
  font-size: 13px; font-weight: 600;
  text-decoration: none;
}
.skip-link:focus {
  transform: translateY(0);
  color: var(--fg-1);
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}
/* The rail and the main column both take focus programmatically from the skip
   link, so they need tabindex="-1" in markup — and that makes them show a
   focus ring on click in some engines. Suppress it only for these two. */
#main-content:focus, #district-nav:focus { outline: none; }

/* The section labels became <h2> so the pages have a real document outline
   ("Current Incident", "Recent Incidents"). .section-label already sets the
   type and .section-label-row zeroes the bottom margin; only the UA top
   margin needed clearing. */
h2.section-label, h2.section-title { margin-top: 0; }

/* Touch targets, measured against WCAG 2.2 AA "Target Size (Minimum)", which
   is 24x24 CSS px — NOT the 44x44 of AAA / Apple HIG. At the 24px bar exactly
   two targets on the site failed:
     .district-expand-btn   13x26  (too narrow)
     .strip-brig            76x21  (too short, mobile)
   Both are widened WITHOUT changing the row height. The rail's vertical
   rhythm and the stat strip's single-line height were both measured against
   real content, so growing them to 44px would push districts below the fold
   and re-wrap the strip — a worse outcome than the thing being fixed. The
   negative margins keep the visible glyph exactly where it was. */
.strip-brig {
  padding-top: 4px;
  padding-bottom: 4px;
  margin-top: -4px;
  margin-bottom: -4px;
}

/* ── Liveness readout ─────────────────────────────────────────────────────
 * Sits at the right end of .section-label-row (which is already flex), so the
 * heading stays left and the freshness reads as page chrome rather than as
 * part of the incident. See vfLive in tokens.js. */
.live-status {
  display: inline-flex; align-items: center; gap: 10px;
  margin-left: auto;
}
.live-age {
  font-size: var(--t-label);
  color: var(--fg-3);
  font-variant-numeric: tabular-nums;
  letter-spacing: .01em;
}
/* Amber, not red. A late refresh is a caution, not an incident — red here
   would collide with the type colour two elements away. */
.live-age.is-stale { color: var(--warn-ink); font-weight: 600; }

/* Deliberately NOT a filled accent pill. On this row it would sit inches from
   the FIRE type badge, which is a red fill with white ink, and read as an
   incident type. Outlined chip, accent ink: unmistakably a notification. */
.live-new {
  display: inline-flex; align-items: center;
  font-family: inherit;
  font-size: var(--t-label); font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase;
  padding: 4px 10px;
  border: 1px solid var(--accent);
  border-radius: var(--radius-pill);
  background: var(--surface-3);
  color: var(--accent-ink);
  cursor: pointer;
  animation: vf-live-in .32s cubic-bezier(.16, 1, .3, 1);
}
.live-new:hover { background: var(--accent); color: #fff; }
.live-new[hidden] { display: none; }

@keyframes vf-live-in {
  from { opacity: 0; transform: translateY(-4px) scale(.96); }
  to   { opacity: 1; transform: none; }
}

/* ── Reduced motion ───────────────────────────────────────────────────────
 * Not a blanket 0.01ms kill: the live dot's blink is the page's only
 * always-on "this is live" signal, so it is stopped at FULL opacity rather
 * than removed. The state it communicates survives; only the movement goes.
 * The arrival pill still appears, it just does not slide. */
@media (prefers-reduced-motion: reduce) {
  .live-dot { animation: none; opacity: 1; }
  .live-new { animation: none; }
  * { scroll-behavior: auto !important; }
}
