/* counting-cars-plates -- "Blackwater" identity tokens (per
   swamp-ops/docs/swamplink-umbrella/plan.md §6) poured into the instrument
   layout from counting-chicken-wings' app.css. Same shell/rail/panel/readout
   system as wings.swamplink.com, so the sibling sites read as one family;
   the palette stays this project's own dark swamp water rather than wings'
   cold steel. Dark is the native Blackwater look and is the default; the
   light override exists for the toggle, not because it's the primary
   design.
*/
:root {
  --paper: #0e1214; --card: #161c1f; --well: #1b2225;
  --line: #263133; --rule: #33403f;
  --ink: #dce4e2; --dim: #7f9089; --faint: #5c6b66;

  /* Shallows teal marks provenance and interaction, same role as wings'
     --stamp. Tannin (amber) is reserved for "read this with care": the
     preliminary/estimate grade and the calculator's warm accents. */
  --stamp: #4fa08f; --stamp-soft: #3a7a6d;

  --measured: #4fa08f; --study: #6f9fe0; --industry: #a888e0;
  --estimate: #e8b13f; --bad: #c1554a;

  --field: #10161a; --hatch: rgba(232,177,63,.14);

  --mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  --display: 'Bitter', Georgia, serif;
  --body-font: 'Alegreya Sans', system-ui, -apple-system, sans-serif;
}

:root[data-theme="light"] {
  --paper: #f3f1ea; --card: #fbfaf5; --well: #eae6d9;
  --line: #ddd6c4; --rule: #b9b096;
  --ink: #1c1712; --dim: #5d5443; --faint: #8a8065;

  --stamp: #2f7d6d; --stamp-soft: #4fa08f;

  --measured: #2f7d6d; --study: #3a5fb0; --industry: #7d55c2;
  --estimate: #a8730f; --bad: #a13f34;

  --field: #ffffff; --hatch: rgba(168,115,15,.14);
}

/* Fonts load from a <link> in the templates' <head>, not an @import here:
   an @import below other rules is invalid CSS and silently dropped, which
   is exactly what happened -- the site rendered in fallback fonts with no
   error anywhere. */

* { box-sizing: border-box; }

[hidden] { display: none !important; }

body {
  margin: 0; background: var(--paper); color: var(--ink);
  font: 15.5px/1.62 var(--body-font);
}
a { color: var(--stamp); }
:focus-visible { outline: 2px solid var(--stamp); outline-offset: 2px; }


/* Quiet family bar — hub chrome is the template. One row, nowrap.
   Rail nav is scoped to .rail so it cannot stand this bar up. */
.chrome {
  display: flex;
  flex-direction: row;
  flex-wrap: nowrap;
  justify-content: flex-start;
  align-items: center;
  gap: 1rem;
  max-width: 1180px;
  margin: 0 auto;
  padding: 0.85rem 22px 0.7rem;
  border-bottom: 1px solid var(--line);
  font-family: var(--mono);
  font-size: 0.78rem;
}
.chrome .home {
  color: var(--ink);
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
}
.chrome .home:hover { color: var(--stamp); }
.chrome .home svg { width: 0.95rem; height: 0.95rem; flex: none; }
.chrome nav {
  display: flex;
  flex-direction: row;
  flex-wrap: nowrap;
  justify-content: flex-start;
  align-items: center;
  gap: 1.1rem;
  flex: 1;
  min-width: 0;
  margin: 0;
  border: 0;
}
.chrome nav a { color: var(--dim); text-decoration: none; white-space: nowrap; }
.chrome nav a:hover { color: var(--ink); }
#swampid-link { flex: none; }
@media (max-width: 400px) {
  .chrome { gap: 0.4rem; font-size: 0.7rem; }
  .chrome nav { gap: 0.45rem; }
  .chrome .home .word { display: none; }
  #swampid-link { margin-left: 0.35rem !important; }
}

/* --------------------------------------------------------------- shell */
.shell {
  display: grid; grid-template-columns: 232px minmax(0, 1fr);
  gap: 0 38px; max-width: 1180px; margin: 0 auto; padding: 0 22px 90px;
  align-items: start;
}
@media (max-width: 900px) {
  .shell { grid-template-columns: minmax(0, 1fr); gap: 0; }
}

.rail { position: sticky; top: 0; padding: 30px 0 40px; }
@media (max-width: 900px) {
  .rail { position: relative; padding: 26px 0 4px; }
}

