  html, body { margin:0; padding:0; height:100%; font-family: -apple-system, system-ui, "Segoe UI", Helvetica, Arial, sans-serif; background:#111; color:#eee; }
  body { display:flex; flex-direction:column; }
  :root {
    --view-chrome-height: 32px;
    /* Base height of a partner logo. Each logo multiplies this by its own
       optical factor (PARTNER_SCALE in common/data_sources.py, applied inline
       by buildPartners()), so the strip is resized by moving this one number
       per width band -- never by touching per-logo heights. */
    --partner-h: 22px;
    /* Separation between logos, uniform across the strip. Steps down with
       --partner-h per width band: at the bottom of the ≤799.98px band the
       whole strip has to fit in 342px (see that block). */
    --partner-gap: 20px;
    --epi-panel-width: 50%;
    --trends-panel-width: 40%;
    /* Band-A (wide-branch) panel widths #zone-search's offset is derived from.
       Each is read by the panel's own width rule (unconditional, all bands)
       and by the band-A search offset (default block only), so the 10px gap
       between panel and search cannot drift. --context-panel-width also sizes
       #context, the right-hand mirror of #context-national: band B's 834px
       boundary is the search's right edge against #context's LEFT edge, so
       the two panels staying equal is load-bearing, not cosmetic. The narrow
       branches use different numbers on purpose -- see the band B/C blocks. */
    --layer-panel-width: min(340px, calc(100vw - 24px));
    --context-panel-width: min(280px, calc(50vw - 24px));
    /* Sets #zone-search-input's min-height AND every band B/C displacement
       offset, so the two can never disagree about how tall the search is. */
    --zone-search-height: 32px;
    /* The clearance a displaced panel must leave below the search: its height
       plus an 8px gap. Split out from --zone-search-height so displaced tops
       (12px + drop) and their max-height give-backs (Xvh - drop) can't drift
       apart arithmetically -- they are the same quantity with opposite signs. */
    --zone-search-drop: calc(var(--zone-search-height) + 8px);
  }
  /* Common full-width header (title + build info on the left, outbreak size
     summary on the right), identical on every page -- see common/chrome.py.
     It's a normal-flow flex item (not position:fixed/absolute), so it takes
     whatever height its content needs and #viewport-area below it simply
     fills the rest of the page. That means none of the positioning below
     (#map, #controls, etc, all originally coded relative to the full
     viewport) needed to change: they're now relative to #viewport-area
     instead, which starts in the same place the viewport used to. */
  #site-header {
    flex:0 0 auto;
    display:flex; align-items:flex-start; justify-content:space-between;
    flex-wrap:wrap; gap:16px;
    padding:10px 16px;
    background:rgba(20,20,20,0.94); color:#f4f4f4;
    box-sizing:border-box;
    font-size:13px; line-height:1.4;
    position:relative; z-index:1400;
  }
  #site-header-left { min-width:0; text-align:left; }
  #site-header-right { flex:0 0 auto; display:flex; flex-direction:column; align-items:flex-end; text-align:right; gap:2px; }
  #site-header h1#page-heading {
    margin:0; font-size:clamp(20px, 3.2vw, 30px); font-weight:700; letter-spacing:0.3px;
    line-height:1.15;
  }
  /* Narrow-screen replacement for #title-sub (see @media max-width:700px
     below for the switch-over) -- just the "dashboard updated" line plus an
     info icon whose popup holds what #title-sub shows inline on wide
     screens (latest SitRep link, build tag). Hidden outright on wide
     screens; display:flex only applies once the media query below sets it. */
  #header-narrow-row {
    display:none;
    align-items:center; justify-content:center; gap:8px;
    margin-top:4px; position:relative;
  }
  #header-narrow-row .sub { margin-top:0; }
  #header-info-btn {
    width:16px; height:16px; border-radius:50%; flex:0 0 auto;
    border:1px solid #666; background:#222; color:#ffd28a;
    font-size:11px; font-weight:700; line-height:1;
    display:inline-flex; align-items:center; justify-content:center;
    padding:0; cursor:pointer;
  }
  #header-info-btn:hover,
  #header-info-btn:focus-visible { border-color:#ffae42; color:#ffae42; outline:none; }
  #header-info-popup {
    display:none;
    position:absolute; left:50%; top:calc(100% + 6px);
    transform:translateX(-50%);
    width:max-content; max-width:min(280px, 80vw);
    background:#1f1f1f; border:1px solid #444; border-radius:6px;
    padding:8px 10px; box-shadow:0 4px 16px rgba(0,0,0,0.45);
    color:#ddd; font-size:10px; line-height:1.4;
    z-index:1500; text-align:left;
  }
  #header-info-popup a { color:#9fcdfb; text-decoration:underline; }
  #header-info-popup a:hover { color:#ffae42; }
  #header-narrow-row:hover #header-info-popup,
  #header-narrow-row:focus-within #header-info-popup,
  #header-narrow-row.open #header-info-popup { display:block; }
  /* Static part of the popup (unlike #header-info-popup-body, which JS
     overwrites) -- stands in for the footer's Contributors/Methods and
     Terms buttons, which are hidden on narrow screens in favor of these. */
  #header-info-popup-links {
    display:flex; flex-direction:column; gap:6px;
    margin-top:8px; padding-top:8px; border-top:1px solid #333;
  }
  #header-info-popup-links .link-btn {
    display:block; width:100%; box-sizing:border-box;
    padding:6px 10px; font-size:11px; text-align:center;
  }
  /* Page-switching tabs, directly under the header (used to live at the
     bottom of the page; the .view-tabs/.view-tab styling below is shared
     with nothing else, so it moved here unchanged). */
  #page-tabs {
    flex:0 0 auto;
    display:flex; justify-content:center; align-items:center;
    padding:6px 10px;
    background:rgba(20,20,20,0.96); color:#f4f4f4;
    border-bottom:1px solid #333;
    box-shadow:0 2px 8px rgba(0,0,0,0.3);
    box-sizing:border-box;
    position:relative; z-index:1300;
  }
  #viewport-area { position:relative; flex:1 1 auto; min-height:0; overflow:hidden; }
  #map { position:absolute; top:0; right:0; bottom:var(--view-chrome-height); left:0; }
  .panel {
    position:absolute; z-index:1000;
    background:rgba(20,20,20,0.92); color:#f4f4f4;
    padding:12px 14px; border-radius:8px;
    box-shadow:0 2px 10px rgba(0,0,0,0.4);
    font-size:13px; line-height:1.4;
    box-sizing:border-box;
  }
  #controls     {
    top:12px; left:12px;
    width:var(--layer-panel-width);
    max-width:var(--layer-panel-width);
  }
  #controls .panel-body { min-width:0; }
  #controls select {
    display:block;
    width:100%;
    max-width:100%;
    box-sizing:border-box;
  }
  /* ── Standalone health-zone search ──────────────────────────────────────
     One node (see common/chrome.py), sibling of #map, serving every view.
     Placement per body.view-* lives further down, in the three band blocks.

     No L.DomEvent.disableClickPropagation/disableScrollPropagation is used or
     needed: #zone-search is a SIBLING of #map, not a descendant of the Leaflet
     container, so its events never reach Leaflet's container-bound handlers in
     the first place. Do not "fix" this by moving the node into the map pane.

     z-index 1200 ties with #trends-split-handle / #epi-split-handle, which
     come later in BODY_TEMPLATE and therefore win the tie. That is fine, and
     it is load-bearing on the rail clamp below: the handles sit at the rail's
     inner edge while the search sits at the map's left corner. The clamp's
     -24px leaves 12px to the panel's nominal edge, but each handle is centred
     ON that edge by margin-right:-5px, so it overhangs 5px onto the map and
     the real clearance is 7px. Widen the search past that, or drop the clamp,
     and the handle starts painting over the search. */
  #zone-search {
    position:absolute; z-index:1200;
    width:min(240px, calc(100vw - 24px));
  }
  #zone-search-input {
    display:block; width:100%; box-sizing:border-box;
    min-height:var(--zone-search-height);
    background:#ffffff; color:#2a2a27;
    border:1px solid #e7e3db; border-radius:4px;
    padding:6px 8px; font-size:12px;
    /* Floats over a basemap rather than sitting in a light rail, so the input
       carries the same lift the dropdown does. */
    box-shadow:0 2px 8px rgba(42,42,39,0.18);
  }
  #zone-search-input:focus {
    outline:none; border-color:#9b7d4e; box-shadow:0 0 0 1px rgba(155,125,78,0.35);
  }
  /* Results and the no-matches panel share one position: exactly one of them
     is ever visible, and the empty panel takes the results panel's place
     rather than stacking under it. */
  #zone-search-results,
  #zone-search-empty {
    position:absolute; left:0; right:0; top:100%;
    margin-top:6px;
    background:#ffffff; border:1px solid #e7e3db; border-radius:4px;
    box-shadow:0 4px 14px rgba(0,0,0,0.14);
    z-index:1100;
  }
  #zone-search-results {
    /* The 30vh arm matters on narrow Trends/Spatial Risk, where the map is
       height:40vh -- an unclamped five-row list opens over the whole map. */
    max-height:min(calc(5 * 32px), 30vh);
    overflow-y:auto;
  }
  #zone-search-results[hidden],
  #zone-search-empty[hidden] { display:none !important; }
  #zone-search-empty {
    padding:8px; font-size:12px; color:#9c968b; font-style:italic;
  }
  /* No :hover rule on purpose -- the controller moves .active on pointermove
     so keyboard and mouse can never highlight two different rows at once. */
  .zone-search-option {
    display:block; width:100%; text-align:left;
    background:#ffffff; color:#2a2a27;
    border:none; border-bottom:1px solid #e7e3db; border-radius:0;
    padding:7px 8px; font-size:12px; line-height:1.35;
    min-height:32px; box-sizing:border-box; cursor:pointer;
  }
  .zone-search-option:last-child { border-bottom:none; }
  .zone-search-option.active { background:#f3f1ec; color:#9b7d4e; }
  /* Announcement-only region (#zone-search-live). */
  .visually-hidden {
    position:absolute; width:1px; height:1px;
    margin:-1px; padding:0; border:0;
    clip-path:inset(50%); overflow:hidden; white-space:nowrap;
  }
  /* ── #zone-search placement, band A (the unqualified default, ≳1000px) ──
     Bands B and C are carved out below with max-width queries only, so no
     viewport width can fall between two rules the way a
     max-width:999px / min-width:1000px pair would at 999.5px.

     On map and context the search sits beside the panel already in the
     corner; the other three tabs have an empty corner. The three rail views
     additionally clamp their width to the visible map -- see the rail clamp
     note on each rule. */
  body.view-map #zone-search {
    top:12px; left:calc(12px + var(--layer-panel-width) + 10px);
  }
  body.view-context #zone-search {
    top:12px; left:calc(12px + var(--context-panel-width) + 10px);
  }
  body.view-trends #zone-search {
    top:12px; left:12px;
    /* The rail can be dragged to 72% (TRENDS_SPLIT_MAX), leaving a map
       narrower than the search; --trends-panel-width is a percentage and
       #zone-search is positioned in #viewport-area, so this tracks a live
       drag for free. */
    width:min(240px, calc(100% - var(--trends-panel-width) - 24px));
  }
  body.view-epi-trends #zone-search {
    top:12px; left:12px;
    width:min(240px, calc(100% - var(--epi-panel-width) - 24px));
  }
  body.view-genomic-epidemiology #zone-search {
    top:12px; left:12px;
    /* #genomic-panel OVERLAYS a full-width #map (no body.view-… #map rule),
       and its width is an inline px style, so genomic.js publishes
       --genomic-panel-width for this clamp. The 70vw fallback matches the
       CSS default before genomic.js runs. */
    width:min(240px, calc(100% - var(--genomic-panel-width, 70vw) - 24px));
  }
  body.stub-view #zone-search { display:none !important; }
  /* Genomic only: revealed by genomicMapHooks.onZoneClick() once the tree
     coordinator has registered -- see that hook in engine.js for why. */
  body.view-genomic-epidemiology #zone-search { display:none; }
  body.view-genomic-epidemiology #zone-search.zone-search-ready { display:block; }
  /* ── Band B — 701px … 999.98px ─────────────────────────────────────────
     The beside-the-panel layout of band A collides with the opposite corner
     here: on map the search would span 362–602px while #info (right:12px,
     max-width:340px) starts at 100vw − 352, meeting at ~954px; on context the
     figures are 302–542px against #context at 100vw − 292, meeting at ~834px.
     #info is content-sized under a bare max-width, so the space actually left
     is not derivable in CSS; context could be clamped (it has a definite
     width) but follows map so the two tabs do not diverge.

     So: corner on every tab, and the two views with something already at
     12px/12px displace it downward. #info needs no displacement -- at ≥701px
     it starts at ≥349px, clear of the search's 252px right edge. */
  @media (max-width: 999.98px) {
    body.view-map #zone-search,
    body.view-context #zone-search { top:12px; left:12px; }
    body.view-map #controls { top:calc(12px + var(--zone-search-drop)); }
    body.view-context #context-national {
      top:calc(12px + var(--zone-search-drop));
      /* Displacement must not push the panel's bottom edge toward the footer:
         give back exactly what the top took. */
      max-height:calc(80vh - var(--zone-search-drop));
    }
  }
  #legend       { bottom:calc(var(--view-chrome-height) + 12px); left:12px; width:max-content; max-width:min(360px, calc(100vw - 24px)); }
  /* Size the legend to its widest line so the (sometimes long) layer title
     stays on one line instead of wrapping. */
  #legend-title { white-space:nowrap; }
  #info         { top:12px; right:12px; max-width:340px; max-height:80vh; overflow-y:auto; }
  /* Trends tab right-hand rail (map left, plots column right) -- same
     mechanics as #epi-trends-panel/#epi-split-handle below, with its own
     width variable so the two tabs remember independent split positions. */
  #trends-panel {
    display:none;
    position:absolute; z-index:1000;
    top:0; right:0; bottom:var(--view-chrome-height);
    width:var(--trends-panel-width);
    background:#f6f5f2; color:#2a2a27;
    border-left:1px solid #e7e3db;
    box-shadow:-2px 0 12px rgba(42,42,39,0.06);
    box-sizing:border-box;
    padding:12px 14px 16px;
    overflow:auto;
    font-size:13px; line-height:1.4;
  }
  body.view-trends #trends-panel { display:flex; flex-direction:column; gap:10px; }
  body.view-trends #map { right: var(--trends-panel-width); }
  #trends-split-handle {
    display:none;
    position:absolute;
    top:0; right:var(--trends-panel-width); bottom:var(--view-chrome-height);
    width:10px; margin-right:-5px; z-index:1200;
    cursor:col-resize; touch-action:none; background:transparent;
  }
  #trends-split-handle::before {
    content:""; position:absolute; top:0; bottom:0; left:50%; width:2px;
    transform:translateX(-50%); background:#d8d3c9;
    transition:background .12s ease, width .12s ease;
  }
  #trends-split-handle:hover::before,
  body.trends-splitting #trends-split-handle::before { width:3px; background:#9b7d4e; }
  body.view-trends #trends-split-handle { display:block; }
  body.trends-splitting { cursor:col-resize; user-select:none; }
  body.trends-splitting #map,
  body.trends-splitting #trends-panel { pointer-events:none; }
  /* #trends-controls (scope/search) and #trends (first plot card) are now
     normal-flow children stacked inside #trends-panel instead of separately
     floating/absolutely-positioned boxes. */
  #trends-controls {
    position:static; flex:0 0 auto;
    width:auto; max-width:none; max-height:none; overflow:visible;
  }
  #trends-plots-column {
    flex:1 1 auto; min-height:0;
    display:flex; flex-direction:column; gap:10px;
  }
  /* Shared by every card in #trends-plots-column (#trends is the first one
     today; future plot cards should reuse this class so they stack the same
     way without needing their own id-based layout rules). */
  .trends-plot-card {
    position:static; flex:0 0 auto;
    width:auto; max-width:none; max-height:none; overflow:visible;
    display:block;
  }
  .trends-labs-body {
    display:flex; flex-direction:column; gap:14px;
  }
  .trends-lab-subplot-title {
    margin:0 0 6px;
    font-size:13px; font-weight:600; color:#2a2a27;
  }
  body.view-trends #controls,
  body.view-trends #legend,
  body.view-trends #info { display:none !important; }
  body.view-epi-trends #controls,
  body.view-epi-trends #legend,
  body.view-epi-trends #info,
  body.view-epi-trends #trends-panel,
  body.view-epi-trends #trends-split-handle,
  body.view-epi-trends #trends-legend,
  body.view-epi-trends #context,
  body.view-epi-trends #context-national { display:none !important; }
  body.view-epi-trends #map {
    right: var(--epi-panel-width);
  }
  #epi-trends-panel {
    display:none;
    position:absolute; z-index:1000;
    top:0; right:0; bottom:var(--view-chrome-height);
    width:var(--epi-panel-width);
    background:#ffffff; color:#2a2a27;
    border-left:1px solid #e7e3db;
    box-shadow:-2px 0 12px rgba(42,42,39,0.06);
    box-sizing:border-box;
    padding:12px 14px 16px;
    overflow:auto;
    font-size:13px; line-height:1.4;
  }
  #epi-split-handle {
    display:none;
    position:absolute;
    top:0;
    right:var(--epi-panel-width);
    bottom:var(--view-chrome-height);
    width:10px;
    margin-right:-5px;
    z-index:1200;
    cursor:col-resize;
    touch-action:none;
    background:transparent;
  }
  #epi-split-handle::before {
    content:"";
    position:absolute;
    top:0; bottom:0;
    left:50%;
    width:2px;
    transform:translateX(-50%);
    background:#d8d3c9;
    transition:background .12s ease, width .12s ease;
  }
  #epi-split-handle:hover::before,
  body.epi-splitting #epi-split-handle::before {
    width:3px;
    background:#9b7d4e;
  }
  body.view-epi-trends #epi-split-handle { display:block; }
  body.epi-splitting { cursor:col-resize; user-select:none; }
  body.epi-splitting #map,
  body.epi-splitting #epi-trends-panel { pointer-events:none; }
  body.view-epi-trends #epi-trends-panel { display:flex; flex-direction:column; gap:8px; }
  body.view-trends #epi-trends-panel,
  body.view-trends #epi-split-handle,
  body.view-trends #epi-trends-legend,
  body.view-trends #epi-float { display:none !important; }
  /* #trends-controls: just the scope buttons now (location search moved to
     the standalone #zone-search component over the map). Kept as a flex row
     so a second control can be added back beside the scope buttons without
     re-deriving the layout; no justify-content, since one child needs none. */
  #trends-controls {
    display:flex; align-items:center;
    gap:10px; flex-wrap:wrap;
  }
  /* Scope is a row of buttons (National / Provincial / Health Zone) rather
     than a dropdown -- segmented-control look, active one highlighted. */
  .trends-scope-row {
    display:flex; gap:6px; flex-wrap:wrap; flex:0 1 auto;
  }
  .trends-scope-btn {
    flex:0 0 auto;
    background:#ffffff; color:#2a2a27; border:1px solid #e7e3db; border-radius:4px;
    padding:6px 10px; font-size:12px; font-weight:600; cursor:pointer;
    box-sizing:border-box; text-align:center;
  }
  .trends-scope-btn:hover { background:#f3f1ec; }
  .trends-scope-btn.active {
    background:rgba(155,125,78,0.16); color:#7c4a12; border-color:#9b7d4e;
  }
  #epi-trends-panel h2 {
    margin:0; font-size:14px; color:#2a2a27; font-weight:700;
  }
  #epi-trends-table-wrap {
    flex:1 1 auto; overflow:auto; border:1px solid #e7e3db; border-radius:6px;
    min-height:180px; background:#ffffff;
  }
  #epi-trends-table {
    width:100%; border-collapse:collapse; font-size:12px; color:#2a2a27;
  }
  #epi-trends-table th, #epi-trends-table td {
    padding:6px 8px; border-bottom:1px solid #e7e3db; text-align:left;
    white-space:nowrap;
  }
  #epi-trends-table th {
    position:sticky; top:0; background:#faf9f7; color:#9c968b; font-weight:600;
    z-index:1;
  }
  #epi-trends-table tr { cursor:pointer; }
  #epi-trends-table tr:hover td { background:#f3f1ec; }
  #epi-trends-table tr.selected td {
    background:rgba(155,125,78,0.12); color:#7c1d1d;
  }
  #epi-trends-table .num { font-variant-numeric:tabular-nums; text-align:center; }
  /* Sortable column headers (replace the old "Rank by relative risk /
     Rank by vulnerability-based priority" buttons) -- click or Enter/Space
     to sort by that column, click again to reverse. .sort-arrow is filled
     in by updateEpiSortIndicators() for whichever column is currently
     active; blank on every other header. */
  #epi-trends-table th.sortable {
    cursor:pointer; user-select:none; white-space:nowrap;
  }
  #epi-trends-table th.sortable:hover,
  #epi-trends-table th.sortable:focus-visible {
    background:#f0ede6; color:#7c4a12;
  }
  #epi-trends-table th.sortable:focus-visible { outline:2px solid #9b7d4e; outline-offset:-2px; }
  #epi-trends-table th.sort-active { color:#7c4a12; }
  #epi-trends-table th .sort-arrow {
    display:inline-block; width:0.9em; margin-left:3px; font-size:9px; color:#9b7d4e;
  }
  /* Shown instead of ranked rows when Provincial scope is active but no
     province has been picked yet via the search box above -- reuses the
     same empty-state text styling as the Trends tab's placeholder copy. */
  #epi-trends-tbody .epi-empty-row td {
    cursor:default; white-space:normal; text-align:center; padding:16px 8px;
  }
  #epi-trends-method {
    font-size:11px; color:#9c968b; margin-top:4px; line-height:1.35;
  }
  #epi-trends-method a { color:#7c4a12; text-decoration:underline; }
  #epi-trends-method a:hover { color:#7c1d1d; }
  #epi-trends-subtitle {
    margin:2px 0 0 0; font-size:12px; color:#9c968b; line-height:1.35;
  }
  #epi-trends-downloads {
    display:flex; flex-wrap:wrap; gap:8px; margin-top:8px;
  }
  #epi-trends-downloads button {
    background:#ffffff; color:#2a2a27; border:1px solid #e7e3db; border-radius:4px;
    padding:6px 10px; font-size:12px; cursor:pointer; font-weight:600; color:#9b7d4e;
  }
  #epi-trends-downloads button:hover {
    background:rgba(155,125,78,0.08); color:#7c1d1d; border-color:#9b7d4e;
  }
  body.epi-map-exporting #zone-search,
  body.epi-map-exporting #epi-trends-legend,
  body.epi-map-exporting #epi-float,
  body.epi-map-exporting #epi-split-handle,
  body.epi-map-exporting #epi-trends-panel,
  body.epi-map-exporting #view-switcher,
  body.epi-map-exporting #site-header,
  body.epi-map-exporting #page-tabs,
  body.epi-map-exporting .leaflet-control-container {
    display:none !important;
  }
  /* Full-bleed map framed on DRC for JPG export. */
  body.epi-map-exporting #map {
    right:0 !important;
    bottom:0 !important;
  }
  #epi-trends-legend {
    display:none;
    position:absolute; z-index:1000;
    /* Sits over the map portion only (map is narrowed by --epi-panel-width
       on this tab), bottom-right of that visible area rather than
       bottom-right of the whole viewport -- otherwise it'd land underneath
       the ranked-zones table panel. Mirrors #trends-legend's positioning. */
    bottom:calc(var(--view-chrome-height) + 12px);
    right:calc(var(--epi-panel-width) + 12px); max-width:min(340px, calc(100vw - 36px));
    background:#ffffff; color:#2a2a27;
    padding:12px 14px; border-radius:8px;
    border:1px solid #e7e3db;
    box-shadow:0 2px 10px rgba(42,42,39,0.08);
    font-size:13px; line-height:1.4;
  }
  body.view-epi-trends #epi-trends-legend { display:block; }
  #epi-trends-legend .legend-row { margin-top:6px; color:#9c968b; }
  #epi-trends-legend .checkbox-row label { color:#2a2a27 !important; }
  #epi-float {
    display:none;
    position:absolute; z-index:1100;
    min-width:200px; max-width:280px;
    background:#ffffff; color:#2a2a27;
    border:1px solid #e7e3db; border-radius:8px;
    padding:10px 12px; box-shadow:0 4px 16px rgba(42,42,39,0.12);
    /* Text matches the active-case marker tooltip (caseMarkerTooltip +
       .case-tt-row): same 12px/1.5, same one-colour label-and-value pair, same
       gapless block under the title. The box itself is deliberately NOT the
       same -- that one is a stock Leaflet tooltip. */
    font-size:12px; line-height:1.5; pointer-events:none;
  }
  body.view-epi-trends #epi-float.visible { display:block; }
  #epi-float strong { color:#2a2a27; }
  #epi-float table { width:100%; border-collapse:collapse; }
  /* Rows carry no padding of their own; line-height does the spacing, as in
     the marker tooltip's flex rows. Labels stay the same colour as values --
     no muted first column here. */
  #epi-float td { padding:0; color:#2a2a27; }
  #epi-float td:last-child { text-align:right; font-variant-numeric:tabular-nums; }
  #trends-legend {
    position:absolute; z-index:1000;
    /* Sits over the map portion only (map is narrowed by --trends-panel-width
       on this tab), bottom-right of that visible area rather than bottom-left
       of the whole viewport -- otherwise it'd land underneath the plots panel. */
    bottom:calc(var(--view-chrome-height) + 12px);
    right:calc(var(--trends-panel-width) + 12px); max-width:300px;
    background:rgba(20,20,20,0.92); color:#f4f4f4;
    padding:12px 14px; border-radius:8px;
    box-shadow:0 2px 10px rgba(0,0,0,0.4);
    font-size:13px; line-height:1.4;
    display:none;
  }
  body.view-trends #trends-legend { display:block; }
  #trends-date-slider {
    flex:1 1 auto; width:auto; min-width:0;
    accent-color:#5b86b3;
    -webkit-appearance:none;
    appearance:none;
    height:6px;
    background:#444;
    border-radius:3px;
    outline:none;
  }
  #trends-date-slider::-webkit-slider-runnable-track {
    height:6px;
    background:#444;
    border-radius:3px;
  }
  #trends-date-slider::-webkit-slider-thumb {
    -webkit-appearance:none;
    appearance:none;
    width:16px;
    height:16px;
    margin-top:-5px;
    border-radius:50%;
    background:#5b86b3;
    border:2px solid #fff;
    box-shadow:0 0 0 1px #5b86b3;
    cursor:pointer;
  }
  #trends-date-slider::-moz-range-track {
    height:6px;
    background:#444;
    border-radius:3px;
  }
  #trends-date-slider::-moz-range-thumb {
    width:16px;
    height:16px;
    border-radius:50%;
    background:#5b86b3;
    border:2px solid #fff;
    box-shadow:0 0 0 1px #5b86b3;
    cursor:pointer;
  }
  #trends-date-label {
    display:block; margin-top:8px; font-size:12px; color:#bbb;
  }
  /* While the date slider is animating (Play) or being dragged, cross-fade
     each health zone's fill colour instead of snapping straight to the new
     value -- scoped to trends-slider-busy so scrubbing/playback feel smooth
     without adding a transition to every other colour change on the map
     (scope switches, search selection, hover highlight, etc). */
  body.trends-slider-busy .leaflet-interactive {
    transition: fill 0.14s linear, fill-opacity 0.14s linear;
  }
  /* Play button + slider share a row now, button to the left. */
  #trends-play-row {
    display:flex; gap:8px; align-items:center; margin-top:8px;
  }
  #trends-play-btn {
    background:#5b86b3; color:#fff; border:1px solid #6f98c0; border-radius:4px;
    width:26px; height:26px; padding:0; font-size:12px; line-height:1;
    cursor:pointer; font-weight:600; flex:0 0 auto;
    display:flex; align-items:center; justify-content:center;
  }
  #trends-play-btn:hover { background:#4d769f; }
  #trends-play-btn.playing { background:#9b7d4e; border-color:#9b7d4e; }
  #trends-legend-desc {
    font-size:10px; color:#888; line-height:1.35; margin:4px 0 8px 0;
  }
  /* Full-width bottom chrome: Contributors/Methods/Terms links on the left
     (across from the partner logos on the right -- tabs used to live here,
     see #page-tabs near the top of the file for where they moved to). */
  #view-switcher {
    position:absolute; left:0; right:0; bottom:0;
    height:var(--view-chrome-height);
    z-index:1100;
    display:grid;
    grid-template-columns:minmax(0, 1fr) auto;
    align-items:center;
    gap:10px;
    box-sizing:border-box;
    padding:4px 10px 4px 12px;
    background:rgba(20,20,20,0.96); color:#f4f4f4;
    border-radius:0;
    border-top:1px solid #333;
    box-shadow:0 -2px 10px rgba(0,0,0,0.35);
  }
  #view-switcher #footer-links {
    min-width:0;
    display:flex;
    justify-content:flex-start;
    align-items:center;
    flex-wrap:wrap;
    gap:14px;
  }
  /* Quiet text links, not buttons: these are two once-a-visit reference links
     and used to render as filled .link-btns competing with the page tabs.
     Scoped to #footer-links so the #header-info-popup-links copies -- a
     stacked menu, where button chrome is right -- are untouched. */
  #footer-links .link-btn {
    padding:0; background:none; border:none; border-radius:0;
    font-size:12px; font-weight:400; color:#ffd28a;
    text-decoration:underline; text-underline-offset:2px;
  }
  #footer-links .link-btn:hover { background:none; color:#ffae42; }
  .view-tabs {
    display:flex; justify-content:center; align-items:center;
    flex-wrap:wrap; gap:6px;
  }
  .view-tab {
    background:#222; color:#ccc; border:1px solid #555; border-radius:4px;
    padding:4px 14px; font-size:12px; cursor:pointer; line-height:1.3;
    white-space:nowrap;
    /* .view-tab is now a real <a href> (cross-page nav) rather than a
       <button>; reset link defaults so it still looks like a tab/button. */
    display:inline-block; text-decoration:none;
  }
  .view-tab:hover { background:#333; color:#eee; }
  .view-tab.active { color:#ffd28a; border-color:#ffae42; background:#2a2418; }
  /* The strip is allowed to give ground (flex-shrink:1, min-width:0). It used
     to be flex-shrink:0, which made #footer-links the only column that could
     yield -- and when it wrapped, its second row fell out of the bar, since
     the bar's height is a variable, not auto. */
  #view-switcher #partners {
    position:static;
    z-index:auto;
    justify-self:end;
    flex-shrink:1;
    min-width:0;
    max-width:none;
    margin:0;
    padding:2px 4px;
    /* White plate for the un-themed fallback only: this bar is
       rgba(20,20,20,0.96) without dashboard-theme.css, where color-on-white
       artwork needs a ground (northeastern.png is transparent-ground RGBA and
       would disappear outright). The theme layer removes both. */
    background:#ffffff;
    border-radius:4px;
    box-shadow:none;
    display:flex; flex-wrap:nowrap; align-items:center;
    justify-content:center;
    /* One separation for the whole strip: --partner-gap drives both the space
       between affiliation groups and the space within one, so every logo sits
       the same distance from its neighbours. buildPartners() still wraps each
       affiliation in .partner-group (see PARTNER_GROUPS in data_sources.py);
       giving #partners a larger gap than .partner-group is all it takes to
       bring grouped spacing back. */
    gap:var(--partner-gap);
  }
  #view-switcher #partners .partner-group {
    display:flex; flex-wrap:nowrap; align-items:center;
    gap:var(--partner-gap); min-width:0;
  }
  #view-switcher #partners a { display:inline-flex; align-items:center; transition:opacity .15s ease; }
  #view-switcher #partners a:hover { opacity:0.78; }
  /* height is set inline per logo as calc(var(--partner-h) * <scale>). */
  #view-switcher #partners img {
    width:auto; max-width:min(14vmin, 110px); display:block; object-fit:contain;
  }
  @media (max-width: 999.98px) {
    :root { --partner-h: 18px; --partner-gap: 18px; }
    #view-switcher #partners img { max-width:min(10vmin, 68px); }
  }
  /* Last step before the ≤700px rules move the links into the ⓘ popup
     entirely. The binding constraint is French: "Contributeurs, données et
     méthodes" + "Conditions d'utilisation" need 327px on one line, and the bar
     spends 32px on padding and the column gap, so the strip has to fit in
     W − 359 — 342px at the bottom of this band. */
  @media (max-width: 799.98px) {
    :root { --partner-h: 15px; --partner-gap: 10px; }
  }
  #context-hint,
  #travel-hint,
  #flow-hint,
  #flow-empty-hint {
    position:absolute; z-index:900;
    top:50%; left:50%; transform:translate(-50%, -50%);
    pointer-events:none; color:#aaa;
    font-size:clamp(14px, 2.5vw, 18px);
    text-align:center; padding:14px 22px;
    display:none; align-items:center; justify-content:center;
    background:rgba(20,20,20,0.82);
    border-radius:14px;
    box-shadow:0 2px 10px rgba(0,0,0,0.35);
    max-width:min(420px, calc(100vw - 48px));
    line-height:1.4;
  }
  /* These map hints sit lower on the map (not dead-centre, which read as an
     obtrusive modal) and use smaller, less prominent text than the shared
     hint base above. The travel-origin hint keeps its centred placement. */
  #context-hint,
  #flow-hint,
  #flow-empty-hint {
    top:auto; bottom:10%; transform:translateX(-50%);
    font-size:clamp(11px, 1.6vw, 13px);
    padding:9px 15px;
  }
  body.view-map.matrix-layer-active #travel-hint { display:flex; }
  body.view-map.flow-layer-active #flow-hint { display:flex; }
  body.view-map.flow-layer-active.flow-hub-selected #flow-hint { display:none; }
  body.view-map.flow-layer-active.flow-hub-no-data #flow-empty-hint { display:flex; }
  .flow-wing-icon {
    background: transparent !important;
    border: none !important;
  }
  .flow-wing-icon svg {
    display: block;
    overflow: visible;
  }
  /* Applies to every plot card's empty state (trends/deaths/positivity/labs),
     not just #trends-body -- previously scoped to that one id, so the other
     cards' placeholder copy rendered in default (dark, larger) text. */
  .trends-empty { color:#888; font-size:12px; }
  #context-national {
    top:12px; left:12px; bottom:auto; right:auto;
    width:var(--context-panel-width);
    max-width:280px;
    max-height:80vh;
    overflow:hidden; display:none;
    flex-direction:column;
    box-sizing:border-box;
  }
  #context {
    top:12px; right:12px; bottom:auto; left:auto;
    width:var(--context-panel-width);
    max-width:280px;
    max-height:80vh;
    overflow:hidden; display:none;
    flex-direction:column;
    box-sizing:border-box;
  }
  body.view-context #controls,
  body.view-context #legend,
  body.view-context #info { display:none !important; }
  body.view-context #context-national,
  body.view-context #context { display:flex; }
  #context-national .panel-header,
  #context .panel-header { flex:0 0 auto; }
  #context-national-body,
  #context-body {
    flex:1 1 auto;
    min-height:0;
    overflow-y:auto;
    -webkit-overflow-scrolling:touch;
  }
  body.view-context #context-hint { display:flex; }
  body.view-context.context-zone-hovered #context-hint { display:none; }
  /* ── "Coming soon" stub pages (Clinical Symptoms, Surveillance and
     Testing, Genomic Epidemiology) ──────────────────────────────────
     Every stub page carries the same body.stub-view class (see
     common/chrome.py), so the map/every normal panel is hidden with one
     rule instead of repeating it per view id; only the matching
     #stub-<id> panel for the active page is then shown. */
  body.stub-view #map,
  body.stub-view #context-hint,
  body.stub-view #travel-hint,
  body.stub-view #flow-hint,
  body.stub-view #flow-empty-hint,
  body.stub-view #controls,
  body.stub-view #legend,
  body.stub-view #info,
  body.stub-view #trends-panel,
  body.stub-view #trends-split-handle,
  body.stub-view #trends-legend,
  body.stub-view #epi-trends-panel,
  body.stub-view #epi-split-handle,
  body.stub-view #epi-trends-legend,
  body.stub-view #epi-float,
  body.stub-view #context,
  body.stub-view #context-national { display:none !important; }
  .stub-panel {
    display:none;
    position:absolute; z-index:1000;
    top:50%; left:50%; transform:translate(-50%, -50%);
    text-align:center;
    min-width:260px; max-width:min(480px, calc(100vw - 48px));
    padding:28px 32px;
  }
  .stub-panel h2 { margin:0 0 8px 0; font-size:20px; color:#ffd28a; }
  .stub-panel p { margin:0; color:#bbb; font-size:14px; }
  body.view-clinical-symptoms #stub-clinical-symptoms,
  body.view-surveillance-testing #stub-surveillance-testing,
  body.view-genomic-epidemiology #stub-genomic-epidemiology { display:block; }
  #context-body.context-empty,
  #context-national-body.context-empty { color:#888; font-size:12px; }
  .context-pillar { margin:0 0 12px 0; padding-left:10px; border-left:3px solid #555; }
  .context-pillar h4 { margin:0 0 4px 0; color:#c4c4c4; }
  .context-pillar .context-meta { font-size:10px; color:#6e6e6e; margin-bottom:4px; }
  .context-pillar .context-meta .scope-tag {
    display:inline-block; margin-right:6px; padding:1px 5px;
    border-radius:3px; background:#2a2a2a; color:#999; text-transform:uppercase;
    letter-spacing:0.4px; font-size:9px;
  }
  .context-pillar p { margin:0; color:#959595; line-height:1.45; font-size:12px; }
  .context-pillar.pillar-epidemiological-coordination { border-left-color:#9fcdfb; }
  .context-pillar.pillar-epidemiological-monitoring { border-left-color:#5dade2; }
  .context-pillar.pillar-epidemiological-management { border-left-color:#ffae42; }
  .context-pillar.pillar-epidemiological-laboratory { border-left-color:#bb8fce; }
  .context-pillar.pillar-epidemiological-infection-prevention-controle { border-left-color:#48c9b0; }
  .context-pillar.pillar-epidemiological-logistics { border-left-color:#e67e22; }
  .context-pillar.pillar-epidemiological-security { border-left-color:#ec7063; }
  .context-pillar.pillar-epidemiological-community-engagement { border-left-color:#58d68d; }
  .context-pillar.pillar-epidemiological-protection-sexual-exploitation-abuse { border-left-color:#c39bd3; }
  .onset-chart-wrap { width:100%; margin-top:4px; }
  .onset-chart-wrap svg { width:100%; max-width:100%; height:auto; display:block; }

  /* Cacher le contenu quand réduit */
  .panel.collapsed .panel-body {
    display: none;
  }

  #info-header,
  .panel-header { display:flex; align-items:center; justify-content:space-between; gap:8px; }
  #info-toggle,
  .panel-toggle { background:transparent; color:#ffd28a; border:1px solid #555; border-radius:4px;
                  width:22px; height:22px; padding:0; cursor:pointer; font-size:14px; line-height:1; }
  #info-toggle:hover,
  .panel-toggle:hover { background:#333; color:#ffae42; }
  #info.collapsed #info-body,
  .panel.collapsed .panel-body { display:none; }
  @media (max-width: 700px) {
    /* Only the logo strip is left in the bar at this width: Methods/Terms move
       into the ⓘ popup (see #footer-links .link-btn below) and the language
       switch into #header-narrow-row, so the bar needs no room for a text row. */
    :root { --view-chrome-height: 36px; }
    .panel          { font-size:12px; padding:6px 8px; }
    /* Narrow viewports: stack the right column (outbreak summary) below the
       left column (title/build info) instead of squeezing them side by side,
       and center everything rather than keeping the left/right alignment
       that only makes sense in the wide two-column layout. */
    #site-header    { flex-direction:column; align-items:center; padding:6px 8px; gap:8px; }
    #site-header-left { text-align:center; }
    #site-header-right { align-items:center; text-align:center; }
    /* #tracker's own centering overrides live in a second copy of this same
       media query further down the file, after #tracker's unconditional
       right-aligned rules -- putting them here (before those base rules)
       meant equal-specificity source order let the base right-aligned rules
       win even on narrow screens, so #site-header-right centered but its
       #tracker contents stayed right-aligned. See the block right after the
       "Tracker stack" rules below. */
    #view-switcher { padding:3px 8px; gap:6px; }
    .view-tab { padding:3px 9px; font-size:11px; }
    /* One scrollable row instead of wrapping onto multiple lines -- 7 tabs
       don't all fit on a phone width, so let the row scroll horizontally
       rather than growing #page-tabs taller. */
    .view-tabs {
      flex-wrap:nowrap; flex:1 1 auto; min-width:0;
      justify-content:flex-start;
      overflow-x:auto; -webkit-overflow-scrolling:touch;
      scrollbar-width:thin;
    }
    .view-tab { flex:0 0 auto; }
    /* Nothing shares the bar here, so the strip centres rather than sitting
       hard right across from an empty column. */
    #view-switcher #partners { justify-self:center; }
    #view-switcher #partners img {
      max-width:min(12vmin, 80px);
    }
    #site-header h1#page-heading { margin-bottom:2px; }
    #site-header .sub { font-size:10px; }
    /* Collapse to: page-heading / [updated line + info icon] / stats-block.
       #title-sub (latest SitRep line + build tag) is replaced by
       #header-narrow-row -- that content moves into the info icon's popup
       instead of just disappearing. */
    #title-sub { display:none; }
    #header-narrow-row { display:flex; }
    #info           { max-width:60vw; }
    /* Contributors/Methods and Terms move into the header info popup on
       narrow screens (see #header-info-popup-links above) instead of
       sitting in the footer -- there isn't room for both there and the
       language switcher/partner logos. */
    #methods-btn,
    #terms-btn { display:none; }
    /* Trends tab: map and plots column stack as a column instead of sitting
       side by side -- there isn't width for both. Map keeps the top ~40% of
       the viewport area, the plots panel takes the rest and scrolls
       independently; the drag-to-resize handle only makes sense for a
       left/right split, so it's hidden here rather than taught to resize
       vertically. */
    body.view-trends #map {
      right:0; bottom:auto; height:40vh;
    }
    body.view-trends #trends-panel {
      top:40vh; left:0; right:0; bottom:var(--view-chrome-height);
      width:auto;
      border-left:none; border-top:1px solid #e7e3db;
      box-shadow:0 -2px 12px rgba(42,42,39,0.06);
    }
    #trends-split-handle { display:none !important; }
    /* Trends tab: replace the full legend (title/description/colour bar/
       ticks/scale/date-slider) with just a small play-button panel,
       top-right -- not enough room for all of that, and autoplay already
       runs on entering the tab, so the button (same #trends-play-btn /
       playTrendsSliderAnimation() as before) just lets you replay/pause. */
    body.view-trends #trends-legend {
      /* Map is full-width in its own top section on narrow screens (see
         "map and plots column stack" rules below), so no panel-width
         offset is needed here the way there is on wider screens. */
      top:calc(12px + var(--zone-search-drop)); bottom:auto; right:12px;
      max-width:none; width:auto; padding:6px;
    }
    #trends-legend-title,
    #trends-legend-desc,
    #trends-legend-bar,
    #trends-legend-ticks,
    #trends-legend-scale,
    #trends-date-slider,
    #trends-date-label { display:none; }
    #trends-play-row { margin-top:0; }
    /* Spatial Risk tab: map and ranked-zones table stack as a column instead
       of sitting side by side, same treatment as the Trends map/plots split
       above. Map keeps the top ~40% of the viewport, the table panel takes
       the rest and scrolls independently; the drag-to-resize handle only
       makes sense for a left/right split, so it's hidden here too. */
    body.view-epi-trends #map {
      right:0; bottom:auto; height:40vh;
    }
    body.view-epi-trends #epi-trends-panel {
      top:40vh; left:0; right:0; bottom:var(--view-chrome-height);
      width:auto;
      border-left:none; border-top:1px solid #e7e3db;
      box-shadow:0 -2px 12px rgba(42,42,39,0.06);
    }
    #epi-split-handle { display:none !important; }
    /* The map-colouring legend collapses to just its title bar by default on
       narrow screens (see wirePanelToggles()'s auto-collapse-on-load check
       in engine.js, which applies to every .panel-toggle panel including
       this one) -- reposition it top-left over the now-shorter map instead
       of bottom-left, and drop it to a compact single-line width so the
       collapsed title bar doesn't span the whole map width. */
    body.view-epi-trends #epi-trends-legend {
      /* Cancel the wide-screen right:calc(...) offset -- the map is
         full-width in its own top section here (see the map/table stacking
         rules above), so this just needs to sit in its own top-left corner. */
      top:calc(12px + var(--zone-search-drop)); left:12px; right:auto; bottom:auto;
      max-width:min(240px, calc(100vw - 24px));
    }
    body.view-context #context-national {
      top:clamp(128px, 24vh, 200px);
      /* Cancels band B's give-back: that max-height compensates band B's 40px
         top displacement, but the top offset just above is NOT displaced --
         this panel already clears the search here. Declared explicitly because
         band B still matches at this width and the cascade resolves per
         declaration, so an unset max-height would inherit band B's. */
      max-height:80vh;
      left:6px; bottom:auto;
      width:min(260px, calc(50vw - 12px));
      max-width:min(260px, 46vw);
    }
    body.view-context #context {
      top:clamp(128px, 24vh, 200px);
      right:6px; bottom:auto;
      width:min(260px, calc(50vw - 12px));
      max-width:min(260px, 46vw);
    }
    #legend         { max-width:60vw; }
    /* Current Snapshot tab: layer controls and zone details used to be
       pushed down to ~28vh from the top, presumably to clear a taller
       header -- now that the narrow header is a compact couple of rows in
       normal flow above #viewport-area (see #header-narrow-row etc. above),
       that offset just leaves them floating awkwardly over the middle of
       the map. Pin them to the actual top of the map pane instead, same as
       #controls/#info sit on wide screens. Both panels are already
       collapsed to just their title bar by default here (see
       wirePanelToggles()'s narrow-screen auto-collapse in engine.js), so
       this mostly affects where that collapsed title bar sits. */
    /* ── Band C — the search owns the whole top row (bands A and B are up by
       the #zone-search component block, ~line 200) ──────────────────────
       Full width, so no clamp has to reason about the opposite corner --
       #info can be re-expanded to 60vw by the user at any time, so no static
       clamp derived from its collapsed width could have been correct. The
       rail clamp is switched off here: the rails stack BELOW a full-width
       map at this width. Everything else at top:12px drops below the search
       by exactly its height. */
    body.view-map #zone-search,
    body.view-context #zone-search,
    body.view-trends #zone-search,
    body.view-epi-trends #zone-search {
      top:12px; left:12px; width:calc(100vw - 24px);
    }
    /* No search on Genomic here: #genomic-panel has no narrow rule, so the
       map is a ~108px strip and any usable box would float over the
       phylogeny. See "Known gaps" in the design spec.

       !important is load-bearing, not defensive: Task 8's readiness gate adds
       body.view-genomic-epidemiology #zone-search.zone-search-ready
       { display:block }, which outranks this rule on specificity. Without it,
       a phone-sized Genomic page would reveal the search as soon as the tree
       coordinator registered. */
    body.view-genomic-epidemiology #zone-search { display:none !important; }
    #controls       { top:calc(12px + var(--zone-search-drop)); }
    #info {
      top:calc(12px + var(--zone-search-drop));
      max-height:calc(80vh - var(--zone-search-drop));
    }
    /* #controls' wide-screen width (min(340px, 100vw-24px) -- needed when
       expanded, to fit the search box/select/checkboxes) makes its
       collapsed-by-default title bar stretch almost the full screen width,
       overlapping the #info panel sitting in the opposite top corner.
       Shrink the collapsed box to fit just its title bar; expanding it
       (tapping the toggle) restores the full width so the controls inside
       still have room. */
    #controls.collapsed { width:auto; max-width:min(160px, calc(60vw - 18px)); }
  }
  @media (max-height: 500px) {
    /* #tracker deliberately has no rules here: this block sits above the
       unconditional #tracker rules, so an equal-specificity override in it
       would lose the source-order tiebreak and silently never apply. The
       tracker's own max-height:500px block lives below those base rules --
       see the comment there. */
    .panel          { font-size:11px; padding:5px 7px; }
    #site-header    { padding:4px 8px; }
    #site-header h1#page-heading { font-size:clamp(16px, 4.5vh, 22px); margin-bottom:1px; letter-spacing:0.2px; }
    #site-header .sub { font-size:9px; }
    #footer-links .link-btn { font-size:11px; }
    #info           { max-height:70vh; }
    #legend         { max-height:60vh; bottom:calc(var(--view-chrome-height) + 8px); }
    body.view-context #context-national { max-height:min(28vh, calc(50vh - 80px)); }
    body.view-context #context { max-height:min(55vh, calc(100vh - 200px)); }
    :root { --view-chrome-height: 28px; --partner-h: 16px; --partner-gap: 11px; }
    #view-switcher { padding:3px 8px; gap:6px; }
    #view-switcher #partners { padding:1px 2px; }
    #view-switcher #partners img { max-width:min(12vmin, 72px); }
    .view-tab { padding:3px 8px; font-size:11px; }
  }
  /* A landscape phone is band C AND max-height:500px; the 70vh cap above wins
     over band C's, so re-apply the displacement give-back here. Deliberately
     NOT folded into the max-height:500px block: a wide, short window (e.g.
     1200x450) is band A, where nothing is displaced. */
  @media (max-width: 700px) and (max-height: 500px) {
    #info { max-height:calc(70vh - var(--zone-search-drop)); }
  }
  /* Tracker stack (outbreak headline numbers, top-right of #site-header;
     rows are right-aligned to hug the header's right edge). Each metric owns
     a column; where a suspected count exists it hangs under that metric's
     label as .qual. See
     docs/superpowers/specs/2026-08-15-tracker-headline-numbers-design.md. */
  #tracker { display:flex; flex-direction:column; align-items:flex-end; margin-top:0; padding:0 4px; }
  #tracker .stats-block  { display:flex; flex-direction:column; align-items:flex-end; }
  #tracker .global-title { font-size:clamp(9px, 1.5vw, 10px); color:#bbb; text-transform:uppercase; letter-spacing:0.6px; margin-bottom:2px; text-align:right; }
  /* flex-start, not flex-end: the recovered cell carries no .qual line, so
     aligning bottoms would push its .num down out of line with the other two. */
  #tracker .global-row { display:flex; align-items:flex-start; gap:clamp(14px, 6vw, 36px); line-height:1.05; }
  #tracker .global-cell { display:flex; flex-direction:column; align-items:center; }
  #tracker .global-cell .num { font-size:clamp(20px, 6vw, 30px); font-weight:700; font-variant-numeric: tabular-nums; line-height:1; }
  #tracker .global-cell .sub { font-size:clamp(9px, 1.5vw, 10px); color:#bbb; text-transform:uppercase; letter-spacing:0.6px; margin-top:2px; }
  /* Deliberately not uppercased or letter-spaced like .sub: this is a sentence
     fragment, not a label, and staying lowercase keeps it subordinate. Hue is
     reserved for what is being counted, so a suspected count gets none -- it
     is the same thing as the number above it, less certain.

     Slightly larger than .sub on purpose: .sub is uppercase, so its cap-height
     does the work, while this line is lowercase and rides on a smaller
     x-height. At 10.5px lowercase it still reads smaller than 10px uppercase.

     nowrap is safe because #site-header sets flex-wrap:wrap -- a count long
     enough to widen the cell pushes #site-header-right onto its own row rather
     than overflowing the viewport. */
  #tracker .global-cell .qual { font-size:clamp(9.5px, 1.5vw, 10.5px); color:#bbb; margin-top:3px; font-variant-numeric: tabular-nums; white-space:nowrap; }
  #tracker .global-cell .qual .qnum { color:#ddd; font-weight:600; }
  #tracker .global-cell.cases  .num { color:#ffd166; }
  #tracker .global-cell.deaths .num { color:#ff4d4d; }
  #tracker .global-cell.recovered .num { color:#8CD790; }
  #tracker .caveat-mark { font-size:0.85em; color:#ffae42; font-weight:700; margin-left:1px; vertical-align:super; line-height:0; }
  #tracker .tracker-footnotes { margin-top:8px; max-width:min(480px, 92vw); font-size:10px; color:#aaa; line-height:1.4; text-align:left; }
  #tracker .tracker-footnotes p { margin:0 0 4px; }
  #tracker .tracker-footnotes p:last-child { margin-bottom:0; }
  #tracker .tracker-footnotes .mark { color:#ffae42; font-weight:700; margin-right:3px; }
  /* Both #tracker media blocks live HERE, after the unconditional rules above,
     and not with their siblings earlier in the file. Media queries add no
     specificity, so an equal-specificity #tracker rule in an earlier block
     loses the source-order tiebreak to the base rule below it and silently
     does nothing -- which is what had happened to every override in here.
     Only display:none survived, because no base rule declares display. */
  @media (max-width: 700px) {
    /* Centering: the base rules right-align to hug the header's right edge,
       which only makes sense in the wide two-column layout. Narrow screens
       stack #site-header-left above #site-header-right and centre both. */
    #tracker { align-items:center; margin-top:0; padding:0 2px; }
    #tracker .stats-block { align-items:center; }
    #tracker .global-title { text-align:center; }
    #tracker .global-row { gap:14px; }
    #tracker .tracker-footnotes { text-align:center; }
  }
  /* After the block above, so a landscape phone -- which matches both -- takes
     this one's tighter gap. That was the original order too. */
  @media (max-height: 500px) {
    #tracker { margin-top:0; padding:0 2px; }
    #tracker .global-title { font-size:9px; margin-bottom:0; }
    #tracker .global-row { gap:clamp(8px, 3vh, 16px); }
    #tracker .global-cell .num { font-size:clamp(16px, 4.5vh, 22px); }
    #tracker .global-cell .sub { font-size:9px; margin-top:0; }
    #tracker .global-cell .qual { font-size:9px; margin-top:1px; }
  }
  #imperial-model-estimates {
    margin-top:6px;
    font-size:11px;
    color:#bbb;
    line-height:1.35;
    text-align:center;
  }
  #imperial-model-estimates .note-wrap {
    display:inline-flex;
    align-items:center;
    justify-content:center;
    gap:6px;
    flex-wrap:wrap;
  }
  #imperial-model-estimates .info-wrap { position:relative; display:inline-flex; align-items:center; }
  #imperial-model-estimates .info-icon {
    width:16px; height:16px; border-radius:50%;
    border:1px solid #666; background:#222; color:#ffd28a;
    font-size:11px; font-weight:700; line-height:1;
    display:inline-flex; align-items:center; justify-content:center;
    padding:0; cursor:help;
  }
  #imperial-model-estimates .info-icon:hover,
  #imperial-model-estimates .info-icon:focus-visible {
    border-color:#ffae42; color:#ffae42; outline:none;
  }
  #imperial-model-estimates .info-tooltip {
    display:none;
    position:absolute;
    left:50%;
    top:calc(100% + 6px);
    transform:translateX(-50%);
    width:max-content;
    max-width:min(280px, 70vw);
    background:#1f1f1f;
    border:1px solid #444;
    border-radius:6px;
    padding:8px 10px;
    box-shadow:0 4px 16px rgba(0,0,0,0.45);
    color:#ddd;
    font-size:10px;
    line-height:1.4;
    z-index:1200;
  }
  #imperial-model-estimates .info-tooltip a {
    color:#9fcdfb; text-decoration:underline;
  }
  #imperial-model-estimates .info-tooltip a:hover { color:#ffae42; }
  #imperial-model-estimates .info-wrap:hover .info-tooltip,
  #imperial-model-estimates .info-wrap:focus-within .info-tooltip { display:block; }
  #site-header .sub { margin-top:4px; font-size:11px; opacity:0.8; }
  /* Language switch: two plain words, not a switch. Two copies exist -- the
     wide one rendered into #title-sub by langSwitchHtml(), the narrow one
     static in #header-narrow-row -- so everything here is keyed off the class,
     never an id. Colours are literals, not var(--link)/var(--muted): those
     tokens only exist in dashboard-theme.css, which the build appends only if
     the file is present (common/theme.py), so a var() here would resolve to
     nothing on the un-themed fallback. */
  select, button { background:#222; color:#eee; border:1px solid #444; padding:4px 6px; border-radius:4px; font-size:12px; box-sizing:border-box; }
  .lang-switch {
    display:inline-flex; align-items:baseline; gap:6px;
    white-space:nowrap;
  }
  .lang-switch .lang-sep { color:#555; }
  .lang-switch .lang-btn {
    background:none !important; border:none !important; border-radius:0;
    padding:0; font-size:inherit; font-family:inherit; line-height:inherit;
    cursor:pointer; color:#9fcdfb !important; text-decoration:underline;
    text-underline-offset:2px;
  }
  .lang-switch .lang-btn:hover { color:#ffae42 !important; }
  /* !important on the border too: the rule above zeroes the generic
     `select, button` border with `border:none !important`, which would
     otherwise win over a plain border-bottom here. */
  .lang-switch .lang-btn.active {
    color:#eee !important; font-weight:700; text-decoration:none;
    border-bottom:2px solid #ffae42 !important; padding-bottom:1px; cursor:default;
  }
  select { max-width:100%; }
  label { display:block; margin-top:6px; font-size:12px; color:#bbb; }
  .swatch { display:inline-block; width:18px; height:12px; margin-right:6px; vertical-align:middle; border:1px solid #444; }
  .swatch-no-data { background:transparent !important; border:1px dashed #888; }
  /* Flow-arrow legend icon: a line with a mid-line chevron, mirroring the
     on-map arrows drawn by addFlowWingMarker() in engine.js (see flowArrowSwatch). */
  .arrow-swatch { display:inline-block; width:26px; height:12px; margin-right:6px; vertical-align:middle; line-height:0; }
  .arrow-swatch svg { display:block; overflow:visible; }
  /* Each flow-arrow legend entry sits on its own line below the gray row. */
  .legend-flow-row { margin-top:3px; }
  .legend-bar { display:block; width:240px; height:12px; }
  .legend-ticks { display:flex; justify-content:space-between; font-size:10px; color:#aaa; width:240px; margin-top:2px; }
  .legend-ticks span { display:inline-block; white-space:nowrap; }
  .legend-ticks span:nth-child(1) { text-align:left;   flex:1; }
  .legend-ticks span:nth-child(2) { text-align:center; flex:1; }
  .legend-ticks span:nth-child(3) { text-align:right;  flex:1; }
  .legend-scale { font-size:10px; color:#888; margin-top:2px; }
  table { border-collapse:collapse; font-size:12px; width:100%; }
  table td { padding:2px 6px; vertical-align:top; }
  table td:first-child { color:#aaa; white-space:nowrap; }
  .info-empty { color:#888; font-style:italic; }
  .footer { font-size:10px; color:#888; margin-top:8px; }
  .checkbox-row { display:flex; align-items:center; margin-top:6px; gap:6px; }
  .case-icon { width:14px; height:14px; border-radius:50%; background:rgba(91,134,179,0.85); border:1.5px solid #fff; box-shadow:0 0 6px rgba(91,134,179,0.45); }
  .case-tt-row { display:flex; justify-content:space-between; gap:12px; }
  .case-tt-row span:last-child { font-variant-numeric: tabular-nums; }
  .genome-icon { border-radius:50%; background:rgba(91,134,179,0.75); border:1.5px solid rgba(255,255,255,0.85); box-shadow:0 0 4px rgba(91,134,179,0.35); box-sizing:border-box; }
  /* Genomic tab: a genome marker whose zone is the current coordinator selection
     (class toggled on the marker's icon element by engine.js highlightZones). */
  .leaflet-marker-icon.genome-marker-sel .genome-icon {
    background:rgba(242,200,75,0.7); border-color:#9a7a16; box-shadow:0 0 6px rgba(242,200,75,0.7);
  }
  /* Trends / Context: marker dots must not steal pointer events from the map. */
  body.view-trends .leaflet-marker-pane .leaflet-marker-icon,
  body.view-context .leaflet-marker-pane .leaflet-marker-icon { pointer-events: none !important; }
  h4 { margin: 8px 0 2px 0; font-size: 12px; color: #ffd28a; font-weight: 600; }
  .link-btn {
    display:inline-block; padding:7px 16px;
    background:#222; color:#ffd28a; text-decoration:none;
    border:1px solid #555; border-radius:4px; font-size:13px; font-weight:600;
    cursor:pointer;
  }
  .link-btn:hover { background:#333; color:#ffae42; border-color:#ffae42; }
  .modal {
    display:none; position:fixed; z-index:5000;
    top:0; left:0; right:0; bottom:0;
    background:rgba(0,0,0,0.6);
    align-items:flex-start; justify-content:center;
    padding:40px 20px; overflow-y:auto;
  }
  .modal.open { display:flex; }
  /* The sheet is capped at the overlay's content box (max-height:100%) and
     scrolls internally via .sheet-body. Without the cap the sheet has no
     height bound, so .modal's own overflow-y:auto makes the OVERLAY the
     scroll container and scrolling drags the whole dialog off screen.

     box-sizing is load-bearing, not tidiness: the sheet carries 28px of
     vertical padding, and under the default content-box, max-height:100%
     caps only the content -- the padding then pushes the outer box 56px past
     the overlay's content box, which is enough to keep the overlay
     scrollable and reintroduce the bug in miniature. */
  .modal .sheet {
    background:#1a1a1a; color:#eee;
    box-sizing:border-box;
    max-width:780px; width:100%;
    max-height:100%;
    display:flex; flex-direction:column;
    overflow:hidden;
    padding:28px 32px; border-radius:8px;
    box-shadow:0 6px 24px rgba(0,0,0,0.6);
    line-height:1.55; font-size:14px;
  }
  /* The rule keeps the scroll boundary legible: content now passes underneath
     the title rather than carrying it away, so without a divider the first
     visible line reads as if it belongs to the heading. */
  .modal .sheet-head {
    flex:0 0 auto;
    padding-bottom:10px; margin-bottom:2px;
    border-bottom:1px solid #333;
  }
  /* min-height:0 is what lets a flex item shrink below its content height --
     without it the body never becomes a scroll container and the cap above
     just clips the overflow instead. */
  .modal .sheet-body {
    flex:1 1 auto; min-height:0;
    overflow-y:auto; overscroll-behavior:contain;
  }
  .modal .close {
    float:right; cursor:pointer; font-size:18px; color:#aaa;
    background:none; border:none; padding:0;
  }
  .modal .close:hover { color:#ffae42; }
  .modal h2 { margin:0 0 4px 0; font-size:18px; color:#ffd28a; }
  .modal h3 { margin:18px 0 4px 0; font-size:15px; color:#ffd28a; }
  .modal h4 { margin:12px 0 4px 0; font-size:13px; color:#ffd28a; }
  .modal p, .modal li { margin:6px 0; }
  .modal ul { margin:6px 0 6px 20px; }
  .modal a { color:#9fcdfb; text-decoration:underline; }
  .modal a:hover { color:#ffae42; }
  .modal .methods-table { border-collapse:collapse; margin:10px 0; font-size:12px; width:100%; }
  .modal .methods-table th, .modal .methods-table td {
    border:1px solid #3a3a3a; padding:4px 8px; text-align:left; vertical-align:top;
  }
  .modal .methods-table th { background:#262626; color:#ffd28a; font-weight:600; }
  .modal .methods-table tr:nth-child(even) td { background:#1f1f1f; }

  /* Genomic Epidemiology tab (Phase 3 seam skeleton). Light rail matching the
     Trends/Spatial-Risk side panels (#f6f5f2/#2a2a27/#e7e3db); real panels +
     collapse arrive in later phases. */

  /* Hide the snapshot-map chrome on this content-rail tab, same as Trends /
     Spatial-Risk / Context do (zone-details #info, layer #controls, #legend). */
  body.view-genomic-epidemiology #controls,
  body.view-genomic-epidemiology #legend,
  body.view-genomic-epidemiology #info { display: none !important; }

  #genomic-panel {
    /* bottom clears the fixed footer (#view-switcher, z-index 1100) like the
       Trends / Spatial-Risk rails do -- otherwise the last card scrolls under it. */
    position: absolute; top: 0; right: 0; bottom: var(--view-chrome-height); width: min(634px, 70vw);
    display: flex; flex-direction: column; gap: 10px;
    background: #f6f5f2; color: #2a2a27; border-left: 1px solid #e7e3db;
    padding: 10px; overflow: auto; z-index: 500;
  }
  #genomic-panel .gen-intro {
    margin: 0; font-size: 11.5px; line-height: 1.5; color: #5c574f;
    background: #fff; border: 1px solid #e7e3db; border-radius: 4px; padding: 8px 10px;
    overflow-wrap: anywhere;
  }
  #genomic-panel .gen-intro a { color: #9b7d4e; text-decoration: underline; }
  #genomic-panel .gen-intro a:hover { color: #7c6231; }
  #genomic-panel .gen-card {
    background: #fff; border: 1px solid #e7e3db; border-radius: 4px; padding: 8px 10px;
  }
  #genomic-panel .gen-card h2 { margin: 0 0 6px; font-size: 13px; color: #2a2a27; }
  #genomic-panel .gen-body { font-size: 12px; color: #5c574f; }
  #genomic-panel .gen-card-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 6px; }
  #genomic-panel .gen-card-head h2 { margin: 0; }
  /* Panel-title tooltip: hover (or focus) the title to explain what the panel
     shows and how to read it. Mirrors the #imperial-model-estimates pattern. */
  #genomic-panel .gen-title { position: relative; display: inline-flex; align-items: center; gap: 5px; cursor: help; }
  #genomic-panel .gen-title:focus-visible { outline: none; }
  #genomic-panel .gen-info {
    width: 14px; height: 14px; border-radius: 50%; flex: none;
    border: 1px solid #c4bdb0; background: #f6f5f2; color: #9c968b;
    font-size: 10px; font-weight: 700; font-style: italic; line-height: 1;
    display: inline-flex; align-items: center; justify-content: center;
  }
  #genomic-panel .gen-title:hover .gen-info,
  #genomic-panel .gen-title:focus-visible .gen-info { border-color: #9b7d4e; color: #9b7d4e; }
  #genomic-panel .gen-tip {
    display: none; position: absolute; left: 0; top: calc(100% + 6px);
    width: max-content; max-width: min(300px, 60vw);
    background: #fff; border: 1px solid #e7e3db; border-radius: 6px; padding: 8px 10px;
    box-shadow: 0 2px 8px rgba(0,0,0,0.12); color: #5c574f;
    font-size: 10.5px; font-weight: 400; line-height: 1.45; white-space: normal; z-index: 1200;
  }
  #genomic-panel .gen-tip sub { font-size: 0.8em; }
  #genomic-panel .gen-title:hover .gen-tip,
  #genomic-panel .gen-title:focus-within .gen-tip { display: block; }
  #genomic-panel .gen-toggles { display: flex; gap: 4px; }
  #genomic-panel .gen-toggle {
    font-size: 11px; padding: 2px 7px; border: 1px solid #e7e3db; border-radius: 4px;
    background: #fff; color: #9c968b; cursor: pointer;
  }
  /* Active state = coloured border + text + a light shade of the dataset's band
     (set per-dataset in genomic.js). Deliberately NOT bold: bold changes the text
     width and reflows the button. */
  #genomic-panel .gen-chart { position: relative; height: 180px; min-height: 180px; }
  #genomic-panel .gen-chart svg { display: block; width: 100%; height: 100%; }
  #genomic-panel .ne-tip {
    position: absolute; pointer-events: none; background: rgba(255,255,255,0.97);
    border: 1px solid #e7e3db; border-radius: 6px; padding: 4px 7px; font-size: 10.5px;
    color: #2a2a27; white-space: nowrap; box-shadow: 0 1px 3px rgba(0,0,0,0.12);
  }
  #genomic-panel .ne-tip-d { font-weight: 700; margin-bottom: 2px; }
  #genomic-panel .ne-tip-ci { color: #9c968b; }

  /* ── Phylogeny (embedded PearTree) ── */
  /* The tree needs a definite height for the embed's height:100% to resolve; the
     rail scrolls, so a fixed area is fine. .gen-tree-wrap is the positioning
     context for both the embed and the overlay legend. */
  #genomic-panel .gen-tree-wrap { position: relative; height: 360px; min-height: 220px; }
  #genomic-panel .gen-tree-body { position: absolute; inset: 0; font-size: 12px; color: #5c574f; }
  /* Retint PearTree's --pt-* interface vars to the light rail palette (id
     specificity beats the bundle's [data-bs-theme]); the canvas/background comes
     from the O'Toole embed theme, not these. */
  #gen-tree-body,
  #gen-tree-body [data-bs-theme],
  #gen-tree-body[data-bs-theme] {
    --pt-bg: #ffffff; --pt-bg-panel: #f3f1ec; --pt-bg-dark: #ece9e2; --pt-bg-deepest: #ece9e2;
    --pt-border: rgba(60,48,36,0.12); --pt-border-strong: rgba(60,48,36,0.26); --pt-border-input: rgba(60,48,36,0.18);
    --pt-divider: rgba(60,48,36,0.10); --pt-divider-section: rgba(60,48,36,0.08); --pt-divider-panel: rgba(60,48,36,0.10);
    --pt-gold: #9b7d4e; --pt-primary: #9b7d4e; --pt-brand-hover: #7c1d1d; --pt-red: #b23b2e;
    --pt-selection-bg: rgba(155,125,78,0.16); --pt-selection-focus: rgba(155,125,78,0.12); --pt-resize-hover: rgba(155,125,78,0.30);
    --pt-btn-default-bg: rgba(155,125,78,0.14); --pt-btn-default-border: rgba(155,125,78,0.50); --pt-btn-default-text: #8a6d3f;
  }
  /* Trim the toolbar: hide the Fit-view pair and the filter save/apply buttons. */
  #gen-tree-body .btn-group:has(#btn-fit),
  #gen-tree-body .fc-add,
  #gen-tree-body .pt-named-filter-wrap { display: none !important; }
  /* Export modal used Bootstrap's (dark-theme) body colour → near-invisible on our
     light panel; force readable ink and lift it over Leaflet's map controls. */
  #gen-tree-body .pt-modal { color: #2a2a27; --bs-body-color: #2a2a27; --bs-secondary-color: rgba(42,42,39,0.62); }
  #gen-tree-body .pt-modal-overlay { z-index: 2000; }
  /* Zone-colour legend: an overlay in the tree area's top-right, toggled by the
     Legend button. Built from the same palette map the tips use (genomic.js). */
  /* Overlay in the tree area's top-LEFT, below the PearTree toolbar. `top` and
     `max-height` are set in genomic.js from the canvas-container's live geometry
     (the toolbar wraps to more rows on a narrow rail), so the box always clears
     the toolbar and scrolls when it's taller than the canvas area. */
  #genomic-panel .gen-tree-legend-box {
    position: absolute; top: 40px; left: 6px; z-index: 3; max-height: calc(100% - 46px);
    overflow: auto; background: rgba(255,255,255,0.95); border: 1px solid #e7e3db;
    border-radius: 4px; padding: 6px 8px; font-size: 10.5px; color: #2a2a27;
    box-shadow: 0 1px 3px rgba(0,0,0,0.12); pointer-events: auto;
  }
  #genomic-panel .gen-tree-legend-box[hidden] { display: none; }
  #genomic-panel .gen-legend-row { display: flex; align-items: center; gap: 6px; line-height: 1.5; }
  #genomic-panel .gen-legend-sw { width: 10px; height: 10px; border-radius: 2px; flex: none; border: 1px solid rgba(0,0,0,0.15); }
  #genomic-panel .gen-legend-lb { white-space: nowrap; }

  /* Drag handle on the panel's left edge (width persisted to localStorage by
     genomic.js). Sits just outside the border so it doesn't overlap content. */
  #genomic-resize {
    position: absolute; top: 0; bottom: 0; left: -3px; width: 8px;
    cursor: col-resize; z-index: 501;
  }
  #genomic-resize::before {
    content: ""; position: absolute; top: 0; bottom: 0; left: 3px; width: 1px;
    background: transparent;
  }
  #genomic-resize:hover::before,
  body.genomic-resizing #genomic-resize::before { background: #9b7d4e; width: 3px; left: 2px; }
  body.genomic-resizing { cursor: col-resize; user-select: none; }


/* --- theme overrides --- */
/*
 * BDBV epidemic dashboard — theme layer
 * Phase A: design tokens + Leaflet chrome
 * Phase B: page, panels, tracker, controls (mapped to existing dashboard IDs)
 */

:root {
  --bg: #f6f5f2;
  --card: #ffffff;
  --border: #e7e3db;
  --ink: #2a2a27;
  --muted: #9c968b;
  --maroon: #7c1d1d;
  --red: #b23b2e;
  --terracotta: #9b7d4e;
  --salmon: #f1ccc6;
  --grip: #d8d3c9;
  --link: #7c4a12;
  --link-hover: var(--maroon);
  --province-outline: #9b7d4e;
  --province-outline-weight: 1;
  /* Slightly heavier when the Trends tab is in Provincial scope, but nowhere
     near as thick as the old 2.4/3.6 defaults -- those made the boundaries
     look like they'd been drawn with a marker, especially now that the
     province shapefile has more detail (more vertices per km of coastline). */
  --province-outline-weight-wide: 1.4;
  /* ── Health-zone state styling ──────────────────────────────────────────
     One resting / hover / selected grammar for zone polygons, shared by every
     tab. See docs/superpowers/specs/2026-08-14-map-zone-state-styling-design.md.

     Every tier's weight is a MULTIPLIER on zoneWeight(zoom), never a fixed px.
     Fixed weights were the old bug: a 1.6px hub stroke or a 0.8px focus stroke
     renders thinner than the resting border at some zooms and thicker at
     others. Multipliers hold the hierarchy at every zoom.

     This whole layer is optional -- build_dashboard.py only appends it when
     non-empty -- so engine.js carries each value below as its themeVar()
     fallback, and tests/test_zone_state_styling.py fails the build if the two
     ever drift apart. */
  --zone-stroke: #fdfaf4;
  --zone-stroke-opacity: 0.7;
  --zone-stroke-weight-base: 1.7;
  /* ramp-min is both the z5 intercept and the clamp floor, deliberately: the
     map sets no minZoom, so below z5 the border simply stops thinning. */
  --zone-stroke-ramp-min: 0.6;
  --zone-stroke-ramp-max: 1.15;
  --zone-stroke-ramp-slope: 0.1;
  --zone-hover-stroke: #ffffff;
  --zone-hover-stroke-opacity: 0.98;
  --zone-hover-weight-mult: 1.7;
  --zone-selected-stroke: #ffae42;
  --zone-selected-stroke-opacity: 1;
  --zone-selected-weight-mult: 2.2;
  --zone-selected-casing: #5c3a12;
  --zone-selected-casing-opacity: 0.9;
  --zone-selected-casing-mult: 3.6;
  /* Fill-less zones: an off-white stroke over the CARTO light basemap is
     invisible, so these keep a warm grey. */
  --zone-nodata-stroke: #6b635a;
  --zone-nodata-stroke-opacity: 0.45;
  --zone-nodata-weight-mult: 1;
  --zone-hidden-weight-mult: 0.7;
  /* "Active zone with no count" is a should-never-happen state sitting on a
     solid mid-grey fill. It stays loud and stays black. */
  --zone-failloud-stroke: #111;
  --zone-failloud-stroke-opacity: 1;
  --zone-failloud-weight-mult: 1;
  --zone-focus-weight-mult: 1.35;
  /* Spatial risk dims non-focus zones by fill; without this their borders stay
     at full strength and a bright mesh fights the dimming. */
  --zone-dim-stroke-opacity: 0.25;
  --zone-role-stroke: #111;
  --zone-role-weight-mult-origin: 1.6;
  --zone-role-weight-mult-epicenter: 1.35;
  /* Provinces get their own multipliers: zone rings are zoom-scaled and
     province rings are not, so sharing multipliers would make which ring reads
     as "heavier" flip mid-zoom. */
  --province-hover-stroke: #ffffff;
  --province-hover-stroke-opacity: 0.98;
  --province-hover-weight-mult: 1.7;
  --province-selected-weight-mult: 2.2;
  --province-selected-casing-mult: 3.6;
  --case-marker: #5b86b3;
  --case-marker-opacity: 0.85;
  --green: #285943;
  --header-bg: #f4f4f4;
  --navy: #1B4671;
}

/* ── Page ─────────────────────────────────────────────────── */
html, body {
  background: var(--bg) !important;
  color: var(--ink) !important;
  font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  -webkit-font-smoothing: antialiased;
}

/* ── Floating panels ──────────────────────────────────────── */
.panel {
  background: var(--card) !important;
  color: var(--ink) !important;
  border: 1px solid var(--border) !important;
  box-shadow: 0 1px 3px rgba(30, 25, 18, 0.08) !important;
}

.panel-header strong,
#info-header strong,
#legend-title strong,
#trends-title {
  color: var(--ink);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
}

#info-toggle,
.panel-toggle {
  color: var(--terracotta) !important;
  border-color: var(--border) !important;
  background: transparent !important;
}
#info-toggle:hover,
.panel-toggle:hover {
  color: var(--maroon) !important;
  border-color: var(--terracotta) !important;
  background: rgba(155, 125, 78, 0.08) !important;
}

/* ── Header (title + tracker) ─────────────────────────────
   NOTE: this used to be #title before the dashboard was split into
   separate pages sharing one header; the header element is now
   #site-header (see Scripts/common/chrome.py). */
#site-header {
  /* Swapped with #page-tabs below: header is now --card (white), tabs bar
     is --header-bg (cream). */
  background: var(--card) !important;
}
#site-header h1#page-heading {
  color: var(--navy) !important;
  letter-spacing: -0.01em;
}
#site-header .sub {
  color: var(--muted) !important;
}
#site-header a {
  color: var(--link) !important;
}
#site-header a:hover {
  color: var(--link-hover) !important;
}

#tracker .global-title,
#tracker .global-cell .sub,
#tracker .global-cell .qual {
  color: var(--muted) !important;
}
#tracker .global-cell .qual .qnum {
  color: var(--ink) !important;
}
#tracker .global-cell.cases .num {
  color: var(--terracotta) !important;
}
#tracker .global-cell.deaths .num {
  color: var(--maroon) !important;
}
#tracker .global-cell.recovered .num {
  color: var(--green) !important;
}
#tracker .caveat-mark,
#tracker .tracker-footnotes .mark {
  color: var(--terracotta) !important;
}
#tracker .tracker-footnotes {
  color: var(--muted) !important;
}

