/* ==========================================================================
   deflockcherokee.com

   No CDN, no analytics, no third-party requests of any kind. Fonts are
   self-hosted from assets/fonts/. An anti-surveillance site that loaded Google
   Analytics — or Google Fonts — would deserve every bit of the mockery it got.

   Colours are AA-verified against the backgrounds they sit on — see
   scripts/ notes and PLAN.md §6. Don't swap an accent without re-checking.
   ========================================================================== */

:root {
  /* Two section backgrounds, alternating. The gap between them is deliberately
     wider than it looks in WCAG terms — contrast ratios compress at the dark end,
     so 1.24:1 here reads clearly as "black" vs "dark grey" on screen.

     Hard ceiling on how light any surface may go: --red only clears 4.5:1 up to
     about #27272d. Nothing in this file may be lighter than --panel-2's #262630
     behind red text. Verified numerically; re-check if you touch these. */
  --bg:            #08080a;   /* odd sections  */
  --band:          #20202a;   /* even sections */

  --surface:       #191921;   /* generic panel outside the checkerboard */
  --surface-2:     #262630;
  --border:        #2a2a33;
  --border-bright: #3d3d49;

  --text:   #f4f4f6;   /* ≥12.7:1 on every surface here */
  --muted:  #a8a8b3;   /* ≥ 5.9:1 */

  --red:        #ff4d3d;  /* 6.1:1 on --bg, 4.6:1 on the lightest panel */
  --red-bright: #ff6b5c;
  --red-link:   #ff8577;  /* ≥6.3:1 */
  --red-deep:   #c2261a;
  --green:      #4ade80;  /* ≥8.6:1 — wins only */

  /* Defaults so var(--panel) always resolves. Sections override them to
     checkerboard; elements outside a section (the ticker, the footer) get these. */
  --panel:   #191921;
  --panel-2: #262630;

  /* Line length. Both are `none` deliberately: text runs the full width of its
     container and wraps only at the edge, the way an unstyled HTML document does.

     These were 92ch and 104ch. That is still wider than typographic convention
     (60-80ch), but on a large screen it left prose filling roughly half the
     1160px container with a column of dead space beside it, and every paragraph
     wrapping well short of the edge. The ask was to use the page, so: no cap.

     The container is still bounded — `.wrap` is min(100% - 2.5rem, 1160px) — so
     "full width" means the content column, not the whole monitor. That single
     number is the one place to change the overall measure now; per-element caps
     are gone on purpose, so don't reintroduce them one selector at a time. */
  --measure: none;
  --measure-wide: none;

  /* --- hero background video: the three knobs worth touching --------------
     Adjust these; nothing else in the hero should need editing.

     --hero-scrim     How dark the overlay is where the text sits.
                      0 = no overlay, 1 = solid black.
                      >>> MINIMUM 0.90 with the current footage. <<<
                      Measured, not guessed: the brightest pixel in this clip is
                      0.218, and below a 0.90 scrim the red accent in the headline
                      drops under 4.5:1 wherever the dense cluster passes behind
                      it. Above 0.90 you are safe at any --hero-video-x.

     --hero-video-x   Horizontal position. Larger = further right.
                      The dense Atlanta cluster lands at roughly
                      (50% + this value) across the viewport, so 12% puts it at
                      about 62%. Safe range 0%-20%; past 20% the zoom no longer
                      covers the edge and you will see a hard border.

     --hero-video-y   Vertical position. Negative lifts the frame, positive drops
                      it. Safe range -15% to 15%.

     --hero-video-zoom       How far the footage is scaled up. Raising it widens
                             the safe range for x and y, at the cost of detail.
     --hero-video-brightness Applied to the footage before the scrim. The clip is
                             already near-black (mean luminance 0.009), so this
                             stays high — dropping it much erases the red dots,
                             which are the only thing worth seeing.
  */
  --hero-scrim: 0.5;
  --hero-video-x: 15%;
  --hero-video-y: 20%;
  --hero-video-zoom: 1.4;
  --hero-video-brightness: 0.9;
  --radius: 4px;

  /* Type: "protest poster" stack, chosen from fonts.html option 1.
     Anton for display, Barlow for body, IBM Plex Mono for the letter.
     All three are SIL Open Font License 1.1 and served from assets/fonts/ —
     nothing is fetched from a font CDN. */
  --display: Anton, Impact, "Haettenschweiler", "Arial Narrow Bold", sans-serif;
  --font: Barlow, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

/* --- self-hosted faces -------------------------------------------------- */
@font-face {
  font-family: Anton;
  src: url('assets/fonts/anton-400.woff2') format('woff2');
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: Barlow;
  src: url('assets/fonts/barlow-400.woff2') format('woff2');
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: Barlow;
  src: url('assets/fonts/barlow-700.woff2') format('woff2');
  font-weight: 700; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'IBM Plex Mono';
  src: url('assets/fonts/ibm-plex-mono-400.woff2') format('woff2');
  font-weight: 400; font-style: normal; font-display: swap;
}

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: clamp(1rem, 0.97rem + 0.15vw, 1.075rem);
  line-height: 1.65;
  /* Wide content (tables, the letter) scrolls inside its own container instead. */
  overflow-x: hidden;
}

/* Anton is a single-weight condensed face. Declaring 400 everywhere stops the
   browser synthesising a fake bold, which smears the already-tight counters.
   Its natural tracking is tight, so the negative letter-spacing comes off. */
h1, h2, h3, h4 {
  font-family: var(--display); font-weight: 400;
  line-height: 1.05; margin: 0 0 0.5em; letter-spacing: 0;
}
h1 { font-size: clamp(2.5rem, 1.5rem + 4.6vw, 5.2rem); letter-spacing: -0.005em; }
h2 { font-size: clamp(1.75rem, 1.3rem + 1.9vw, 2.7rem); }
h3 { font-size: clamp(1.1rem, 1rem + 0.5vw, 1.35rem); }
p  { margin: 0 0 1rem; max-width: var(--measure); }

a { color: var(--red-link); text-decoration-thickness: 1px; text-underline-offset: 3px; }
a:hover { color: var(--red-bright); }

:focus-visible {
  outline: 3px solid var(--red-bright);
  outline-offset: 3px;
  border-radius: 2px;
}

img, svg { max-width: 100%; }

/* --- skip link ----------------------------------------------------------- */
.skip {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  background: var(--red); color: var(--bg);
  padding: 0.7rem 1.1rem; font-weight: 700; border-radius: 0 0 var(--radius) 0;
}
.skip:focus { left: 0; }

/* --- layout ------------------------------------------------------------- */
.wrap { width: min(100% - 2.5rem, 1160px); margin-inline: auto; }

section { padding-block: clamp(2.25rem, 4vw, 3.75rem); }

/* Checkerboard the sections. --panel is the background for cards *inside* a section,
   so it has to shift too — otherwise cards vanish into the band on every other
   stripe. Every card, table and callout reads var(--panel), never a hard-coded
   surface colour.

   Odd sections sit on black and their cards step lighter. Even sections sit on the
   grey band and their cards step *darker* — stepping lighter there would push the
   lightest surface past the point where red text still clears AA. */
