
.viz {
  /* Four categorical slots, not eight. In a stacked bar a series can drop to
     zero, which puts two slots that are not neighbours in the palette order
     physically side by side -- so these charts are governed by the ALL-PAIRS
     check, not the adjacent one. Under all-pairs the six-slot set fails hard
     (dark: magenta vs aqua is dE 1.6 for a deuteranope). Enumerating every
     subset of the reference slots, four is the ceiling and this is one of the
     two that clear both modes; see _validate_palette.py --serve. */
  --s1:#247fd8; --s2:#eda100; --s3:#e87ba4; --s4:#008300;
  /* Text set inside a fill picks white or ink by the fill's luminance, so an
     in-segment label always clears contrast: white on the yellow slot measures
     2.17:1, ink on it 9.09:1. Computed with _validate_palette.contrast(). */
  --s1-ink:#0b0b0b; --s2-ink:#0b0b0b; --s3-ink:#0b0b0b; --s4-ink:#ffffff;
  --o1:#1c5cab; --o2:#2a78d6; --o3:#5598e7; --o4:#86b6ef;
  --viz-grid:#e1e0d9; --viz-axis:#c3c2b7;
}
/* Keyed off the site's own persisted/toggleable theme (html[data-theme]) rather
   than prefers-color-scheme, so charts match dark mode whenever the reader (or
   their stored preference) has actually switched it on, not just when the OS
   happens to agree -- the two can and do diverge once a manual toggle exists.
   viz-grid/viz-axis are lifted brighter than the OS-media version this
   replaces (that one measured ~1.2-1.4:1 against the dark panel, effectively
   invisible); s1-s4 keep their _validate_palette.py-checked hues unchanged. */
:root[data-theme="dark"] .viz {
  --s1:#3987e5; --s2:#c98500; --s3:#d55181; --s4:#008300;
  --s1-ink:#0b0b0b; --s2-ink:#0b0b0b; --s3-ink:#0b0b0b; --s4-ink:#ffffff;
  --o1:#86b6ef; --o2:#5598e7; --o3:#2a78d6; --o4:#1c5cab;
  --viz-grid:#47453f; --viz-axis:#aaa69e;
}
.hero { display:flex; align-items:baseline; gap:16px; flex-wrap:wrap;
  margin:6px 0 20px; }
.hero b { font-size:56px; line-height:1; font-weight:600; letter-spacing:-1px; }
.hero span { color:var(--muted); font-size:14px; max-width:520px; }
figure.chart { background:var(--panel); border:1px solid var(--line); border-radius:8px;
  padding:18px 20px 14px; margin:0 0 16px; }
figure.chart h3 { margin:0; font-size:15px; font-weight:600; }
figure.chart h3 .fignum { color:var(--muted); font-weight:400; font-variant-numeric:tabular-nums; }
figure.chart .why { color:var(--muted); font-size:13px; margin:4px 0 14px;
  max-width:70ch; line-height:1.55; }
figure.chart svg { display:block; width:100%; max-width:760px; height:auto;
  overflow:visible; }
.viz text { font:12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
  Helvetica, Arial, sans-serif; fill:var(--muted); }
.viz text.val { fill:var(--ink); font-weight:600; font-variant-numeric:tabular-nums; }
.viz text.cat { fill:var(--ink); }
/* Set by an inline style on the element, not this rule: a presentation
   attribute would lose to `.viz text` above and render muted grey. */
.viz text.inseg { font-weight:600; }
.viz .grid { stroke:var(--viz-grid); stroke-width:1; }
.viz .axis { stroke:var(--viz-axis); stroke-width:1; }
.viz .hit { fill:transparent; }
/* One entrance per chunk of cases, oldest first -- the swarm builds the way
   the record itself did, rather than appearing all at once. Grouped rather
   than per-dot: an individually animated element gets its own compositor
   layer, and animating each of a thousand-plus dots on its own overloads
   that badly enough that the reveal never finishes rather than just running
   slow. No bounce, no overshoot, and gone entirely for anyone who has asked
   their system to reduce motion. */
.viz .bee { transform-box:fill-box; transform-origin:center;
  animation:bee-in .3s cubic-bezier(.2,.6,.2,1) backwards; }