#imperial-model-estimates {
  color: var(--muted) !important;
}
#imperial-model-estimates .info-icon {
  border-color: var(--border) !important;
  background: var(--card) !important;
  color: var(--terracotta) !important;
}
#imperial-model-estimates .info-icon:hover,
#imperial-model-estimates .info-icon:focus-visible {
  border-color: var(--terracotta) !important;
  color: var(--maroon) !important;
}
#imperial-model-estimates .info-tooltip {
  background: var(--card) !important;
  border-color: var(--border) !important;
  color: var(--ink) !important;
  box-shadow: 0 4px 16px rgba(30, 25, 18, 0.12) !important;
}
#imperial-model-estimates .info-tooltip a {
  color: var(--link) !important;
}

/* ── Controls + legend ──────────────────────────────────── */
label {
  color: var(--muted) !important;
}
select, button {
  background: var(--card) !important;
  color: var(--ink) !important;
  border-color: var(--border) !important;
}

/* Zone search box (#controls panel): the base dashboard.css styles this as
   a plain dark <input>/dropdown (#222/#1a1a1a), which was never picked up
   by the "select, button" override above since it's an <input> and a plain
   <div> listbox -- leaving a black box sitting inside the themed light
   panel. Match it to the rest of the panel chrome instead. */
#zone-search-input {
  background: var(--card) !important;
  color: var(--ink) !important;
  border-color: var(--border) !important;
}
#zone-search-input:focus {
  border-color: var(--terracotta) !important;
  box-shadow: 0 0 0 1px rgba(155, 125, 78, 0.35) !important;
}
#zone-search-results {
  background: var(--card) !important;
  border-color: var(--border) !important;
  box-shadow: 0 4px 14px rgba(30, 25, 18, 0.18) !important;
}
.zone-search-option {
  color: var(--ink) !important;
  border-bottom-color: var(--border) !important;
}
.zone-search-option:hover,
.zone-search-option.active {
  background: rgba(155, 125, 78, 0.12) !important;
  color: var(--maroon) !important;
}
.zone-search-empty {
  color: var(--muted) !important;
}
.checkbox-row label {
  color: var(--ink) !important;
}
.footer,
.legend-scale,
.legend-ticks,
#legend-gray {
  color: var(--muted) !important;
}
.info-empty {
  color: var(--muted) !important;
}
table td:first-child {
  color: var(--muted) !important;
}
h4 {
  color: var(--terracotta) !important;
}