.brand { display: flex; flex-direction: column; gap: 8px; }
h1 {
  font-family: var(--display); font-weight: 800;
  font-size: clamp(23px, 2.4vw, 28px); margin: 0; line-height: 1.1;
  letter-spacing: -0.01em;
}
.sub {
  font-family: var(--display); font-weight: 800;
  font-size: clamp(28px, 4.4vw, 42px); line-height: 1.05;
  letter-spacing: -0.02em; color: var(--estimate);
  margin: 0; max-width: 14ch; font-style: normal;
}
.spot { color: var(--dim); margin: 0; font-size: 12.5px; max-width: 34ch;
        line-height: 1.55; padding-top: 12px;
        border-top: 1px solid var(--line); }
.lnk {
  background: none; border: 0; padding: 0; cursor: pointer;
  font: inherit; color: var(--stamp);
  border-bottom: 1px solid var(--stamp-soft);
}
.lnk:hover { color: var(--ink); border-bottom-color: var(--ink); }

.rail nav { display: flex; flex-direction: column; margin: 26px 0 22px;
      border-left: 1px solid var(--line); }
.rail nav button {
  background: none; border: 0; border-left: 2px solid transparent;
  margin-left: -1px; text-align: left;
  color: var(--dim); padding: 6px 0 6px 13px; cursor: pointer;
  font-family: var(--mono); font-size: 11.5px; letter-spacing: .03em;
  text-transform: uppercase; line-height: 1.35;
}
.rail nav button:hover { color: var(--ink); }
.rail nav button.on { color: var(--stamp); border-left-color: var(--stamp);
                font-weight: 500; }