@keyframes bee-in { from { opacity:0; transform:scale(0.001); }
  to { opacity:1; transform:scale(1); } }
@media (prefers-reduced-motion: reduce) {
  .viz .bee { animation:none; }
}
.legend { display:flex; flex-wrap:wrap; gap:6px 16px; margin:12px 0 2px;
  font-size:12.5px; color:var(--ink); }
.legend i { display:inline-block; width:10px; height:10px; border-radius:2px;
  margin-right:6px; vertical-align:-1px; }
.pair { display:grid; grid-template-columns:repeat(auto-fit, minmax(330px, 1fr));
  gap:16px; }
.pair figure.chart svg { max-width:none; }
details.tbl { margin-top:12px; border-top:1px solid var(--line); padding-top:8px; }
details.tbl summary { cursor:pointer; color:var(--muted); font-size:12.5px; }
details.tbl table { min-width:0; margin-top:8px; font-variant-numeric:tabular-nums; }
details.tbl td, details.tbl th { padding:5px 10px; font-size:12.5px; }
#tip { position:fixed; z-index:9; pointer-events:none; opacity:0;
  transition:opacity .08s; background:var(--panel); color:var(--ink);
  border:1px solid var(--line); border-radius:6px; padding:6px 10px;
  font-size:12.5px; box-shadow:0 4px 14px rgba(0,0,0,.16); max-width:280px; }
#tip b { font-variant-numeric:tabular-nums; }
.wheremap { height:520px; border:1px solid var(--line); border-radius:8px;
  background:var(--bg); margin-top:10px; }
/* No tile layer here -- the map is drawn entirely from local GeoJSON
   boundaries and markers, so Leaflet's own default (a hardcoded #ddd,
   baked into leaflet.css and never theme-aware) is what shows through as
   the sea/void, a light-grey slab stranded in an otherwise dark page. */
.wheremap.leaflet-container { background:var(--bg); }
.wheremap.dead { display:flex; align-items:center; justify-content:center;
  height:auto; padding:26px; color:var(--muted); font-size:13px;
  text-align:center; }
.wheredecade { font-size:32px; font-weight:600; letter-spacing:-1px;
  margin:14px 0 -6px; color:var(--ink); min-height:38px; }
/* Leaflet's own tooltip chrome (background, border, the little pointer arrow)
   is built for a short hint, not a label sitting permanently on the map next
   to a filled circle -- stripped here so only the text remains, readable
   against map tiles of any colour via a stroke rather than a box. */
.leaflet-tooltip.wherelabel { background:transparent; border:none;
  box-shadow:none; padding:0; color:var(--ink); pointer-events:none;
  font:600 11px/1.2 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
  Helvetica, Arial, sans-serif;
  text-shadow:0 1px 2px var(--panel), 0 -1px 2px var(--panel),
    1px 0 2px var(--panel), -1px 0 2px var(--panel); }
.leaflet-tooltip.wherelabel::before { display:none; }
.wherefilters { display:flex; flex-wrap:wrap; gap:8px 18px; align-items:center;
  margin:10px 0 0; font-size:13px; }
.wherefilters label { display:flex; align-items:center; gap:7px;
  color:var(--muted); cursor:pointer; }
.wherefilters i { display:inline-block; width:10px; height:10px;
  border-radius:2px; }
.wherefilters .dec { margin-left:auto; }
.wherefilters select { font:inherit; padding:4px 8px; border-radius:6px;
  border:1px solid var(--control-border); background:var(--panel); color:var(--ink); }
/* #whereplay is the one button on this page that isn't inside a .tier-toggle/
   .monthheat-toggle/.choro-controls group, so none of those rules reach it --
   it was rendering as an unstyled native button (light chrome, in dark mode
   too, since it has no CSS tying its colours to the theme at all). Matched
   here to the same look the other toggle buttons use. */
.wherefilters button { background:var(--panel); border:1px solid var(--control-border);
  border-radius:4px; color:inherit; cursor:pointer; font:inherit; padding:4px 10px; }
.wherefilters button:hover { border-color:var(--s1); }
/* Leaflet draws its own popup on its own white; these keep it on ours. */
.leaflet-popup-content { font:13px/1.6 inherit; color:var(--ink); }
.leaflet-popup-content i { display:inline-block; width:9px; height:9px;
  border-radius:2px; margin-right:6px; }