.link-btn {
  background: var(--card) !important;
  color: var(--terracotta) !important;
  border-color: var(--border) !important;
}
.link-btn:hover {
  background: rgba(155, 125, 78, 0.08) !important;
  color: var(--maroon) !important;
  border-color: var(--terracotta) !important;
}

/* ── Page tabs (full-width bar under the header) ──────────── */
#page-tabs {
  background: var(--header-bg) !important;
  border-bottom: 1px solid var(--border) !important;
  box-shadow: 0 1px 3px rgba(30, 25, 18, 0.08) !important;
}

/* ── View switcher (full-width bottom chrome) ─────────────── */
#view-switcher {
  background: var(--card) !important;
  border-top: 1px solid var(--border) !important;
  border-left: none !important;
  border-right: none !important;
  border-bottom: none !important;
  box-shadow: 0 -1px 3px rgba(30, 25, 18, 0.08) !important;
}
.view-tab {
  background: var(--card) !important;
  color: var(--muted) !important;
  border-color: var(--border) !important;
}
.view-tab:hover {
  background: rgba(155, 125, 78, 0.08) !important;
  color: var(--ink) !important;
}
.view-tab.active {
  color: var(--terracotta) !important;
  border-color: var(--terracotta) !important;
  background: rgba(155, 125, 78, 0.12) !important;
}