main > section {
  --panel: #191921; --panel-2: #262630;
  background: var(--bg);
}
main > section:nth-of-type(even) {
  --panel: #0f0f14; --panel-2: #1d1d26;
  background: var(--band);
}

.eyebrow {
  display: inline-block;
  font-size: 0.75rem; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--red); margin-bottom: 0.9rem;
}
.lede { font-size: 1.15em; color: var(--muted); }

/* --- nav ---------------------------------------------------------------- */
.nav {
  position: sticky; top: 0; z-index: 50;
  background: rgba(8, 8, 10, 0.92);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--border);
}
.nav-inner { display: flex; align-items: center; gap: 1.25rem; padding-block: 0.7rem; }
/* One word, all caps, split by colour: DEFLOCK white / CHEROKEE red.
   Anton is set tight by default and all-caps runs need air, so the tracking goes
   slightly positive here rather than negative.

   The wordmark MUST stay wrapped in .wm. The flex `gap` below applies between
   every flex item, and a bare text node counts as one — so `DEFLOCK<span>CHEROKEE`
   directly inside the flex container rendered a 0.6rem gap in the middle of the
   word. Keeping the whole wordmark inside one item confines the gap to the space
   after the recording light, where it belongs. */
.nav-brand {
  font-family: var(--display);
  display: inline-flex; align-items: center; gap: 0.34rem;
  font-weight: 400; color: var(--text);
  text-transform: uppercase; letter-spacing: 0.03em;
  text-decoration: none; font-size: 1.15rem; white-space: nowrap;
}
.nav-brand .wm span { color: var(--red); }

/* Recording-light ring, after deflockcobb.com. Hard-cuts between two states
   rather than fading: dark centre with a red rim, then a red centre with a white
   rim. step-end keeps it a blink, not a pulse — a fade reads as "loading",
   a blink reads as "recording".

   Hovering the brand toggles it off and back on again. Hover is the only
   control — there is no timer. Making the stop stick needs JS (js/nav.js),
   because CSS `animation-play-state: paused` resumes the moment the pointer
   leaves.

   Purely decorative, so aria-hidden in markup. */
.rec {
  flex: none; width: 16px; height: 16px; border-radius: 50%;
  border: 3px solid var(--red);
  background: var(--bg);
  animation: rec-blink 1.3s step-end infinite;
}
@keyframes rec-blink {
  0%,  49.99%  { border-color: var(--red); background: var(--bg);  }
  50%, 100%    { border-color: #f4f4f6;    background: var(--red); }
}

/* Toggled by js/nav.js on hover or focus. Removing it starts the blink again. */
.rec.is-stopped {
  animation: none;
  border-color: var(--red);
  background: var(--bg);
}

/* A blinking element is a genuine photosensitivity concern. Under reduced-motion
   it never animates at all and simply sits as a solid ring. */
@media (prefers-reduced-motion: reduce) {
  .rec { animation: none; border-color: var(--red); background: var(--bg); }
}
/* ⚠️ Hidden by default, shown only from 820px up. Measured 3 August 2026: eight
   links wrap to FIVE rows at 360px, making the sticky bar 193px tall — about a
   third of a phone screen, permanently. Row counts by width: 360px→5, 420px→4,
   520px→3, 640px→2, 820px→1. 820 is therefore the first width where the bar is a
   single 76px row, which is the only state worth showing.

   Deliberately no hamburger. The page is one long scroll with big section
   headings, the brand mark still returns you to the top, and a menu would mean
   JavaScript for navigation on a site where every other interaction degrades
   without it. Losing eight jump links on a phone costs less than that. */
.nav-links {
  display: none;
  gap: 0.35rem 1.15rem; flex-wrap: wrap;
  margin-left: auto; list-style: none; padding: 0;
  font-size: 0.9rem; font-weight: 600;
}
@media (min-width: 820px) { .nav-links { display: flex; } }
.nav-links a { color: var(--muted); text-decoration: none; }
.nav-links a:hover { color: var(--text); }

/* --- buttons ------------------------------------------------------------ */
.btn {
  display: inline-flex; align-items: center; gap: 0.55rem;
  padding: 0.85rem 1.5rem;
  font: inherit; font-weight: 700;
  border-radius: var(--radius); border: 2px solid transparent;
  text-decoration: none; cursor: pointer;
}
.btn-primary { background: var(--red); color: var(--bg); }
.btn-primary:hover { background: var(--red-bright); color: var(--bg); }
.btn-ghost { border-color: var(--border-bright); color: var(--text); background: transparent; }
.btn-ghost:hover { border-color: var(--red); color: var(--text); }

/* --- hero --------------------------------------------------------------- */
.hero { padding-block: clamp(2.5rem, 5vw, 4.5rem) clamp(3rem, 5vw, 5rem); }
.hero-grid {
  display: grid; gap: clamp(1.5rem, 4vw, 3.5rem);
  grid-template-columns: 1fr;
  align-items: center;
}
/* The art column has to widen for the logo to actually grow — at 0.65fr it was
   only ~362px, so the mark was already filling 94% of it and raising its width
   alone would have changed nothing.

   0.7fr is a deliberate ceiling, not a rounded guess. The h1 is Anton at up to
   5.2rem (~83px at a 1440px viewport), where "Take the cameras down." needs
   roughly 750px. The text column is ~745px today, so it is already near the
   limit; 0.7fr costs it only ~27px, while 0.75fr or beyond risks the headline
   wrapping mid-phrase ("Take the cameras / down."), which loses more than a
   larger graphic gains. If a bigger mark matters more than that, drop the h1
   clamp max to ~4.6rem first and then widen this. */
@media (min-width: 900px) { .hero-grid { grid-template-columns: 1.3fr 0.7fr; } }

.hero h1 { margin-bottom: 0.35em; }
.hero h1 em { font-style: normal; color: var(--red); }
.hero-sub { font-size: clamp(1.05rem, 1rem + 0.4vw, 1.3rem); color: var(--muted); }
.hero-cta { display: flex; flex-wrap: wrap; gap: 0.85rem; margin-top: 1.75rem; }
.hero-art { justify-self: center; }
/* The global `img { max-width: 100% }` keeps this from overflowing its column at
   narrower widths, so the cap can be generous. */
.hero-logo { display: block; width: min(420px, 84vw); height: auto; }

.art-note {
  margin: 0.7rem auto 0; font-size: 0.72rem; color: var(--muted);
  text-align: center; letter-spacing: 0.06em; text-transform: uppercase;
}

/* --- ticker -------------------------------------------------------------
   Marquees are a vestibular trigger. prefers-reduced-motion stops it dead
   and falls back to a static, wrapped list — not a frozen overflowing row. */
.ticker {
  border-block: 1px solid var(--border);
  background: var(--panel);
  overflow: hidden;
  padding-block: 0.7rem;
}
.ticker-track {
  display: flex; width: max-content;
  animation: scroll 48s linear infinite;
}
.ticker-group {
  /* max-width and margin are reset explicitly: .ticker-group is a <p>, so the global
     `p { max-width: var(--measure); margin-bottom: 1rem }` rule reaches it. --measure
     is `none` today, but this stays as a guard — the last time that variable held a
     real value, the cap made the nowrap content overflow its own box and overlap the
     duplicate track. flex: none stops it shrinking below max-content for the same
     reason. */
  max-width: none;
  margin: 0;
  flex: none;
  display: flex; align-items: center; gap: 2rem; padding-right: 2rem;
  font-size: 0.85rem; font-weight: 700; letter-spacing: 0.05em;
  text-transform: uppercase; white-space: nowrap; color: var(--muted);
}
.ticker-group > span { flex: none; }
.ticker-group b { color: var(--red); font-weight: 700; }

@keyframes scroll { to { transform: translateX(-50%); } }

@media (prefers-reduced-motion: reduce) {
  .ticker { padding-block: 0.9rem; }
  .ticker-track { animation: none; width: auto; flex-wrap: wrap; }
  .ticker-group { flex-wrap: wrap; white-space: normal; }
  .ticker-group[aria-hidden="true"] { display: none; }
}

/* --- the deputies section ------------------------------------------------
   The lead story: local, criminal, and found by the Sheriff's own audits. */
.dep-lede { font-size: clamp(1.1rem, 1rem + 0.5vw, 1.4rem); color: var(--text); }
.dep-lede strong { color: var(--red); }

.dep-charges { margin-top: 1.25rem; color: var(--muted); font-size: 0.95rem; }

/* Roster beside the timeline, not three big portrait cards across the page.
   Small thumbnails keep the faces present without handing them the whole screen —
   the subject of this section is the misuse of the system, not three individuals. */
.dep-layout {
  display: grid; gap: 1.5rem; margin-top: 2rem;
  grid-template-columns: 1fr;
  align-items: start;
}
@media (min-width: 860px) {
  /* Photos left, the argument right. The column is sized to the photos rather
     than to a fraction, so the text keeps a sensible measure at any width. */
  .dep-layout { grid-template-columns: minmax(0, 216px) minmax(0, 1fr); gap: clamp(1.75rem, 3vw, 2.75rem); }
}

.dep-roster {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: 0.75rem;
}
.dep-roster li {
  display: flex; align-items: center; gap: 0.9rem;
  padding: 0.7rem; border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--panel);
}
.dep-roster img {
  flex: none; width: 58px; height: 72px;        /* 4:5, small on purpose */
  object-fit: cover; border-radius: 3px;
  background: var(--panel-2);
  /* Mild desaturation only — the three sources have different colour casts and this
     makes them read as one set. No vignette, no heavy grade: nothing that
     editorialises the faces of people who have not been convicted. */
  filter: saturate(0.82) contrast(1.02);
}
.dep-roster > li > div { min-width: 0; }
.dep-name {
  font-size: 1rem; font-weight: 400; margin: 0 0 0.1rem;
  letter-spacing: -0.01em; line-height: 1.15;
}
.dep-role {
  font-size: 0.72rem; color: var(--red); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.08em; margin: 0;
}