.leaflet-popup-content .also { color:var(--muted); font-size:12px; }
.leaflet-popup-content-wrapper, .leaflet-popup-tip {
  background:var(--panel); color:var(--ink); }
.caveats { background:var(--panel); border:1px solid var(--line); border-radius:8px;
  padding:16px 20px; }
.caveats h3 { margin:0 0 8px; font-size:15px; }
.caveats li { margin-bottom:6px; color:var(--muted); font-size:13px; line-height:1.55; }
.caveats b { color:var(--ink); font-weight:600; }
.caveats p.why { margin:8px 0 0; }
h2.sect { font-size:13px; font-weight:600; text-transform:uppercase;
  letter-spacing:.08em; color:var(--muted); margin:30px 0 12px;
  padding-bottom:6px; border-bottom:1px solid var(--line); }

.morph { position:relative; width:100%; max-width:760px; margin-top:4px; }
.morph canvas { display:block; width:100%; height:auto; }
.morph .cap { position:absolute; left:5%; top:8%; max-width:60%; opacity:0;
  transition:opacity .6s ease; pointer-events:none; }
.morph .cap b { display:block; font-size:20px; font-weight:600;
  color:var(--ink); letter-spacing:-.3px; }
.morph .cap span { display:block; margin-top:6px; font-size:12.5px;
  color:var(--muted); line-height:1.5; }
.morph .year { position:absolute; right:5%; top:8%; font-size:28px;
  font-weight:600; color:var(--muted); font-variant-numeric:tabular-nums;
  opacity:0; transition:opacity .3s ease; pointer-events:none; }
.morph .morph-progress { margin-top:8px; }
.morph .morph-progress progress { appearance:none; -webkit-appearance:none;
  width:100%; height:6px; border:0; border-radius:999px; overflow:hidden;
  background:var(--viz-grid); }
.morph .morph-progress progress::-webkit-progress-bar {
  background:var(--viz-grid); border-radius:999px; }
.morph .morph-progress progress::-webkit-progress-value {
  background:var(--s1); border-radius:999px; }
.morph .morph-progress progress::-moz-progress-bar {
  background:var(--s1); border-radius:999px; }
.morph .static { display:none; padding:22px 4px; color:var(--muted);
  font-size:13px; line-height:1.6; }
@media (prefers-reduced-motion: reduce) {
  .morph canvas, .morph .cap, .morph .year, .morph .morph-progress { display:none; }
  .morph .static { display:block; }
}
/* Set by the script itself if d3 or topojson fail to load from the CDN --
   same fallback as reduced motion, since there is no map to show either way. */
.morph.dead canvas, .morph.dead .cap, .morph.dead .year,
.morph.dead .morph-progress { display:none; }
.morph.dead .static { display:block; }

.storyboard { position:relative; width:100%; max-width:760px; margin-top:4px; }
.storyboard canvas { display:block; width:100%; height:auto; }
.storyboard .card { position:absolute; left:50%; top:6%;
  transform:translateX(-50%); max-width:280px;
  background:var(--panel); border:1px solid var(--line); border-radius:8px;
  padding:12px 14px; font-size:12px; line-height:1.55; color:var(--ink);
  opacity:0; transition:opacity 1s ease; box-shadow:0 6px 20px rgba(0,0,0,.18); }
.storyboard .card i { display:block; color:var(--muted); font-size:11px;
  margin-top:8px; font-style:normal; }
.storyboard .static { display:none; padding:22px 4px; color:var(--muted);
  font-size:13px; line-height:1.6; }
@media (prefers-reduced-motion: reduce) {
  .storyboard canvas, .storyboard .card { display:none; }
  .storyboard .static { display:block; }
}

.story img { display:block; width:100%; height:auto; border-radius:6px;
  background:var(--bg); }
.story .credit { margin:6px 0 0; font-size:11px; color:var(--muted);
  line-height:1.4; }
.story h4 { margin:14px 0 4px; font-size:15px; font-weight:600; color:var(--ink); }
.story .ctx { margin:0 0 10px; font-size:12.5px; color:var(--muted); line-height:1.55; }
.story blockquote { margin:0 0 8px; padding:6px 0 6px 12px;
  border-left:3px solid var(--line); font-size:13px; font-style:italic;
  color:var(--ink); line-height:1.5; }