/* No plate under the theme: this bar is already --card, so the box was a
   hairline rectangle drawn around eight logos, making them read as one slab.
   The base stylesheet keeps its white plate for the un-themed fallback, where
   the bar is dark -- see #view-switcher #partners in Scripts/assets/dashboard.css. */
#view-switcher #partners {
  background: none !important;
  border: none !important;
  padding: 0 !important;
  box-shadow: none !important;
}

/* Footer links and the language switch: quiet text, in the theme's link
   colours rather than the base stylesheet's amber. */
#footer-links .link-btn {
  background: none !important;
  border: none !important;
  color: var(--link) !important;
}
#footer-links .link-btn:hover {
  background: none !important;
  color: var(--link-hover) !important;
}
.lang-switch .lang-btn {
  color: var(--link) !important;
}
.lang-switch .lang-btn:hover {
  color: var(--link-hover) !important;
}
.lang-switch .lang-btn.active {
  color: var(--ink) !important;
  border-bottom: 2px solid var(--terracotta) !important;
}
.lang-switch .lang-sep {
  color: var(--border) !important;
}

#context-hint,
#travel-hint,
#flow-hint,
#flow-empty-hint {
  background: rgba(255, 255, 255, 0.92) !important;
  color: var(--muted) !important;
  border: 1px solid var(--border);
  box-shadow: 0 1px 3px rgba(30, 25, 18, 0.08) !important;
}
#trends-body.trends-empty {
  color: var(--muted) !important;
}