.dep-how {
  padding: clamp(1.4rem, 3vw, 2rem);
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
}
.dep-provenance {
  margin: 1.25rem 0 0; padding-top: 1.1rem;
  border-top: 1px solid var(--border);
  font-size: 0.82rem; color: var(--muted);
}
.dep-how h3 { margin-bottom: 0.8rem; }
.dep-how ol { margin: 0; padding-left: 1.3rem; display: flex; flex-direction: column; gap: 0.6rem; }
.dep-how li { font-size: 0.95rem; color: var(--muted); max-width: var(--measure-wide); }
.dep-how li strong { color: var(--text); }

/* --- wins --------------------------------------------------------------- */
/* No section-level tint here — the checkerboard owns section backgrounds now. */
.win-card {
  border: 1px solid var(--border);
  background: var(--panel); border-radius: var(--radius);
  padding: clamp(1.5rem, 3vw, 2.25rem);
}
.win-card .eyebrow { color: var(--green); }
/* No left rule — quotes are set off by size, italics and an inset panel instead. */
.win-quote {
  margin: 1.5rem 0 0; padding: 1.1rem 1.25rem;
  background: var(--panel-2); border-radius: var(--radius);
  font-size: 1.1rem; font-style: italic; color: var(--text);
}
.win-quote cite { display: block; margin-top: 0.5rem; font-size: 0.85rem; font-style: normal; color: var(--muted); }

/* --- stat row ----------------------------------------------------------- */
.stats {
  display: grid; gap: 1px; margin-top: 2.5rem;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  background: var(--border); border: 1px solid var(--border);
  border-radius: var(--radius); overflow: hidden;
}
.stat { background: var(--panel); padding: 1.25rem 1.1rem; }
.stat-num {
  font-family: var(--display);
  display: block; font-size: clamp(1.9rem, 1.4rem + 1.8vw, 2.6rem);
  font-weight: 400; letter-spacing: -0.04em; color: var(--red); line-height: 1;
}
.stat-label { display: block; margin-top: 0.4rem; font-size: 0.8rem; color: var(--muted); line-height: 1.4; }

/* --- case cards --------------------------------------------------------- */
.cards {
  display: grid; gap: 1.25rem; margin-top: 2.5rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 330px), 1fr));
}
.card {
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 1.5rem;
  display: flex; flex-direction: column;
}
.card-tag {
  font-size: 0.7rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--red); margin-bottom: 0.7rem;
}
.card h3 { margin-bottom: 0.6rem; }
.card p { font-size: 0.95rem; color: var(--muted); }
.card p strong { color: var(--text); font-weight: 700; }
.card-sources { margin-top: auto; padding-top: 1rem; display: flex; flex-direction: column; gap: 0.3rem; }
.card-sources a { font-size: 0.82rem; }

/* --- linked video thumbnails ---------------------------------------------
   Self-hosted stills that link out to YouTube. Deliberately NOT iframes and NOT
   hotlinked images: either would make Google a party to every page load. The
   play triangle is drawn in CSS from clip-path, so there is no icon file and
   nothing else to fetch.

   They sit directly under the card heading, two across. At a ~370px card that
   leaves each still about 170px wide, so everything below is scaled for that
   size rather than the full-width version this replaced.

   No captions: the titles are baked into the thumbnails themselves, so repeating
   them underneath was saying the same thing twice. That makes each alt attribute
   the link's entire accessible name — see the note in index.html. */
.vids {
  list-style: none; margin: 0 0 1.25rem; padding: 0;
  display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem;
}
.vids a { display: block; text-decoration: none; }
.vids img {
  display: block; width: 100%; height: auto; border-radius: var(--radius);
  border: 1px solid var(--border-bright);
}
/* The badge is centred on the still itself, not on the whole link — hence the
   wrapper. Centring it against the anchor would mean doing arithmetic against
   the title and byline underneath, which breaks the moment a title wraps. */
.vid-shot { position: relative; display: block; }
.vid-shot::after,
.vid-shot::before { content: ''; position: absolute; pointer-events: none; }
.vid-shot::after {
  left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: 2rem; height: 2rem; border-radius: 50%;
  background: rgba(8, 8, 10, 0.72); border: 2px solid #f4f4f6;
}
/* Play triangle, drawn with clip-path so there is no icon file to fetch.
   Nudged right of centre because a triangle's optical centre sits left of its
   bounding box. */