.story blockquote cite { display:block; margin-top:4px; font-size:11px;
  font-style:normal; color:var(--muted); }

.forcegraph { position:relative; width:100%; max-width:760px; margin-top:4px; }
.forcegraph canvas { display:block; width:100%; height:auto; }
.forcegraph .year { position:absolute; right:5%; top:5%; font-size:26px;
  font-weight:600; color:var(--muted); font-variant-numeric:tabular-nums;
  transition:opacity .4s ease; }
.forcegraph .static { display:none; padding:22px 4px; color:var(--muted);
  font-size:13px; line-height:1.6; }
@media (prefers-reduced-motion: reduce) {
  .forcegraph canvas, .forcegraph .year { display:none; }
  .forcegraph .static { display:block; }
}
.forcegraph.dead canvas, .forcegraph.dead .year { display:none; }
.forcegraph.dead .static { display:block; }

.choropleth { width:100%; max-width:760px; margin-top:4px; }
.choropleth svg { display:block; width:100%; height:auto; aspect-ratio:680/560;
  cursor:grab; touch-action:none; }
.choropleth svg:active { cursor:grabbing; }
.choropleth path { transition:opacity .15s ease; }
.choropleth path:hover, .choropleth path:focus { opacity:.75; outline:none; }
.choropleth .choro-controls { display:flex; flex-wrap:wrap; align-items:center;
  gap:6px 10px; margin-bottom:8px; }
.choropleth .choro-controls button { padding:3px 10px; font-size:13px;
  line-height:1.4; }
.choropleth .choro-zoom-in, .choropleth .choro-zoom-out { min-width:30px;
  font-weight:600; }
.choropleth .choro-hint { flex:1 1 100%; font-size:12px; color:var(--muted); }
.choropleth .ramp { display:flex; flex-wrap:wrap; align-items:center; gap:8px;
  margin:12px 0 2px; font-size:12px; color:var(--muted); }
.choropleth .ramp .bar { flex:0 0 160px; height:10px; border-radius:2px;
  background:linear-gradient(to right, #ffffb2, #fecc5c, #fd8d3c, #f03b20, #bd0026); }
.choropleth .ramp .none { display:inline-block; width:10px; height:10px;
  border-radius:2px; background:var(--viz-grid); vertical-align:-1px;
  margin-right:4px; }
.choropleth .static { display:none; padding:22px 4px; color:var(--muted);
  font-size:13px; line-height:1.6; }
.choropleth.dead svg, .choropleth.dead .ramp, .choropleth.dead .choro-controls {
  display:none; }
.choropleth.dead .static { display:block; }
.choropleth .choro-loading { display:none; align-items:center; gap:10px;
  padding:40px 4px; color:var(--muted); font-size:13px; }
.choropleth.loading .choro-loading { display:flex; }
.choropleth.loading svg, .choropleth.loading .ramp,
.choropleth.loading .choro-controls { display:none; }
.choropleth .spinner { width:16px; height:16px; border-radius:50%;
  border:2px solid var(--viz-grid); border-top-color:var(--viz-axis);
  animation:choro-spin .8s linear infinite; flex:0 0 auto; }
@keyframes choro-spin { to { transform:rotate(360deg); } }

.monthheat-toggle, .tier-toggle { display:flex; flex-wrap:wrap; gap:6px 10px;
  margin-bottom:10px; }
.monthheat-toggle button, .tier-toggle button { padding:3px 10px; font-size:13px;
  line-height:1.4; border-radius:4px; border:1px solid var(--control-border);
  background:var(--panel); color:var(--ink); font-family:inherit; cursor:pointer; }
.monthheat-toggle button.active, .tier-toggle button.active {
  background:var(--control-active-bg); color:var(--control-active-ink);
  border-color:var(--control-active-bg); font-weight:700;
  text-decoration:underline; text-decoration-thickness:2px;
  text-underline-offset:3px; }
.monthheat .mh-row rect { transition:y .6s ease, fill .5s ease; }
.monthheat .mh-row { transition:opacity .5s ease; }
.monthheat .mh-row text.cat, .monthheat .mh-row text.pct { transition:y .6s ease; }