/* ── Context tab (national / provincial / zone pillars) ─── */
#context-national-body.context-empty,
#context-body.context-empty {
  color: var(--muted) !important;
}
.context-pillar h4 {
  color: var(--terracotta) !important;
}
.context-pillar .context-meta {
  color: var(--muted) !important;
}
.context-pillar .context-meta .scope-tag {
  background: #f3f1ec !important;
  color: var(--terracotta) !important;
  border: 1px solid var(--border);
}

/* ── "Coming soon" stub pages ──────────────────────────────── */
.stub-panel {
  background: var(--card) !important;
  color: var(--ink) !important;
  border: 1px solid var(--border) !important;
  box-shadow: 0 2px 10px rgba(30, 25, 18, 0.1) !important;
}
.stub-panel h2 {
  color: var(--terracotta) !important;
}
.stub-panel p {
  color: var(--muted) !important;
}
.context-pillar p {
  color: var(--ink) !important;
}

/* ── Modals ─────────────────────────────────────────────── */
.modal .sheet {
  background: var(--card) !important;
  color: var(--ink) !important;
  box-shadow: 0 8px 32px rgba(30, 25, 18, 0.15) !important;
}
.modal h2, .modal h3, .modal h4 {
  color: var(--terracotta) !important;
}
/* The base sheet's divider is a dark-chrome grey; on the light sheet it has to
   be the theme's border token or it reads as a black hairline. */