.vid-shot::before {
  left: 50%; top: 50%; z-index: 1;
  transform: translate(-36%, -50%);
  width: 0.66rem; height: 0.76rem; background: #f4f4f6;
  clip-path: polygon(0 0, 100% 50%, 0 100%);
}
/* Hover is now the only affordance — there is no caption to colour — so the
   still and the badge both have to move. */
.vids a:hover img { border-color: var(--red); }
.vids a:hover .vid-shot::after { background: var(--red); border-color: var(--red); }
/* Inset caveat note. Reads as a distinct aside via its panel, not a left rule. */
.card-flag {
  margin: 0 0 1rem; padding: 0.8rem 0.95rem;
  background: var(--panel-2); border-radius: var(--radius);
  font-size: 0.88rem; color: var(--muted);
}
.card-flag b { color: var(--text); }
.card.card-wide { grid-column: 1 / -1; }

/* The Case is a deliberate 3x2: six cards, three across, two rows. Fixed columns
   rather than auto-fit so the grid can't reflow into an odd 4+2 or 2+2+2 at
   in-between widths. The cards are trimmed to comparable lengths so the two rows
   sit at similar heights — `align-items: stretch` (the grid default) makes every
   card in a row as tall as its tallest sibling, so one long card would leave
   white space in the other two.

   Scoped to #case: the HOA section reuses .cards with 3 and 2 cards, where
   auto-fit is the right behaviour. */
@media (min-width: 1000px) {
  #case .cards { grid-template-columns: repeat(3, 1fr); }
  #case .card.card-wide { grid-column: auto; }
}
@media (min-width: 620px) and (max-width: 999px) {
  #case .cards { grid-template-columns: repeat(2, 1fr); }
  #case .card.card-wide { grid-column: auto; }
}

/* --- Q&A ----------------------------------------------------------------
   Native <details>/<summary>: keyboard-operable, screen-reader-announced, and
   indexed by search engines even while collapsed — all without a line of JS. */
.qa {
  margin-top: 2.5rem;
  display: flex; flex-direction: column; gap: 0.75rem;
}
.qa details {
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius);
}
/* `.reveal` rides along with all of this — same disclosure widget, used for any
   block long enough that most readers would rather scroll past it. Keeping one
   implementation means the marker only has to be got right once. */
.qa summary,
.reveal > summary {
  display: flex; align-items: flex-start; gap: 0.9rem;
  padding: 1.05rem 1.25rem;
  cursor: pointer; list-style: none;
  font-size: clamp(1rem, 0.96rem + 0.25vw, 1.15rem);
  font-weight: 700; color: var(--text);
}
/* Kill the default disclosure triangle in every engine, then draw our own. */
.qa summary::-webkit-details-marker,
.reveal > summary::-webkit-details-marker { display: none; }
.qa summary::marker,
.reveal > summary::marker { content: ''; }
/* The +/− is drawn, not typed. As glyphs they inherit Barlow's metrics — the
   minus sits on the maths axis, the plus does not — so neither lands on the
   optical centre of the box. Two background layers, each positioned `center`,
   cannot drift: the open state simply drops the vertical one. */
/* The offset is derived, not eyeballed. `align-items: flex-start` puts the square
   at the top of the summary's content box, so centring it on the first line of
   the question means pushing it down by half the difference between the line box
   and the square. The old hand-picked 0.12rem left it 2.5px high, which is small
   enough to look like a mistake rather than a style. The literal fallback stays
   for anything without `lh` units. */
.qa summary::before,
.reveal > summary::before {
  content: '';
  flex: none; width: 1.35rem; height: 1.35rem;
  margin-top: 0.12rem;
  margin-top: calc((1lh - 1.35rem) / 2);
  border: 1px solid var(--red); border-radius: 3px;
  background:
    linear-gradient(var(--red), var(--red)) center / 0.62rem 1.7px no-repeat,
    linear-gradient(var(--red), var(--red)) center / 1.7px 0.62rem no-repeat;
}
.qa details[open] summary::before,
.reveal[open] > summary::before {
  background:
    linear-gradient(var(--red), var(--red)) center / 0.62rem 1.7px no-repeat;
}
.qa summary:hover,
.reveal > summary:hover { color: var(--red-bright); }
.qa details[open] summary { padding-bottom: 0.5rem; }

/* --- disclosure block ----------------------------------------------------
   Same widget as the Q&A, sized like a section sub-heading rather than a
   question, because that is what it replaces. */
.reveal {
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius);
}
.reveal > summary {
  font-family: var(--display); font-weight: 400;
  font-size: clamp(1.35rem, 1.1rem + 1vw, 1.9rem);
  letter-spacing: 0.01em;
  padding: 1.15rem 1.35rem;
}
.reveal[open] > summary { padding-bottom: 0.35rem; }
.reveal-body { padding: 0.5rem 1.35rem 1.5rem; }
.reveal-body > :last-child { margin-bottom: 0; }
@media (min-width: 600px) { .reveal-body { padding-left: 3.6rem; } }

/* Single column, deliberately. This was briefly `columns: 2` to fill the row —
   it forced the reader down the left and back up the right, and broke sentences
   across the gutter mid-clause. Don't reintroduce it. */
.qa-body { padding: 0 1.25rem 1.35rem; }
/* Indent the answer to line up with the question text, past the +/− marker. */
@media (min-width: 600px) { .qa-body { padding-left: 3.5rem; } }
.qa-body p { color: var(--muted); font-size: 0.97rem; max-width: var(--measure-wide); }
.qa-body p:last-child { margin-bottom: 0; }
.qa-body strong { color: var(--text); }
.qa-list {
  margin: 0 0 1rem; padding-left: 1.2rem;
  display: flex; flex-direction: column; gap: 0.55rem;
  color: var(--muted); font-size: 0.97rem; max-width: var(--measure-wide);
}
.qa-list strong { color: var(--text); }
.qa-sources {
  display: flex; flex-direction: column; gap: 0.3rem;
  margin-top: 1.1rem; padding-top: 0.9rem;
  border-top: 1px solid var(--border);
}
.qa-sources a { font-size: 0.82rem; }

/* --- map ---------------------------------------------------------------- */
.map-shell { margin-top: 2rem; }
/* ⚠️ z-index/isolation are load-bearing, not decoration. Leaflet assigns its panes
   and controls z-indexes up to 1000 (200 tiles, 400 overlay, 600 marker, 700 popup,
   800 control, 1000 the corner containers). The sticky .nav is z-index 50, so the
   map painted straight over the header as you scrolled past it — on desktop and
   mobile both.

   The fix is a stacking context rather than an arms race: with `z-index: 0` the whole
   map subtree paints as one layer at level 0 in the root context, so Leaflet's 1000
   is only 1000 *within* the map. `isolation: isolate` states the same intent for
   anything that ignores the z-index route. Do not "fix" a future overlap by raising
   .nav above 1000 — that just moves the fight. */