@media (max-width: 900px) {
  .rail nav {
    flex-direction: row; flex-wrap: nowrap; border-left: 0;
    border-bottom: 1px solid var(--line); margin: 16px 0 16px;
    overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch;
    mask-image: linear-gradient(to right,
      #000, #000 calc(100% - 28px), transparent);
    -webkit-mask-image: linear-gradient(to right,
      #000, #000 calc(100% - 28px), transparent);
  }
  .rail nav::-webkit-scrollbar { display: none; }
  .rail nav button {
    border-left: 0; border-bottom: 2px solid transparent;
    /* 13px vertical padding puts these at ~44px tall -- finger-sized tap
       targets on the one nav that is always touched, not moused. */
    margin: 0 0 -1px; padding: 13px 14px 13px 0; white-space: nowrap; flex: none;
  }
  .rail nav button.on { border-bottom-color: var(--stamp); }
}

.main { padding: 30px 0 0; min-width: 0; }
@media (max-width: 900px) { .main { padding-top: 4px; } }

.scope {
  font-family: var(--mono); font-size: 11px; letter-spacing: .04em;
  color: var(--faint); margin: 0 0 14px; line-height: 1.5;
  border-left: 2px solid var(--line); padding: 2px 0 2px 10px;
}
.scope b { color: var(--dim); font-weight: 500; }

/* --------------------------------------------------------------- panels */
.panel {
  background: var(--card); border: 1px solid var(--line);
  padding: 20px 22px; margin-bottom: 14px;
}
.view { display: none; } .view.on { display: block; }

.controls { display: flex; gap: 15px; flex-wrap: wrap; align-items: flex-end; }
label {
  display: block; font-family: var(--mono); font-size: 10.5px;
  color: var(--faint); text-transform: uppercase; letter-spacing: .09em;
  margin-bottom: 5px;
}
input[type=number], input[type=text], select {
  background: var(--field); color: var(--ink); border: 1px solid var(--rule);
  /* 16px, not 15: iOS Safari auto-zooms the page on focusing any field whose
     font-size is under 16px, and there is no way to opt out that doesn't
     also break pinch-zoom accessibility. */
  border-radius: 0; padding: 9px 11px; font-size: 16px; min-width: 90px;
  font-family: var(--mono);
}
input[type=number]:hover, input[type=text]:hover, select:hover { border-color: var(--stamp-soft); }
select { min-width: 200px; font-family: var(--body-font); }
input[type=text]:focus, input[type=number]:focus, select:focus {
  outline: 2px solid var(--stamp-soft); outline-offset: -1px;
}

/* ------------------------------------------------------------- readout */
.readout {
  display: grid; grid-template-columns: auto minmax(0, 1fr);
  gap: 0 20px; align-items: start; padding: 6px 0 4px;
}
@media (max-width: 620px) {
  .readout { grid-template-columns: minmax(0, 1fr); gap: 0; }
}
.headline {
  font-family: var(--display); font-weight: 800;
  font-size: clamp(48px, 8vw, 80px); letter-spacing: -0.03em;
  color: var(--ink); line-height: .9; font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}
.headline-label {
  color: var(--dim); margin-top: 10px; font-size: 15px; max-width: 32ch;
}

.band { padding-top: 12px; }
@media (max-width: 620px) { .band { padding-top: 22px; } }
.bandbar {
  position: relative; height: 34px; background: var(--well);
  border: 1px solid var(--rule);
  background-image: repeating-linear-gradient(to right,
    transparent 0, transparent calc(10% - 1px),
    var(--rule) calc(10% - 1px), var(--rule) 10%);
}
.bandfill {
  position: absolute; top: 3px; bottom: 3px; left: 3px;
  background: var(--stamp); min-width: 2px;
  transition: width .35s cubic-bezier(.22,.61,.36,1);
}
.bandfill::after {
  content: ""; position: absolute; top: -3px; bottom: -3px; right: -1px;
  width: 3px; background: var(--ink);
}
@media (prefers-reduced-motion: reduce) { .bandfill { transition: none; } }
.bandends {
  display: flex; justify-content: space-between; gap: 12px;
  font-family: var(--mono); font-size: 11px; color: var(--faint);
  margin-top: 7px; text-transform: uppercase; letter-spacing: .05em;
}
/* Marker for where the current state sits between the band's two ends --
   the calculator's own reading laid over the bucket-average scale. */
.bandmark {
  position: absolute; top: -5px; bottom: -5px; width: 2px;
  background: var(--estimate); z-index: 2;
}
.bandmark::after {
  content: attr(data-label); position: absolute; top: -18px; left: 50%;
  transform: translateX(-50%); white-space: nowrap;
  font-family: var(--mono); font-size: 10px; color: var(--estimate);
}

.floorline {
  margin-top: 22px; padding-top: 16px; border-top: 1px solid var(--line);
  color: var(--dim); max-width: 64ch;
}
.floorline b { color: var(--ink); font-weight: 600; }

/* ------------------------------------------------------------ reasoning */
details { margin-top: 18px; border-top: 1px solid var(--line); padding-top: 14px; }
summary {
  cursor: pointer; color: var(--stamp); font-family: var(--mono);
  font-size: 11.5px; text-transform: uppercase; letter-spacing: .07em;
  list-style: none; user-select: none;
}
summary::-webkit-details-marker { display: none; }
summary::before { content: "+ "; }
details[open] summary::before { content: "\2212 "; }

/* -------------------------------------------------------------- badges
   Evidence grades. "verified" figures ink solid in the measured colour;
   "preliminary" ones stay dashed and hatched, same discipline wings uses
   for its estimate grade -- a figure this project has not fully sourced
   yet must look different everywhere it appears. */
.badge {
  font-family: var(--mono); font-size: 9.5px; font-weight: 500;
  text-transform: uppercase; letter-spacing: .1em;
  padding: 2px 6px; border-radius: 0; border: 1px solid; background: transparent;
}
.badge.verified { color: var(--measured); border-color: var(--measured); }
.badge.preliminary {
  color: var(--estimate); border-color: var(--estimate); border-style: dashed;
  background: repeating-linear-gradient(45deg,
    transparent 0 3px, var(--hatch) 3px 4px);
}

/* ------------------------------------------------------------ zip lookup
   Same three-way grade the /about page's caveats already draw: "mapped"
   (documented cameras, solid measured colour), "absence" (independently
   corroborated zero, solid but a distinct hue so it never reads as "same as
   mapped"), and "gap" (no data either way -- dashed, reuses the
   .badge.preliminary hatch, because a gap deserves the same "don't over-read
   this" treatment a preliminary figure gets). */
.badge.mapped { color: var(--measured); border-color: var(--measured); }
.badge.absence { color: var(--study); border-color: var(--study); }
.badge.gap {
  color: var(--estimate); border-color: var(--estimate); border-style: dashed;
  background: repeating-linear-gradient(45deg,
    transparent 0 3px, var(--hatch) 3px 4px);
}
/* Map-only: CT's boundary-vintage note. Deliberately colorless -- neither
   the finding blue nor the warning amber, because it is neither. */
.badge.vintage { color: var(--faint); border-color: var(--faint);
                 border-style: dashed; }
.map-detail { margin-top: 12px; }

.map-zoom {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  margin: 0 0 10px; font-family: var(--mono); font-size: 11px;
  color: var(--faint); text-transform: uppercase; letter-spacing: .06em;
}
.map-zoom b { color: var(--stamp); font-weight: 500; }
.map-zoom .btn { padding: 4px 9px; font-size: 10px; }

/* ------------------------------------------------------------ county map
   One SVG path per county, class = shading step. The five mapped steps are
   the measured teal at rising opacity over the --field ground -- opacity
   steps instead of five hand-picked hues so both themes get a coherent ramp
   from the same two tokens. Absence is solid --study (blue): a corroborated
   zero is a *finding*, and must never share a visual family with the amber
   hatch that means "nobody knows". The CT vintage hatch is deliberately
   colorless (--faint): not a finding, not a gap, just a boundary footnote. */
/* aspect-ratio matches the topojson viewBox (975x610) so the box holds its
   final size from first paint -- the boundary file is ~800KB and loads
   async on first visit to this view, and without a reserved size the box
   snaps from 0 to full height once it lands, shoving the note and panels
   below it down the page (a classic CLS jump, worse here because it's felt
   as "clicking the map moved the page"). Background/border live here too
   so the reserved box reads as an empty map frame, not a blank gap, while
   waiting. */
.map-box {
  position: relative; aspect-ratio: 975 / 610; max-height: 74vh;
  background: var(--field); border: 1px solid var(--line);
}
.map-box svg {
  display: block; width: 100%; height: auto;
  /* A zoomed viewBox can be far taller than it is wide (Florida, Illinois),
     and width:100% + height:auto would make the panel metres long. Cap the
     height and let preserveAspectRatio letterbox it instead. */
  max-height: 74vh;
}
/* Counties outside the zoomed state stay visible for context but must not
   read as part of the selection. */
path.cty.offstate { opacity: .28; }
path.cty { stroke: var(--paper); stroke-width: .35; fill: var(--well); }
path.cty.m1 { fill: var(--measured); fill-opacity: .16; }
path.cty.m2 { fill: var(--measured); fill-opacity: .32; }
path.cty.m3 { fill: var(--measured); fill-opacity: .5; }
path.cty.m4 { fill: var(--measured); fill-opacity: .7; }
path.cty.m5 { fill: var(--measured); fill-opacity: .94; }
path.cty.absence { fill: var(--study); fill-opacity: .5; }
path.cty.gap { fill: url(#hatch-gap); }
path.cty.vintage { fill: url(#hatch-vintage); }
path.cty:hover { stroke: var(--ink); stroke-width: 1; }
path.cty.picked { stroke: var(--stamp); stroke-width: 1.4; }

/* Bivariate mode: camera-density tier (rows below) x crime-rate tier
   (columns below), each axis its own token (--measured for cameras,
   --bad for crime) mixed against --field at rising opacity, then the two
   axis colours mixed 50/50 -- so low-low reads as near-neutral (both mixes
   are close to --field) and high-high reads as a distinct third hue (teal
   x red), the same "read colour from CSS custom properties" contract the
   density map above already keeps. bv-nodata reuses the gap hatch (no
   Return A agency joined -- a reporting gap, not a zero); bv-vintage
   reuses the CT vintage hatch (see corpus/_build_bivariate.py). */
:root {
  --cam-1: color-mix(in srgb, var(--measured) 16%, var(--field));
  --cam-2: color-mix(in srgb, var(--measured) 50%, var(--field));
  --cam-3: color-mix(in srgb, var(--measured) 85%, var(--field));
  --crm-1: color-mix(in srgb, var(--bad) 16%, var(--field));
  --crm-2: color-mix(in srgb, var(--bad) 50%, var(--field));
  --crm-3: color-mix(in srgb, var(--bad) 85%, var(--field));
}
path.cty.bv-11 { fill: color-mix(in srgb, var(--cam-1) 50%, var(--crm-1)); }
path.cty.bv-12 { fill: color-mix(in srgb, var(--cam-1) 50%, var(--crm-2)); }
path.cty.bv-13 { fill: color-mix(in srgb, var(--cam-1) 50%, var(--crm-3)); }
path.cty.bv-21 { fill: color-mix(in srgb, var(--cam-2) 50%, var(--crm-1)); }
path.cty.bv-22 { fill: color-mix(in srgb, var(--cam-2) 50%, var(--crm-2)); }
path.cty.bv-23 { fill: color-mix(in srgb, var(--cam-2) 50%, var(--crm-3)); }
path.cty.bv-31 { fill: color-mix(in srgb, var(--cam-3) 50%, var(--crm-1)); }
path.cty.bv-32 { fill: color-mix(in srgb, var(--cam-3) 50%, var(--crm-2)); }
path.cty.bv-33 { fill: color-mix(in srgb, var(--cam-3) 50%, var(--crm-3)); }
path.cty.bv-nodata { fill: url(#hatch-gap); }
path.cty.bv-vintage { fill: url(#hatch-vintage); }

/* Shoplifting mode: fill is the 2025 NIBRS shoplifting-rate tier (--bad
   ramp, same three-step shape as the bivariate axes above); retail ALPR
   presence is NOT folded into the fill -- it's a binary flag with no
   independent corroboration source (see corpus/_build_shoplifting.py), so
   blending it into a hue would claim a resolution the data doesn't have.
   It gets a distinct stroke instead, layered on top of whichever fill
   applies, so "high shoplifting rate" and "has mapped retail ALPR" stay
   two separately-readable facts on the same county. */
path.cty.sl-1 { fill: var(--crm-1); }
path.cty.sl-2 { fill: var(--crm-2); }
path.cty.sl-3 { fill: var(--crm-3); }
path.cty.sl-nodata { fill: url(#hatch-gap); }
path.cty.sl-vintage { fill: url(#hatch-vintage); }
/* --industry (unused elsewhere on the map), not --stamp -- --stamp is the
   click-to-pin selection colour (.picked below), and reusing it here would
   make a picked, retail-marked county indistinguishable from an ordinary
   pick. */
path.cty.sl-retail { stroke: var(--industry); stroke-width: 1.1; }

/* The raw node overlay: five paths, one per density tier, each holding a
   few thousand bin squares (app.js builds them). Never hit-tested, so the
   county underneath still answers every hover and click.

   Neutral --ink rather than a data hue, deliberately: all three choropleth
   schemes below it already spend colour on a classification, and these
   squares aren't a class -- they're the observations the classes were
   computed from. Opacity carries the density instead. */
g.pts, g.ptszoom { pointer-events: none; }
/* Zoomed to one state: the layer as a whole drops back the same way
   path.cty.offstate does, and g.ptszoom (a clipped <use> of it) redraws the
   state itself at full strength. */
g.pts.offstate { opacity: .3; }
path.pt { stroke: none; fill: var(--ink); }
path.pt-1 { fill-opacity: .3; }
path.pt-2 { fill-opacity: .45; }
path.pt-3 { fill-opacity: .62; }
path.pt-4 { fill-opacity: .8; }
path.pt-5 { fill-opacity: .96; }

/* State borders need the louder stroke only in dark, where the county
   fills swallow --rule; light's paler fills keep the quiet one legible. */
path.st { fill: none; stroke: var(--dim); stroke-width: 1.1;
          stroke-opacity: .75; pointer-events: none; }
:root[data-theme="light"] path.st {
  stroke: var(--rule); stroke-width: .8; stroke-opacity: 1;
}

.map-tip {
  position: absolute; z-index: 5; pointer-events: none;
  background: var(--card); border: 1px solid var(--rule);
  color: var(--dim); padding: 7px 10px;
  font-family: var(--mono); font-size: 11.5px; line-height: 1.5;
  max-width: 240px;
}
.map-tip b { color: var(--ink); font-weight: 600; }

.map-legend {
  display: flex; align-items: center; gap: 8px 14px; flex-wrap: wrap;
  margin: 0 0 12px; font-family: var(--mono); font-size: 10.5px;
  color: var(--faint); text-transform: uppercase; letter-spacing: .06em;
}
.map-legend i {
  display: inline-block; width: 14px; height: 11px;
  background: var(--field); border: 1px solid var(--line);
}
.ml-ramp { display: inline-flex; }
.ml-ramp i { border-right-width: 0; }
.ml-ramp i:last-child { border-right-width: 1px; }
/* Legend chips repaint the same tokens as the county fills above; a chip
   that drifts from its map class lies, so change them together. */
.ml-ramp .mc-m1 { background: color-mix(in srgb, var(--measured) 16%, var(--field)); }
.ml-ramp .mc-m2 { background: color-mix(in srgb, var(--measured) 32%, var(--field)); }
.ml-ramp .mc-m3 { background: color-mix(in srgb, var(--measured) 50%, var(--field)); }
.ml-ramp .mc-m4 { background: color-mix(in srgb, var(--measured) 70%, var(--field)); }
.ml-ramp .mc-m5 { background: color-mix(in srgb, var(--measured) 94%, var(--field)); }
.ml-scale { display: inline-flex; align-items: center; }
.ml-chip { display: inline-flex; align-items: center; gap: 6px; }
.mc-absence { background: color-mix(in srgb, var(--study) 50%, var(--field)); }
.mc-gap {
  background: repeating-linear-gradient(45deg,
    var(--well) 0 3px, color-mix(in srgb, var(--estimate) 55%, var(--well)) 3px 4px);
}
.mc-vintage {
  background: repeating-linear-gradient(-45deg,
    var(--well) 0 3px, color-mix(in srgb, var(--faint) 60%, var(--well)) 3px 4px);
}
.ml-ramp .mc-sl1 { background: var(--crm-1); }
.ml-ramp .mc-sl2 { background: var(--crm-2); }
.ml-ramp .mc-sl3 { background: var(--crm-3); }
.mc-retail { background: var(--field); border-color: var(--industry); border-width: 2px; }
/* Same repaint contract as the ramps above -- these mirror path.pt-N's
   fill-opacity over the map's own background, not over --field, because
   that is what the squares actually sit on. */
.ml-ramp .mc-pt1 { background: color-mix(in srgb, var(--ink) 30%, var(--well)); }
.ml-ramp .mc-pt2 { background: color-mix(in srgb, var(--ink) 45%, var(--well)); }
.ml-ramp .mc-pt3 { background: color-mix(in srgb, var(--ink) 62%, var(--well)); }
.ml-ramp .mc-pt4 { background: color-mix(in srgb, var(--ink) 80%, var(--well)); }
.ml-ramp .mc-pt5 { background: color-mix(in srgb, var(--ink) 96%, var(--well)); }

.map-mode {
  display: flex; gap: 8px; margin: 0 0 12px;
}
.map-mode button.on { border-color: var(--stamp); color: var(--stamp); }
/* The overlay control sits on its own row under the mode row: it is not
   one of the mutually exclusive modes and should not look like one. */
.map-overlay { align-items: center; }
.mo-label {
  font-family: var(--mono); font-size: 10.5px; color: var(--faint);
  text-transform: uppercase; letter-spacing: .06em;
}

/* Bivariate legend: a real 3x3 grid (crime rate rising upward, camera
   density rising rightward) so the two axes read as axes, not a flat list
   of nine swatches -- same repaint-the-map-tokens contract as .ml-ramp. */
.bv-legend { display: flex; align-items: flex-end; gap: 14px; flex-wrap: wrap; }
.bv-grid {
  display: grid; grid-template-columns: repeat(3, 16px); grid-template-rows: repeat(3, 16px);
  gap: 1px; border: 1px solid var(--line);
}
.bv-grid i { display: block; width: 16px; height: 16px; }
/* Positioned explicitly by tier rather than relying on markup order: column
   = camera tier (1-3, left to right), row = 4 - crime tier (crime tier 3
   "highest" drawn at the top, matching the y-axis label below). Class names
   still match the map's bv-{cam}{crime} classes so the fills stay identical
   by construction -- only grid-column/row differ from the map paths. */
.bv-grid .bv-11 { grid-column: 1; grid-row: 3; background: color-mix(in srgb, var(--cam-1) 50%, var(--crm-1)); }
.bv-grid .bv-21 { grid-column: 2; grid-row: 3; background: color-mix(in srgb, var(--cam-2) 50%, var(--crm-1)); }
.bv-grid .bv-31 { grid-column: 3; grid-row: 3; background: color-mix(in srgb, var(--cam-3) 50%, var(--crm-1)); }
.bv-grid .bv-12 { grid-column: 1; grid-row: 2; background: color-mix(in srgb, var(--cam-1) 50%, var(--crm-2)); }
.bv-grid .bv-22 { grid-column: 2; grid-row: 2; background: color-mix(in srgb, var(--cam-2) 50%, var(--crm-2)); }
.bv-grid .bv-32 { grid-column: 3; grid-row: 2; background: color-mix(in srgb, var(--cam-3) 50%, var(--crm-2)); }
.bv-grid .bv-13 { grid-column: 1; grid-row: 1; background: color-mix(in srgb, var(--cam-1) 50%, var(--crm-3)); }
.bv-grid .bv-23 { grid-column: 2; grid-row: 1; background: color-mix(in srgb, var(--cam-2) 50%, var(--crm-3)); }
.bv-grid .bv-33 { grid-column: 3; grid-row: 1; background: color-mix(in srgb, var(--cam-3) 50%, var(--crm-3)); }
.bv-axes {
  display: grid; grid-template-columns: 16px auto; align-items: center;
  font-family: var(--mono); font-size: 10px; color: var(--faint);
  text-transform: uppercase; letter-spacing: .05em; row-gap: 2px;
}
.bv-axis-y {
  writing-mode: vertical-rl; transform: rotate(180deg); text-align: center;
  grid-row: 1; grid-column: 1;
}
.bv-axis-x { grid-column: 2; grid-row: 2; }

.find-form { display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end; }
.find-form input[type=text] { min-width: 140px; letter-spacing: .04em; }
.find-result { margin-top: 4px; }
.find-result .fr-head {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  margin-bottom: 10px;
}
.find-result .fr-place {
  font-family: var(--display); font-weight: 700; font-size: 20px;
  color: var(--ink);
}
.find-result .fr-body { color: var(--dim); max-width: 64ch; }
.find-result .fr-body b { color: var(--ink); font-weight: 600; }
.find-empty { color: var(--faint); font-size: 14px; }

/* --------------------------------------------------------- figure cards */
.figure-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px; }
@media (max-width: 800px) { .figure-grid { grid-template-columns: 1fr; } }
.figure-card {
  background: var(--card); border: 1px solid var(--line);
  padding: 16px 18px; display: flex; flex-direction: column; gap: 6px;
}
.figure-value-row { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.figure-value {
  font-family: var(--display); font-weight: 800; font-size: 26px;
  color: var(--ink); font-variant-numeric: tabular-nums; letter-spacing: -0.02em;
}
.figure-unit { font-family: var(--mono); color: var(--dim); font-size: 12.5px; }
.figure-subject { margin: 0; font-size: 14px; color: var(--dim); line-height: 1.5; }
.figure-meta {
  font-size: 11.5px; font-family: var(--mono); color: var(--faint);
  margin-top: 4px;
}
.figure-meta a { color: var(--stamp-soft); }

/* ------------------------------------------------------ landscape catalog
   Reuses .figure-grid/.figure-card/.badge wholesale -- these entries are
   the same visual weight as a figure, just descriptive instead of numeric,
   so the two views should read as siblings, not as different products. */
.cat-head {
  margin: 22px 0 10px; font-family: var(--display); font-size: 15px;
  color: var(--ink);
}
.cat-head:first-child { margin-top: 0; }
.ls-name { font-family: var(--display); font-weight: 700; font-size: 16px; color: var(--ink); }

.figure-chart {
  margin: 8px 0 0;
  width: 100%;
}
.figure-chart img {
  display: block;
  width: 100%;
  height: auto;
}
.figure-chart figcaption {
  margin: 8px 0 0;
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--faint);
}

/* --------------------------------------------------------------- tables */
table { width: 100%; border-collapse: collapse; font-size: 14px; }
th, td { text-align: left; padding: 7px 10px; border-bottom: 1px solid var(--line); }
th { font-family: var(--mono); color: var(--faint); font-size: 10.5px;
     font-weight: 500; text-transform: uppercase; letter-spacing: .08em; }
td.num { text-align: right; font-family: var(--mono); font-size: 13.5px;
         font-variant-numeric: tabular-nums; }
/* Sortable headers stay visually headers -- the button is a hit target and
   a keyboard stop, not a control that should look like one. */
th .sortbtn {
  all: unset; cursor: pointer; display: inline-block; width: 100%;
  font: inherit; color: inherit; letter-spacing: inherit; text-transform: inherit;
}
th.num .sortbtn { text-align: right; }
th .sortbtn:hover { color: var(--ink); }
th .sortbtn:focus-visible { outline: 2px solid var(--stamp); outline-offset: 2px; }
th[aria-sort]:not([aria-sort="none"]) .sortbtn { color: var(--ink); }

tr.picked { background: var(--well); }
tr.picked td:first-child { border-left: 2px solid var(--stamp); }

.note { color: var(--faint); font-size: 13px; }
/* Legend for the state map's colorscale -- the two ends of the same
   gradient the choropleth trace is drawn in, so the legend can't drift
   from the map. */
.swatch {
  display: inline-block; width: 12px; height: 12px; border-radius: 2px;
  vertical-align: -1px; margin-right: 5px;
}
.swatch-lo { background: var(--measured); }
.swatch-hi { background: var(--bad); }
#statemap { cursor: pointer; }
.muted { color: var(--dim); font-size: 14px; margin: 0 0 16px; max-width: 72ch; }
/* Source URLs print verbatim on figure permalinks; an unbroken one must
   wrap rather than push a phone viewport sideways. */
.muted a, .figure-meta a { overflow-wrap: anywhere; }
h3 { margin: 0 0 5px; font-family: var(--display); font-weight: 700;
     font-size: 17px; letter-spacing: -0.01em; }
h4 { margin: 0 0 4px; font-family: var(--mono); font-weight: 500;
     font-size: 11px; text-transform: uppercase; letter-spacing: .05em;
     color: var(--faint); text-align: center; }

.change-maps {
  display: grid; grid-template-columns: 1fr 1fr; gap: 18px; margin: 4px 0;
}
@media (max-width: 700px) {
  .change-maps { grid-template-columns: 1fr; }
}

.err {
  color: var(--bad); font-size: 13px; margin: 10px 0 0;
  border-left: 2px solid var(--bad); padding-left: 10px;
}
#boot-error {
  margin: 14px 22px 0; color: var(--bad); font-size: 14px;
  border: 1px solid var(--bad); padding: 12px 14px;
}

/* ---------------------------------------------------------------- news */
.news-kicker {
  font-family: var(--mono); font-weight: 500; font-size: 11px;
  text-transform: uppercase; letter-spacing: .08em; color: var(--faint);
}
.news-list { border-top: 1px solid var(--line); }
.news-row {
  display: flex; gap: 12px; align-items: baseline;
  padding: 9px 0; border-bottom: 1px solid var(--line);
}
.news-date {
  font-family: var(--mono); font-size: 11px; color: var(--stamp);
  flex: 0 0 52px;
}
.news-title { flex: 1; font-size: 14px; color: var(--ink);
              text-decoration: none; overflow-wrap: anywhere; }
.news-title:hover { color: var(--stamp); text-decoration: underline; }
.news-src {
  font-family: var(--mono); font-size: 11px; color: var(--dim);
  text-align: right; flex: 0 1 auto; max-width: 34%;
  overflow-wrap: anywhere;
}
.news-empty { margin: 10px 0 0; }
@media (max-width: 560px) {
  .news-row { flex-wrap: wrap; gap: 4px 12px; }
  .news-title { flex-basis: 100%; order: 3; }
}

/* ---------------------------------------------------------------- busy */
#busy { position: fixed; top: 0; left: 0; right: 0; height: 2px;
        background: transparent; z-index: 99; pointer-events: none; }
#busy.on::after {
  content: ""; position: absolute; top: 0; height: 2px; width: 30%;
  background: var(--stamp); animation: slide 1.1s ease-in-out infinite;
}
@keyframes slide { 0% { left: -30%; } 100% { left: 100%; } }
@media (prefers-reduced-motion: reduce) {
  #busy.on::after { animation: none; left: 0; width: 100%; opacity: .5; }
}

.btn {
  background: transparent; color: var(--dim);
  border: 1px solid var(--rule); border-radius: 0;
  padding: 8px 13px; font-family: var(--mono); font-size: 11px;
  text-transform: uppercase; letter-spacing: .07em; cursor: pointer;
}
.btn:hover { border-color: var(--stamp); color: var(--stamp); }
#theme-toggle { margin-top: 20px; width: 100%; }
@media (max-width: 900px) {
  #theme-toggle { position: absolute; top: 28px; right: 0; width: auto;
                  margin-top: 0; padding: 12px 13px; }
}

/* Build stamp. */
footer.build {
  margin-top: 32px; padding-top: 14px; border-top: 1px solid var(--line);
  color: var(--faint); font-size: 12px; display: flex; flex-wrap: wrap;
  gap: 6px 14px; align-items: baseline;
}
footer.build .sep { color: var(--line); }
footer.build code { font-family: var(--mono); font-size: 11px; color: var(--dim); }
footer.build .ver { color: var(--dim); font-weight: 600; }

footer.sisters {
  text-align: center; padding: 12px 0; color: var(--faint); font-size: 13px;
}