.modal .sheet-head {
  border-bottom-color: var(--border) !important;
}
.modal .close {
  color: var(--muted) !important;
}
.modal .close:hover {
  color: var(--maroon) !important;
}
.modal a {
  color: var(--link) !important;
}
.modal a:hover {
  color: var(--link-hover) !important;
}
.modal .methods-table th {
  background: #f3f1ec !important;
  color: var(--terracotta) !important;
  border-color: var(--border) !important;
}
.modal .methods-table td {
  border-color: var(--border) !important;
}
.modal .methods-table tr:nth-child(even) td {
  background: #faf9f7 !important;
}

/* Unscoped duplicate of the rule above -- kept in step with it deliberately;
   both have to go for the strip to lose its box. */
#partners {
  background: none !important;
  border: none !important;
  box-shadow: none !important;
}

/* ── Active-case markers ──────────────────────────────────── */
.case-icon {
  background: rgba(91, 134, 179, var(--case-marker-opacity)) !important;
  border: 1.5px solid #fff !important;
  box-shadow: 0 0 6px rgba(91, 134, 179, 0.45) !important;
}

/* ── Leaflet chrome ───────────────────────────────────────── */
.leaflet-control-zoom a {
  background: var(--card) !important;
  color: var(--ink) !important;
  border-color: var(--border) !important;
}
.leaflet-control-zoom a:hover {
  background: rgba(155, 125, 78, 0.08) !important;
  color: var(--maroon) !important;
}
.leaflet-bar {
  box-shadow: 0 1px 2px rgba(30, 25, 18, 0.1) !important;
  border-color: var(--border) !important;
}
.leaflet-control-attribution {
  font-size: 9px !important;
  color: var(--muted) !important;
  background: rgba(255, 255, 255, 0.85) !important;
}
.leaflet-container .leaflet-interactive:focus,
.leaflet-container svg:focus,
.leaflet-container path:focus {
  outline: none;
}