#map {
  height: clamp(340px, 55vh, 560px);
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface-2);
  position: relative; z-index: 0; isolation: isolate;
}
.leaflet-container { font-family: var(--font); background: var(--surface-2); }
/* OSM raster tiles are bright; knock them back so the map sits in a dark page. */
.leaflet-tile-pane { filter: invert(1) hue-rotate(180deg) brightness(0.92) contrast(0.9) saturate(0.55); }
.leaflet-control-attribution {
  background: rgba(10, 10, 12, 0.85) !important; color: var(--muted) !important; font-size: 10px !important;
}
.leaflet-control-attribution a { color: var(--red-link) !important; }
.leaflet-bar { border: 1px solid var(--border) !important; }
.leaflet-bar a {
  background: var(--surface) !important; color: var(--text) !important;
  border-bottom-color: var(--border) !important;
}
.leaflet-bar a:hover { background: var(--surface-2) !important; }
.leaflet-popup-content-wrapper, .leaflet-popup-tip {
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border-bright); border-radius: var(--radius);
}
.leaflet-popup-content { margin: 0.75rem 0.9rem; font-size: 0.85rem; }
.leaflet-popup-content b { color: var(--red); }
.leaflet-popup-close-button { color: var(--muted) !important; }

.map-note { margin-top: 1.25rem; font-size: 0.85rem; color: var(--muted); }

/* Legend for the two camera layers. */
.map-legend {
  display: flex; flex-direction: column; gap: 0.3rem;
  padding: 0.55rem 0.7rem;
  background: rgba(8, 8, 10, 0.88);
  border: 1px solid var(--border); border-radius: var(--radius);
  font-size: 0.72rem; color: var(--text); line-height: 1.3;
}
.map-legend span { display: flex; align-items: center; gap: 0.45rem; }
.map-legend i {
  width: 9px; height: 9px; border-radius: 50%; flex: none;
  background: var(--red);
}
.map-legend .dot-here { background: var(--red); }
.map-legend .dot-near { background: var(--red-deep); width: 7px; height: 7px; }
.map-legend .legend-hint {
  margin-top: 0.2rem; padding-top: 0.35rem;
  border-top: 1px solid var(--border);
  color: var(--muted); font-size: 0.66rem;
}

/* Shown only when JS is unavailable or the map fails to load. */
.map-fallback {
  padding: 1.25rem; border: 1px dashed var(--border-bright);
  border-radius: var(--radius); background: var(--panel); font-size: 0.92rem;
}

/* --- contract renewal tracker -------------------------------------------- */
.ct-grid {
  display: grid; gap: 1.25rem; margin-top: 2rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
}
.ct-card {
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 1.4rem;
  display: flex; flex-direction: column;
}
/* Urgency reads through the border, not a left bar. */
.ct-card.is-urgent { border-color: var(--red); }
.ct-card.is-soon   { border-color: var(--border-bright); }

.ct-head { display: flex; flex-wrap: wrap; gap: 0.5rem 0.85rem; align-items: baseline; }
.ct-head h3 { margin: 0; }
.ct-status {
  font-size: 0.65rem; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase;
  padding: 0.2rem 0.5rem; border-radius: 999px; white-space: nowrap;
  border: 1px solid var(--border-bright); color: var(--muted);
}
.ct-status.is-active    { background: var(--red);   color: var(--bg); border-color: var(--red); }
.ct-status.is-pending   { background: var(--red-deep); color: #fff;   border-color: var(--red-deep); }
.ct-status.is-canceled { background: var(--green); color: var(--bg); border-color: var(--green); }
.ct-status.is-none      { color: var(--green); border-color: var(--green); }

.ct-body-name { font-size: 0.82rem; color: var(--muted); margin: 0.35rem 0 1rem; }

.ct-deadline {
  margin: 0 0 1.1rem; padding: 0.85rem 1rem;
  background: var(--panel-2); border-radius: var(--radius);
  display: flex; flex-direction: column; gap: 0.2rem;
}
.ct-days {
  font-family: var(--display); font-size: 1.9rem; line-height: 1;
  color: var(--muted); letter-spacing: 0.01em;
}
.ct-deadline.is-urgent .ct-days { color: var(--red); }
.ct-deadline.is-soon   .ct-days { color: var(--text); }
.ct-deadline.is-past   .ct-days { color: var(--muted); }
.ct-when { font-size: 0.8rem; color: var(--muted); line-height: 1.4; }
.ct-when strong { color: var(--text); }

.ct-facts { margin: 0 0 1.1rem; display: grid; gap: 0.4rem; }
.ct-facts > div { display: flex; gap: 0.6rem; font-size: 0.85rem; }
.ct-facts dt { flex: none; width: 9.5rem; color: var(--muted); }
.ct-facts dd { margin: 0; color: var(--text); }

.ct-notes { font-size: 0.86rem; color: var(--muted); margin: 0 0 1rem; }
.ct-meetings { font-size: 0.82rem; color: var(--muted); margin: 0 0 1rem; }
.ct-meetings strong { color: var(--text); }
.ct-links {
  margin: auto 0 0; padding-top: 0.9rem; border-top: 1px solid var(--border);
  display: flex; flex-direction: column; gap: 0.3rem;
}
.ct-links a { font-size: 0.8rem; }
.ct-error { color: var(--muted); font-size: 0.92rem; }

.ct-ask {
  margin: 0 0 1rem; padding-left: 1.3rem;
  display: flex; flex-direction: column; gap: 0.45rem;
  font-size: 0.9rem; color: var(--muted); max-width: var(--measure-wide);
}

/* --- steps -------------------------------------------------------------- */
/* Five items, so the track minimum is tuned to land on three columns at full
   width — 3 + 2 reads as a deliberate arrangement, where the 4 + 1 you get from
   a 270px minimum leaves card five alone beside three empty columns and looks
   like something got deleted. If the list changes length, re-check this number. */
.steps {
  display: grid; gap: 1.25rem; margin-top: 2.5rem; padding: 0; list-style: none;
  counter-reset: step;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr));
}
.steps li {
  counter-increment: step;
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 1.4rem;
}
/* The number belongs to the heading, not above it. As its own block at 1.9rem it
   sat on a line of its own with the title beneath, which read as an indent rather
   than as a numbered item. On the h3 it inherits the heading's face and size and
   sets on the same line, and a title that wraps flows under the number normally.
   em, not rem, so the gap tracks the heading's own clamp(). */
.steps h3 { font-size: 1.05rem; margin-bottom: 0.4rem; }
.steps h3::before {
  content: counter(step);
  color: var(--red); margin-right: 0.45em;
}
.steps p { font-size: 0.9rem; color: var(--muted); margin: 0; }

/* --- letter ------------------------------------------------------------- */
/* ⚠️ display:none is the DEFAULT, and that is deliberate. The letter picker is an
   enhancement: js/letter-switch.js adds .is-switched once it can actually drive the
   dropdown. Without JavaScript the control never appears and every .letter-choice
   stays visible, so a reader sees both letters instead of a dead <select>.

   Corollary, and the thing not to get wrong: never hide a .letter-choice from this
   stylesheet. The hiding belongs to the script, so its absence fails open. */
.letter-controls {
  display: none;
  flex-wrap: wrap; gap: 0.85rem; align-items: flex-end;
  margin-top: 2rem; margin-bottom: 1.25rem;
}
.letter-controls.is-switched { display: flex; }

/* Space between the two drafts when JavaScript is off and both are showing. Costs
   nothing when only one is visible. */
.letter-choice + .letter-choice { margin-top: 2.25rem; }
/* min-width:0 and max-width:100% together stop the select from pushing past the
   viewport on a narrow phone. A <select> takes its intrinsic width from its
   longest option, and a flex item won't shrink below that on its own. */
.field { display: flex; flex-direction: column; gap: 0.35rem; flex: 1 1 min(100%, 320px); min-width: 0; }
.field label { font-size: 0.78rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }
.field select {
  max-width: 100%;
  padding: 0.8rem 1rem; font: inherit;
  background: var(--panel); color: var(--text);
  border: 1px solid var(--border-bright); border-radius: var(--radius);
}

.letter-box {
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--panel); overflow: hidden;
}
.letter-head {
  display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center;
  padding: 0.7rem 1rem; border-bottom: 1px solid var(--border);
  background: var(--panel-2);
  font-size: 0.72rem; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted);
}
.letter-head .btn { margin-left: auto; padding: 0.5rem 1rem; font-size: 0.8rem; letter-spacing: 0; text-transform: none; }
/* No measure cap, matching the rest of the page — the draft fills its panel
   rather than sitting in a 76ch column with the panel's right half empty.
   The one thing still wrapped to a fixed width is the numbered list in
   js/letters.js, which hard-wraps at 72 columns because clipboard text can't
   carry a CSS hanging indent. That is comfortably inside any panel width, so
   the two no longer have to agree. */
.letter-body {
  margin: 0; padding: 1.5rem;
  font-family: var(--mono); font-size: 0.85rem; line-height: 1.7;
  white-space: pre-wrap; overflow-x: auto;
  color: var(--text);
}
.letter-tip {
  margin-top: 1.25rem; padding: 1rem 1.15rem;
  border: 1px solid var(--border);
  background: var(--panel); border-radius: var(--radius); font-size: 0.92rem;
}
.letter-tip p:last-child { margin-bottom: 0; }

/* --- officials ---------------------------------------------------------- */
.gov { display: grid; gap: 1.25rem; margin-top: 2.5rem; }
.gov-card {
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius); padding: clamp(1.25rem, 3vw, 1.9rem);
}
/* Emphasis now comes from the count pill and heading size, not a left rule. */
.gov-card.is-major { padding: clamp(1.5rem, 3.5vw, 2.25rem); }
.gov-head { display: flex; flex-wrap: wrap; gap: 0.6rem 1rem; align-items: baseline; margin-bottom: 0.35rem; }
.gov-head h3 { margin: 0; }
.gov-count {
  font-size: 0.72rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--bg); background: var(--red);
  padding: 0.25rem 0.6rem; border-radius: 999px; white-space: nowrap;
}
.gov-count.is-zero { background: var(--green); }
.gov-meta { font-size: 0.88rem; color: var(--muted); margin-bottom: 1rem; }

.table-scroll { overflow-x: auto; margin-block: 1rem; }
table { border-collapse: collapse; width: 100%; min-width: 380px; font-size: 0.9rem; }
caption { text-align: left; font-size: 0.8rem; color: var(--muted); padding-bottom: 0.6rem; }
th, td { text-align: left; padding: 0.6rem 0.9rem 0.6rem 0; border-bottom: 1px solid var(--border); }
th { font-size: 0.72rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); font-weight: 700; }
td:last-child, th:last-child { padding-right: 0; }

.gov-grid { display: grid; gap: 1.25rem; grid-template-columns: repeat(auto-fit, minmax(min(100%, 250px), 1fr)); }

.verify {
  display: inline-block; margin-top: 0.4rem;
  font-size: 0.72rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--bg); background: var(--muted);
  padding: 0.15rem 0.45rem; border-radius: 3px;
}

/* --- callouts ----------------------------------------------------------- */
.callout {
  border: 1px solid var(--border);
  background: var(--panel); border-radius: var(--radius);
  padding: 1.25rem 1.4rem; margin-block: 1.5rem;
}
.callout p { max-width: var(--measure-wide); }
.callout p:last-child { margin-bottom: 0; }
.callout h3 { font-size: 1.05rem; }
/* Cautionary variant: muted tag and a dashed edge instead of a coloured bar. */
.callout-warn { border-style: dashed; border-color: var(--border-bright); }
.callout-warn .callout-tag { color: var(--muted); }
/* A direct ask of the reader. Solid red edge so it outranks the dashed fine
   print sitting under it — this is the one thing on the page only they can do. */
.callout-ask { border-color: var(--red); }
.callout-tag {
  display: block; font-size: 0.72rem; font-weight: 700; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--red); margin-bottom: 0.5rem;
}

/* --- resources ---------------------------------------------------------- */
.res { display: grid; gap: 2rem; margin-top: 2.5rem; grid-template-columns: repeat(auto-fit, minmax(min(100%, 290px), 1fr)); }
.res h3 { font-size: 0.95rem; letter-spacing: 0.04em; text-transform: uppercase; color: var(--red); margin-bottom: 0.9rem; }
.res ul { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 0.65rem; }
.res li { font-size: 0.9rem; line-height: 1.45; }
.res .src { display: block; color: var(--muted); font-size: 0.8rem; }

/* --- footer ------------------------------------------------------------- */
.foot { border-top: 1px solid var(--border); padding-block: 3rem; background: var(--surface); }
/* Positive tracking, unlike the headings. The display face is condensed, so the
   -0.03em that tightens a large heading closes this line up to the point where
   the letterforms start reading as one shape at footer size. */
.foot-cta {
  font-family: var(--display); font-size: clamp(1.4rem, 1.1rem + 1.4vw, 2.1rem);
  font-weight: 400; letter-spacing: 0.03em; margin-bottom: 1rem;
}
.foot-cta span { color: var(--red); }
.foot p { font-size: 0.88rem; color: var(--muted); }
.foot-legal { margin-top: 1.5rem; padding-top: 1.5rem; border-top: 1px solid var(--border); font-size: 0.8rem; }