/* ── Epidemiological trends panel / float ─────────────────── */
#epi-trends-panel {
  background: var(--card) !important;
  color: var(--ink) !important;
  border-left-color: var(--border) !important;
}
#epi-trends-panel h2 { color: var(--ink) !important; }
#epi-trends-panel .epi-controls label,
#epi-trends-method { color: var(--muted) !important; }
#epi-trends-method a {
  color: var(--link) !important;
}
#epi-trends-method a:hover { color: var(--maroon) !important; }
#epi-trends-subtitle { color: var(--muted) !important; }
#epi-trends-downloads button {
  background: var(--card) !important;
  color: var(--terracotta) !important;
  border-color: var(--border) !important;
}
#epi-trends-downloads button:hover {
  background: rgba(155, 125, 78, 0.08) !important;
  color: var(--maroon) !important;
  border-color: var(--terracotta) !important;
}
#epi-trends-panel select,
#epi-trends-panel button.epi-rank-btn {
  background: var(--card) !important;
  color: var(--ink) !important;
  border-color: var(--border) !important;
}
#epi-trends-panel button.epi-rank-btn { color: var(--terracotta) !important; }
#epi-trends-panel button.epi-rank-btn:hover {
  background: rgba(155, 125, 78, 0.08) !important;
  color: var(--maroon) !important;
  border-color: var(--terracotta) !important;
}
#epi-trends-panel button.epi-rank-btn.active {
  background: rgba(155, 125, 78, 0.12) !important;
  color: var(--maroon) !important;
  border-color: var(--terracotta) !important;
}
#epi-trends-table-wrap { border-color: var(--border) !important; background: var(--card) !important; }
#epi-trends-table { color: var(--ink) !important; }
#epi-trends-table th,
#epi-trends-table td { border-bottom-color: var(--border) !important; }
#epi-trends-table th {
  background: #faf9f7 !important;
  color: var(--muted) !important;
}
#epi-trends-table tr:hover td { background: #f3f1ec !important; }
#epi-trends-table tr.selected td {
  background: rgba(155, 125, 78, 0.12) !important;
  color: var(--maroon) !important;
}
#epi-float.panel {
  background: var(--card) !important;
  color: var(--ink) !important;
  border-color: var(--border) !important;
}
#epi-float strong { color: var(--ink) !important; }
/* Label and value share one colour, matching the active-case marker tooltip.
   No muted first column here -- see dashboard.css/#epi-float td. */