.vh {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* Video cameras are a separate claim from plate readers — see js/map.js. */
.map-legend .dot-video { background: #ffb020; }

/* --- Act Local ------------------------------------------------------------
   One section absorbing what used to be Renewals, The Letter and Officials.
   The per-jurisdiction detail lives on generated act/*.html pages, so the cards
   here are navigation, not content. */
.act-sub {
  margin-top: clamp(2rem, 4vw, 3rem);
  padding-top: clamp(1.75rem, 3.5vw, 2.5rem);
  border-top: 1px solid var(--border);
}
.act-sub > h3 { font-size: clamp(1.35rem, 1.1rem + 1vw, 1.9rem); }

/* Whole-card links. The card can't use <p>/<div> children because it's an
   anchor, so the inner spans need a layout mode that stacks them like blocks.
   That was `display: grid; align-content: start`, which packed the tracks at the
   top and left the slack outside them — so `margin-top: auto` on .ct-go had no
   free space to consume and the link would not pin. A flex column stacks the
   same way and does give the auto margin something to eat. */
a.ct-link {
  display: flex; flex-direction: column; gap: 0.6rem;
  text-decoration: none; color: var(--text);
}
a.ct-link:hover { border-color: var(--red); }
a.ct-link:hover .ct-go { color: var(--red-bright); }
.ct-name {
  font-family: var(--display); font-weight: 400;
  font-size: clamp(1.15rem, 1rem + 0.6vw, 1.5rem);
  letter-spacing: 0.01em; line-height: 1.1;
}
.ct-cams { font-size: 0.82rem; color: var(--muted); }
/* margin-top:auto pins this to the foot of the card, so the links line up across
   a row even when one jurisdiction's name wraps to two lines and its neighbors'
   do not. Needs a.ct-link to stay a flex column — see the note there. */
.ct-go {
  margin-top: auto; padding-top: 0.9rem; font-size: 0.8rem; font-weight: 700;
  color: var(--red-link); letter-spacing: 0.01em;
}
/* .ct-head is a flex row in the old markup; keep that working for anchors. */
a.ct-link .ct-head { display: flex; flex-wrap: wrap; gap: 0.5rem 0.8rem; align-items: baseline; }

/* --- jurisdiction marks ---------------------------------------------------
   Each jurisdiction's own boundary, dotted with its own cameras — generated by
   scripts/build-city-marks.mjs, which explains why these rather than the cities'
   official seals. Fixed width/height on the <img> so the grid doesn't reflow
   when the SVGs land. */
.ct-mark, .act-mark { flex: none; display: block; }

/* --- official seals ------------------------------------------------------
   When a jurisdiction supplies its own seal or logo it replaces the generated
   mark. Two problems come with that, and the chip solves both.

   Contrast: these arrive as dark ink on transparency — Holly Springs is dark
   green, Canton's wordmark is green and mid-grey — and would sink into a
   #191921 panel. A near-white chip restores the contrast the logos were drawn
   for, and the round-cornered seals that already carry white backgrounds blend
   into it invisibly.

   Ownership: the chip visually detaches the badge from our layout, which is the
   honest reading — these are the councils' marks, not the campaign's. The footer
   disclaims affiliation in words; this does it in pixels.

   Aspect ratios vary wildly (square seals vs 250x89 wordmarks), so the chip
   fixes the HEIGHT and lets width follow, capped so a wide wordmark can't push
   the name off the card. */
.ct-chip, .act-chip {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  background: #f7f7f9; border-radius: var(--radius);
}
.ct-chip { height: 72px; padding: 0.45rem 0.55rem; }
.act-chip { height: 120px; padding: 0.7rem 0.85rem; }
.ct-mark.is-seal, .act-mark.is-seal {
  height: 100%; width: auto; object-fit: contain;
}
.ct-chip .is-seal { max-width: 104px; }
.act-chip .is-seal { max-width: 180px; }

/* The round seals are square artwork and look weak floating in a padded box, so
   they run edge to edge — `sealFill` in data/contracts.json. The wide wordmarks
   keep their padding: Canton and Holly Springs are lettering, and lettering that
   touches the edge of its own badge reads as a mistake.

   THE CHIP IS A CIRCLE FOR THESE, NOT A SQUARE. The artwork already has fully
   transparent corners, so a square chip was painting white into them. Rounding it
   to 50% lets the corners show the panel behind, which is the point.

   The ring is not decoration either. These seals have dark rims — measured
   against --panel they run 1.03:1 (Waleska) to 1.42:1 (Nelson), far under the
   3:1 needed for a shape to be discernible — so without it the discs bleed into
   the panel and look bitten. It is drawn with box-shadow rather than padding or
   a border so it sits OUTSIDE the box: the seal still fills its container
   exactly, and the ring costs no layout space. */
.ct-chip.is-fill, .act-chip.is-fill {
  padding: 0; border-radius: 50%; background: none;
  box-shadow: 0 0 0 2px #f7f7f9;
}
.ct-chip.is-fill { width: 72px; }
.act-chip.is-fill { width: 120px; }
.is-fill .is-seal { width: 100%; height: 100%; max-width: none; }
.ct-top {
  display: flex; gap: 0.9rem; align-items: center;
}
/* Beside a 72px mark the name and status read better stacked than baselined. */
.ct-top .ct-head { flex-direction: column; align-items: flex-start; gap: 0.45rem; }
a.ct-link .ct-deadline { display: flex; flex-direction: column; gap: 0.15rem; }

/* --- generated jurisdiction pages ---------------------------------------- */
.act-h1 {
  font-size: clamp(2rem, 1.4rem + 2.6vw, 3.4rem);
  margin-bottom: 0.3em;
}
.act-h1 + .lede { margin-bottom: 1.75rem; }

/* Mark beside the title, wrapping above it once the row gets tight. The text
   column needs a min-width or the flex item refuses to shrink and the mark gets
   pushed off on narrow screens instead of wrapping. */
.act-head { display: flex; gap: clamp(1rem, 3vw, 1.9rem); align-items: flex-start; flex-wrap: wrap; }
.act-head > div { flex: 1 1 18rem; min-width: 0; }
.act-head .eyebrow { margin-top: 0.35rem; }

/* --- section headers -----------------------------------------------------
   Plain block flow: eyebrow, heading, then the text underneath, like any
   normal document. This was briefly a two-column grid with the heading left and
   the lede right — it filled the row but read as two disconnected blocks, and
   left a tall gap under short headings. Reverted deliberately; don't reintroduce.

   The wrapper stays purely as a spacing hook for the intro group. */
.sec-head { margin-bottom: clamp(1.5rem, 3vw, 2.25rem); }
.sec-head h2 { margin-bottom: 0.5rem; }
.sec-head .lede + .lede { margin-top: -0.25rem; }

/* Standalone notes under the map and elsewhere have the full container to use. */
.map-note { max-width: var(--measure-wide); }

/* --- hero background video ------------------------------------------------
   Everything here is gated on `.hero:has(.hero-video)`, so the hero renders
   exactly as it does today until the <video> element is uncommented. No file,
   no request, no visual change.

   THE CONTRAST PROBLEM, and why it is solved this way.

   Over an arbitrary video frame, text contrast cannot be guaranteed by colour
   alone. Assuming the worst case — a pure white frame — a flat black scrim would
   have to reach 85% opacity before --red cleared 4.5:1, by which point the video
   is effectively invisible.

   The current footage is a near-black map — measured mean luminance 0.009, with
   a brightest pixel of 0.139 in the region the text occupies. So it needs almost
   no darkening: brightness(0.9) keeps the red dots legible where an aggressive
   filter would have erased the only thing worth seeing.

   The scrim is still a LEFT-WEIGHTED GRADIENT rather than a flat wash, because
   that is what lets the animation stay visible on the right while the text sits
   on a near-solid ground. Verified against the actual brightest pixel in the
   footage, with brightness(0.9) applied:

     At the default --hero-scrim of 0.92, against this clip's brightest pixel:
       text zone   --text 16.6:1   --red 5.6:1   --muted 7.8:1
       right/logo  --text  6.6:1                 <- no text here

   The dark zone holds to 62% of the VIEWPORT, not of the text column. That
   distinction caused a real bug: the hero text lives inside a centred 1160px
   wrap, so at 1000-1200px wide its right edge reaches 62% of the viewport, and
   an earlier 34% stop left the headline sitting in the light zone where red only
   managed 2.4:1.

   All hero text must stay left of 62%. If you swap the footage for something
   brighter, re-measure before shipping — these numbers are specific to this
   clip, not a general guarantee. */

.hero { position: relative; overflow: hidden; }
.hero > .wrap { position: relative; z-index: 2; }

.hero-video {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  z-index: 0;
  /* Zoom then offset. Doing it with a transform rather than an over-wide element
     keeps x and y independent of object-fit, so the two knobs behave the way you
     would expect: percentages are of the hero's own box, and the zoom provides
     the slack that stops an offset exposing a hard edge. */
  transform: translate(var(--hero-video-x), var(--hero-video-y)) scale(var(--hero-video-zoom));
  filter: brightness(var(--hero-video-brightness)) saturate(0.95) contrast(1.05);
  pointer-events: none;
}

/* The scrim. Only exists when a video does. */
.hero:has(.hero-video)::before {
  content: '';
  position: absolute; inset: 0; z-index: 1;
  pointer-events: none;
  background:
    /* Holds --hero-scrim across the text, then falls away proportionally so one
       variable controls the whole ramp. */
    linear-gradient(to right,
      rgb(8 8 10 / var(--hero-scrim)) 0%,
      rgb(8 8 10 / var(--hero-scrim)) 62%,
      rgb(8 8 10 / calc(var(--hero-scrim) * 0.75)) 78%,
      rgb(8 8 10 / calc(var(--hero-scrim) * 0.39)) 100%),
    /* Feathers the top and bottom edges into the surrounding page. Scaled off the
       same variable so lowering --hero-scrim lightens the whole thing coherently.

       Note these two gradients composite: 1-(1-a)(1-b). At mid-height the
       vertical layer contributes ~0.27, so the effective darkness over the text
       is slightly ABOVE --hero-scrim, never below. That errs safe — the measured
       minimum of 0.90 is therefore conservative. */
    linear-gradient(to bottom,
      rgb(8 8 10 / calc(var(--hero-scrim) * 0.92)) 0%,
      rgb(8 8 10 / calc(var(--hero-scrim) * 0.29)) 30%,
      rgb(8 8 10 / calc(var(--hero-scrim) * 0.29)) 70%,
      rgb(8 8 10 / calc(var(--hero-scrim) * 0.98)) 100%);
}

/* On a narrow screen the layout is single-column, so there is no clear right
   side to reveal — the gradient becomes a vertical one instead. */
@media (max-width: 899px) {
  .hero:has(.hero-video)::before {
    background: linear-gradient(to bottom,
      rgb(8 8 10 / calc(var(--hero-scrim) * 0.97)) 0%,
      rgb(8 8 10 / var(--hero-scrim)) 45%,
      rgb(8 8 10 / calc(var(--hero-scrim) * 1.04)) 100%);
  }
}

/* --muted only reaches 4.5:1 in the text zone by a small margin, and the
   subhead is the longest run of small text over the video. Promote it. */
.hero:has(.hero-video) .hero-sub { color: var(--text); }
.hero:has(.hero-video) .art-note { color: var(--text); opacity: 0.85; }

/* Narrow screens get the still, not the motion — but they DO get the still.

   ⚠️ Two corrections to what this rule used to say. It claimed to avoid shipping
   "megabytes to phones": assets/hero.mp4 is 410 KB, so that was never the saving it
   sounded like, and `display: none` does not reliably stop a browser honouring
   `preload` anyway — the file gets fetched and then hidden. And it claimed "the
   poster frame carries the image", which `display: none` made false: it hides the
   poster along with the video, so a portrait phone got a flat dark panel and no
   picture at all. That is the bug reported on 3 August — the video "appears" in
   landscape simply because 844px clears this 699px breakpoint.
   
   So: paint the poster on the hero itself, then hide the element. Same composition,
   no motion, no wasted decode. Keep the position in sync with --hero-video-x if the
   framing is ever retuned. */
@media (max-width: 699px) {
  .hero:has(.hero-video) {
    /* ⚠️ The flat 10% black layer is not decoration — it stands in for the
       `filter: brightness(0.9)` that .hero-video carries. Every contrast figure
       measured for this hero assumed the footage was darkened by that filter, and a
       background-image cannot be filtered without filtering the text on top of it.
       A 10% black wash over the poster is the same darkening by another route, so
       the measured ratios still hold. Change both together or neither. */
    background-image:
      linear-gradient(rgb(8 8 10 / 0.1), rgb(8 8 10 / 0.1)),
      url('assets/hero-poster.jpg');
    background-size: cover;
    background-position: 62% center;
    background-repeat: no-repeat;
  }
  .hero-video { display: none; }
}
/* Reduce Motion: drop the movement, KEEP the image. `display: none` used to remove
   the poster along with the video, leaving a flat panel — a reader who asked for
   less motion had asked for that, not for less content. Pausing via the attribute
   is not possible from CSS, so the element stays and its poster frame shows: same
   composition, no movement. */
@media (prefers-reduced-motion: reduce) {
  .hero-video { animation: none; }
}

/* Credit for third-party footage. Small, but present — the underlying camera data
   is ODbL and the visualisation is someone else's work. */
.hero-credit {
  margin: 0.5rem auto 0; max-width: 34ch;
  font-size: 0.66rem; line-height: 1.45; color: var(--muted);
  text-align: center; letter-spacing: 0.01em;
}
.hero-credit [data-credit-todo] { opacity: 0.75; }

/* The questions that follow the deputies lede. The first one carries the section,
   so it is set larger than the paragraphs that answer it. */
.dep-questions { margin-bottom: 0; }
.dep-questions p { color: var(--muted); max-width: var(--measure); }
.dep-ask {
  font-size: clamp(1.05rem, 0.98rem + 0.45vw, 1.3rem);
  line-height: 1.5;
  color: var(--text) !important;
}
.dep-ask strong { color: var(--red); }

/* --- deputies: photo sidebar ---------------------------------------------
   The roster is a sidebar now, not a row under a heading. It sticks while the
   argument scrolls past on wide screens — the faces are the evidence for the
   text beside them, so keeping them in view is the point. */
.dep-side { display: flex; flex-direction: column; gap: 0.75rem; }

@media (min-width: 860px) {
  .dep-side { position: sticky; top: 5.5rem; }
  /* Slightly larger here than in the old inline row: this is the column's
     whole job, so the faces can carry more weight. */
  .dep-roster img { width: 68px; height: 85px; }
}

.dep-main > .dep-lede { margin-top: 0; }
.dep-main .card-sources { margin-top: 1.5rem; padding-top: 1.1rem; border-top: 1px solid var(--border); }