#epi-float td { color: var(--ink) !important; }

#epi-split-handle::before {
  background: var(--grip) !important;
}
#epi-split-handle:hover::before,
body.epi-splitting #epi-split-handle::before {
  background: var(--terracotta) !important;
}

/* ── Trends panel (map left / plots column right) ─────────── */
#trends-panel {
  background: var(--bg) !important;
  color: var(--ink) !important;
  border-left-color: var(--border) !important;
}
/* Scope buttons (National / Provincial / Health Zone). These are plain
   <button> elements, which the generic "select, button" rule above already
   themes with !important -- these more-specific overrides are needed so the
   hover/active states aren't clobbered by that rule (same reason
   #epi-trends-panel button.epi-rank-btn needs its own overrides below). */
.trends-scope-btn {
  background: var(--card) !important;
  color: var(--ink) !important;
  border-color: var(--border) !important;
}
.trends-scope-btn:hover {
  background: rgba(155, 125, 78, 0.08) !important;
  color: var(--maroon) !important;
  border-color: var(--terracotta) !important;
}
.trends-scope-btn.active {
  background: rgba(155, 125, 78, 0.16) !important;
  color: var(--link) !important;
  border-color: var(--terracotta) !important;
}
/* #trends-search-results renders plain, class-less <button> results (see
   engine.js renderTrendsSearchResults), so -- like .trends-scope-btn above
   -- the hover/active state needs its own !important override or the
   generic "select, button" rule wins and the highlight never shows. */
#trends-search-results button {
  color: var(--ink) !important;
  border-bottom-color: var(--border) !important;
}
#trends-search-results button:hover,
#trends-search-results button.active {
  background: rgba(155, 125, 78, 0.12) !important;
  color: var(--maroon) !important;
}
#trends-split-handle::before {
  background: var(--grip) !important;
}
#trends-split-handle:hover::before,
body.trends-splitting #trends-split-handle::before {
  background: var(--terracotta) !important;
}

/* body.view-epi-trends #title centering override removed: the header is
   #site-header now, full-width and identical across all four pages -- see
   Scripts/common/chrome.py and the "Multi-page structure" README section. */
