/* Jasper Watch design system.
   Palette: warm East Texas earth — sand ground, espresso ink, pine green
   chrome, one loud burnt orange reserved for the primary action.
   Tier colors live ONLY on the map/incident surfaces and are always paired
   with a ring shape, so credibility never rides on hue alone. */

@font-face {
  font-family: 'Archivo'; font-style: normal; font-weight: 500 800; font-display: swap;
  src: url('/assets/fonts/archivo-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Archivo'; font-style: normal; font-weight: 500 800; font-display: swap;
  src: url('/assets/fonts/archivo-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
  font-family: 'Public Sans'; font-style: normal; font-weight: 400 700; font-display: swap;
  src: url('/assets/fonts/publicsans-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Public Sans'; font-style: normal; font-weight: 400 700; font-display: swap;
  src: url('/assets/fonts/publicsans-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

:root {
  --paper: #f7f0e4;
  --card: #fffbf4;
  --sunk: #f5ecdf;
  --ink: #2a1d16;
  --muted: #6f5b4c;
  --line: #e0d0b8;
  --line-strong: #c9b394;
  --brand: #1c5a44;
  --brand-ink: #133f30;
  --cta: #cf5014;
  --cta-hover: #a83d0b;
  --on-cta: #fff8f0;
  --t-reported: #b07d2b;
  --t-corroborated: #c0451c;
  --t-verified: #1f6b4f;
  --t-reported-bg: #f6e9d0;
  --t-corroborated-bg: #f7e0d6;
  --t-verified-bg: #d9ebe1;
  --danger: #a3301f;
  /* The nav underline on Register to Vote. Points at --cta rather than holding
     its own value, which puts it in the same family as the other things that
     ask the reader to act rather than to read — the home page's report button
     and the Know Your Rights badge. (The header's own Report button is the
     green base .btn, not this orange; only .btn.cta is.) Red was the first
     attempt and carried a party meaning nobody wanted on a site whose candidate
     guide says it endorses nobody. var() resolves where it is used, so both
     themes follow --cta with no second declaration. */
  --vote-mark: var(--cta);
  /* The ALPR layer. Purple was its colour when the toggle was a chip above the
     map; moving it onto the map lost that, and this puts it back. Deliberately
     outside the warm palette — it is somebody else's hardware on our streets,
     not part of this site's own vocabulary. */
  --flock: #6d28d9;
  /* Jurisdiction, one hue each. Low saturation on purpose: the status flag on
     the same row is the loud thing, and these have to sit beside it without
     turning the summary into a paint chart. Not the tier colours, not --flock,
     not the party reds and blues — all four already mean something else. */
  /* Status. Contested is the loud one on purpose: it is the only row where a
     vote changes an outcome. Unopposed stays grey so it never competes. */
  --f-hot-bg: #8f1d1d; --f-hot: #ffecec;
  --f-cold-bg: #e6e0d6; --f-cold: #6b6259;
  /* Jurisdiction: one hue each, dark for the whole jurisdiction and light for a
     subdivision of it. Countywide vs Precinct 2 were previously the same green
     and could not be told apart at a glance. */
  --j-county-bg: #e7e2f7; --j-county: #4a3a80;
  --j-county-all-bg: #4c3a86; --j-county-all: #f3efff;
  --j-city-bg: #dde8f6; --j-city: #26537f;
  --j-city-all-bg: #1f4a7d; --j-city-all: #eef5fd;
  --j-state-bg: #f7e8cd; --j-state: #7a5210;
  --j-state-all-bg: #9a6410; --j-state-all: #fff6e8;
  --shadow: 0 1px 2px rgba(42, 29, 22, .06), 0 4px 14px rgba(42, 29, 22, .05);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --paper: #221b17;
    --card: #2e2620;
    --sunk: #392f28;
    --ink: #f2e8db;
    --muted: #b39c85;
    --line: #473a30;
    --line-strong: #64513f;
    --brand: #6cc39c;
    --brand-ink: #a3ddc1;
    --cta: #f5813f;
    --cta-hover: #ff9a5e;
    --on-cta: #201007;
    --t-reported: #dda94a;
    --t-corroborated: #e5764e;
    --t-verified: #5fbd90;
    --t-reported-bg: #3a2c14;
    --t-corroborated-bg: #3d2117;
    --t-verified-bg: #163227;
    --danger: #e78573; --flock: #8b5cf6;
    --f-hot-bg: #b33636; --f-hot: #ffecec;
    --f-cold-bg: #3a332b; --f-cold: #bdb1a2;
    --j-county-bg: #322a4d; --j-county: #c3b6ee;
    --j-county-all-bg: #6a5aae; --j-county-all: #f2eeff;
    --j-city-bg: #22314a; --j-city: #a9c6e8;
    --j-city-all-bg: #3a72b5; --j-city-all: #eef5fd;
    --j-state-bg: #40331d; --j-state: #dcb877;
    --j-state-all-bg: #b8801f; --j-state-all: #fff6e8;
    --flock: #8b5cf6;
    --flock: #8b5cf6;
    --shadow: 0 1px 2px rgba(0, 0, 0, .3), 0 4px 14px rgba(0, 0, 0, .25);
  }
}
:root[data-theme="dark"] {
  --paper: #221b17; --card: #2e2620; --sunk: #392f28; --ink: #f2e8db;
  --muted: #b39c85; --line: #473a30; --line-strong: #64513f;
  --brand: #6cc39c; --brand-ink: #a3ddc1; --cta: #f5813f; --cta-hover: #ff9a5e;
  --on-cta: #201007;
  --t-reported: #dda94a; --t-corroborated: #e5764e; --t-verified: #5fbd90;
  --t-reported-bg: #3a2c14; --t-corroborated-bg: #3d2117; --t-verified-bg: #163227;
  --danger: #e78573; --flock: #8b5cf6;
  --f-hot-bg: #b33636; --f-hot: #ffecec;
  --f-cold-bg: #3a332b; --f-cold: #bdb1a2;
  --j-county-bg: #322a4d; --j-county: #c3b6ee;
  --j-county-all-bg: #6a5aae; --j-county-all: #f2eeff;
  --j-city-bg: #22314a; --j-city: #a9c6e8;
  --j-city-all-bg: #3a72b5; --j-city-all: #eef5fd;
  --j-state-bg: #40331d; --j-state: #dcb877;
  --j-state-all-bg: #b8801f; --j-state-all: #fff6e8;
  --shadow: 0 1px 2px rgba(0, 0, 0, .3), 0 4px 14px rgba(0, 0, 0, .25);
}

* { box-sizing: border-box; }
/* Present to assistive technology, absent on screen. Used where a visual
   element carries information that its own markup cannot state — a calendar
   face, an icon standing in for a date. */
.sr-only {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}
/* Explicit display rules (e.g. .legend-row{display:flex}) beat the UA sheet's
   [hidden]{display:none}, so hidden elements would still render. */
[hidden] { display: none !important; }
html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font: 400 17px/1.62 "Public Sans", system-ui, -apple-system, sans-serif;
  -webkit-font-smoothing: antialiased;
}
main { max-width: 900px; margin: 0 auto; padding: 28px 18px 80px; }
main.narrow { max-width: 690px; }

h1, h2, h3, .display {
  font-family: "Archivo", "Public Sans", system-ui, sans-serif;
  color: var(--ink);
  letter-spacing: -0.02em;
  text-wrap: balance;
  line-height: 1.12;
}
h1 { font-size: clamp(1.9rem, 5.2vw, 2.6rem); font-weight: 800; margin: 0.35em 0; }
h2 {
  font-size: clamp(1.3rem, 3vw, 1.6rem); font-weight: 700;
  margin: 1.9em 0 0.5em; padding-bottom: 0.3em;
  border-bottom: 2px solid var(--line-strong);
}
h3 { font-size: 1.1rem; font-weight: 700; margin: 1.5em 0 0.35em; }
/* The opening claim of a long-form page. Bigger than body copy and set in the
   heading face, but not a heading — it is the page's first sentence, and making
   it an h2 would put a phantom section in the outline. */
.page-lede {
  font-family: "Archivo", sans-serif; font-weight: 600;
  font-size: clamp(1.1rem, 2.6vw, 1.3rem); line-height: 1.35;
  color: var(--brand-ink); margin: 0.2em 0 0.7em; max-width: 46ch;
}
p { margin: 0.65em 0; }
a { color: var(--brand-ink); text-underline-offset: 2px; }
a:hover { color: var(--cta); }
.muted { color: var(--muted); }
.small { font-size: 0.88rem; }
.eyebrow {
  font-family: "Archivo", sans-serif; font-size: 0.74rem; font-weight: 700;
  letter-spacing: 0.13em; text-transform: uppercase; color: var(--muted);
}

/* ---------- header ---------- */
header.site { border-bottom: 2px solid var(--line-strong); background: var(--card); }
.site-inner {
  max-width: 900px; margin: 0 auto; padding: 11px 18px;
  display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
}
.brand { display: flex; align-items: center; gap: 10px; text-decoration: none; color: var(--brand-ink); }
.brand svg { width: 27px; height: 27px; flex: none; color: var(--brand); }
.brand span {
  font-family: "Archivo", sans-serif; font-weight: 800; font-size: 1.2rem;
  letter-spacing: -0.03em; color: var(--ink);
}
nav.site-nav { display: flex; gap: 2px 16px; flex-wrap: wrap; margin-left: auto; align-items: center; }
nav.site-nav a {
  text-decoration: none; color: var(--ink); font-size: 0.93rem; font-weight: 600;
  padding: 5px 1px;
}
/* Hover changes the colour and nothing else. It used to draw a bottom border,
   which is the same signal the current page uses — so hovering Resources while
   reading What is this? underlined both of them and neither meant anything. */
nav.site-nav a:hover { color: var(--brand-ink); }
/* The current page is marked with the same mechanism, thickness and offset as
   Register to Vote's permanent orange one, so the two sit on one line rather
   than at two different heights drawn by two different properties. */
nav.site-nav a[aria-current="page"] {
  color: var(--brand-ink);
  text-decoration: underline; text-decoration-color: var(--brand);
  text-decoration-thickness: 2px; text-underline-offset: 5px;
}
/* Register to Vote is underlined on every page, so when it is also the current
   page it keeps its own colour rather than stacking a second one. */
nav.site-nav a.nav-vote[aria-current="page"] { text-decoration-color: var(--vote-mark); }
/* `nav.site-nav a` outranks a bare `.btn`, so the nav link colour was silently
   repainting the Report button's label in near-black ink on dark green. Match
   the selector weight rather than reaching for !important. */
nav.site-nav a.btn { color: var(--on-cta); text-decoration: none; }
nav.site-nav a.btn:hover { color: var(--on-cta); }
.lang-toggle {
  border: 1.5px solid var(--line-strong); background: var(--paper); color: var(--ink);
  border-radius: 99px; padding: 5px 14px; cursor: pointer;
  font: 700 0.93rem "Archivo", sans-serif; letter-spacing: 0.02em;
}
.lang-toggle:hover { border-color: var(--brand); color: var(--brand-ink); }
.theme-toggle {
  border: 1.5px solid var(--line-strong); background: var(--paper); color: var(--ink);
  border-radius: 99px; width: 40px; height: 32px; cursor: pointer;
  font-size: 0.95rem; line-height: 1; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
}
/* The glyph is drawn at 24 units square; sized to 17 inside a 40x32 pill it has
   real space around it rather than touching the border on every side. */
.theme-toggle svg { width: 17px; height: 17px; display: block; }
.theme-toggle:hover { border-color: var(--brand); color: var(--brand-ink); }

/* Dark mode: full-brightness OSM tiles glare against a dark page. Soften them
   rather than inverting, which would make the map unreadable. */
:root[data-theme="dark"] .leaflet-tile { filter: brightness(.82) saturate(.8) contrast(1.06); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .leaflet-tile { filter: brightness(.82) saturate(.8) contrast(1.06); }
}

@media (max-width: 560px) {
  .site-inner { padding: 9px 14px; gap: 9px; }
  nav.site-nav { margin-left: 0; width: 100%; gap: 2px 13px; }
  nav.site-nav a { font-size: 0.87rem; }
  main { padding: 20px 15px 64px; }
}

/* ---------- buttons ---------- */
.btn {
  display: inline-block; border: 0; border-radius: 6px; cursor: pointer;
  font: 700 1rem "Archivo", sans-serif; letter-spacing: -0.01em;
  padding: 12px 24px; text-decoration: none;
  background: var(--brand); color: var(--on-cta);
  transition: transform .12s ease, background-color .12s ease;
}
.btn:hover { background: var(--brand-ink); color: var(--on-cta); transform: translateY(-1px); }
.btn.cta {
  background: var(--cta); font-size: 1.12rem; padding: 16px 30px;
  box-shadow: 0 2px 0 var(--cta-hover);
}
.btn.cta:hover { background: var(--cta-hover); box-shadow: 0 1px 0 var(--cta-hover); }
.btn.secondary {
  background: transparent; color: var(--ink); border: 0;
  box-shadow: inset 0 0 0 2px var(--line-strong);
}
.btn.secondary:hover {
  background: transparent; color: var(--brand-ink);
  box-shadow: inset 0 0 0 2px var(--brand);
}
/* Outline in --cta rather than a filled orange button: this sits beside the
   state portal button, and two solid buttons side by side rank neither. The
   border and the label carry the emphasis, the fill arrives on hover. */
.btn.secondary.accent { color: var(--cta); box-shadow: inset 0 0 0 2px var(--cta); }
.btn.secondary.accent:hover {
  background: var(--cta); color: var(--on-cta); box-shadow: inset 0 0 0 2px var(--cta);
}
.btn.small-btn { font-size: 0.86rem; padding: 7px 15px; }
.btn:disabled { opacity: .5; cursor: default; transform: none; }
/* Two buttons that belong together get one width between them, so neither
   outranks the other by having a longer label. */
.btn-pair { display: flex; flex-wrap: wrap; gap: 10px; align-items: stretch; }
.btn-pair > .btn { flex: 1 1 0; min-width: 12ch; text-align: center; display: grid; place-items: center; }
@media (max-width: 560px) { .btn-pair > .btn { flex: 1 1 100%; } }
@media (prefers-reduced-motion: reduce) { .btn, .btn:hover { transition: none; transform: none; } }

/* ---------- forms ---------- */
label { display: block; font-family: "Archivo", sans-serif; font-weight: 700; font-size: 0.94rem; margin: 18px 0 5px; }
input[type="text"], input[type="datetime-local"], input[type="number"], textarea, select {
  width: 100%; padding: 10px 12px; border: 1.5px solid var(--line-strong); border-radius: 6px;
  background: var(--card); color: var(--ink); font: 400 1rem "Public Sans", sans-serif;
}
input:focus, textarea:focus, select:focus { border-color: var(--brand); }
textarea { min-height: 130px; resize: vertical; }
fieldset { border: 1.5px solid var(--line); border-radius: 8px; margin: 18px 0; padding: 12px 16px; background: var(--card); }
legend { font-family: "Archivo", sans-serif; font-weight: 700; font-size: 0.94rem; padding: 0 7px; }
.radio-row { display: flex; gap: 9px; align-items: flex-start; margin: 10px 0; }
.radio-row input { margin-top: 5px; }
.hint { color: var(--muted); font-size: 0.87rem; margin-top: 4px; }
:focus-visible { outline: 3px solid var(--cta); outline-offset: 2px; }

label.inline { display: inline; margin: 0; }

/* ---------- gazetteer autocomplete ---------- */
.place-search { position: relative; display: flex; gap: 8px; align-items: stretch; flex-wrap: wrap; }
.place-search input { flex: 1; min-width: 200px; }
.place-search .chip { white-space: nowrap; }
#place-results {
  position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 900;
  list-style: none; margin: 0; padding: 4px; max-height: 260px; overflow-y: auto;
  background: var(--card); border: 1.5px solid var(--line-strong); border-radius: 8px;
  box-shadow: var(--shadow);
}
#place-results li {
  display: flex; justify-content: space-between; align-items: baseline; gap: 12px;
  padding: 8px 10px; border-radius: 5px; cursor: pointer;
}
#place-results li.active, #place-results li:hover { background: var(--sunk); }
.place-name { font-weight: 600; }
.place-kind {
  font-family: "Archivo", sans-serif; font-size: 0.7rem; font-weight: 700;
  letter-spacing: 0.07em; text-transform: uppercase; color: var(--muted); flex: none;
}
#pick-map { height: 320px; border: 1.5px solid var(--line-strong); border-radius: 8px; margin-top: 10px; }

/* The picker opens with no pin on purpose: a pin already sitting somewhere
   counts as a chosen location, and someone could submit a report pinned to a
   block they never meant. So the empty map has to say what to do instead.
   pointer-events: none, or the prompt would swallow the very tap it asks for. */
.pick-wrap { position: relative; }
.map-prompt {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  z-index: 700; pointer-events: none; margin: 0;
  background: var(--card); color: var(--ink);
  border: 1.5px solid var(--line-strong); border-radius: 99px;
  padding: 9px 18px; box-shadow: var(--shadow);
  font: 600 0.92rem "Public Sans", sans-serif; text-align: center;
}


/* ---------- structured chip sets ---------- */
.chipset { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0 4px; }
.chipset .chip { font-size: 0.9rem; padding: 7px 15px; }

/* ---------- media ---------- */
.policy-list { margin: 8px 0 12px; padding-left: 20px; font-size: 0.88rem; color: var(--muted); }
.policy-list li { margin: 5px 0; }
input[type="file"] { font: 400 0.92rem "Public Sans", sans-serif; color: var(--ink); max-width: 100%; }
.media-list { list-style: none; margin: 12px 0 0; padding: 0; }
.media-list li {
  display: flex; gap: 10px; align-items: center; flex-wrap: wrap;
  padding: 8px 10px; border: 1.5px solid var(--line); border-radius: 6px; margin-bottom: 7px;
  background: var(--paper);
}
.media-kind {
  font-family: "Archivo", sans-serif; font-size: 0.68rem; font-weight: 800;
  letter-spacing: 0.07em; text-transform: uppercase; padding: 3px 9px; border-radius: 99px;
  background: var(--sunk); color: var(--muted);
}
.media-photo { background: var(--t-verified-bg); color: var(--t-verified); }
.media-video { background: var(--t-corroborated-bg); color: var(--t-corroborated); }
.media-audio { background: var(--t-reported-bg); color: var(--t-reported); }
.media-name { flex: 1; min-width: 120px; font-size: 0.9rem; word-break: break-all; }
.media-state { color: var(--muted); }
.linkish {
  border: 0; background: none; color: var(--brand-ink); cursor: pointer;
  font: 600 0.86rem "Public Sans", sans-serif; text-decoration: underline; padding: 2px 4px;
}

/* ---------- cards, pills, notices ---------- */
.card {
  background: var(--card); border: 1.5px solid var(--line); border-radius: 8px;
  padding: 18px 20px; margin: 16px 0; box-shadow: var(--shadow);
}
.pill {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: "Archivo", sans-serif; font-size: 0.72rem; font-weight: 800;
  letter-spacing: 0.06em; text-transform: uppercase; padding: 3px 11px; border-radius: 99px;
}
.pill::before {
  content: ''; width: 9px; height: 9px; border-radius: 50%;
  border: 2.5px solid currentColor; box-sizing: border-box;
}
.pill.reported { background: var(--t-reported-bg); color: var(--t-reported); }
.pill.corroborated { background: var(--t-corroborated-bg); color: var(--t-corroborated); }
.pill.corroborated::before { box-shadow: inset 0 0 0 1.5px var(--t-corroborated-bg), inset 0 0 0 3px currentColor; }
.pill.verified { background: var(--t-verified-bg); color: var(--t-verified); }
.pill.verified::before { background: currentColor; }
.notice {
  border-left: 4px solid var(--brand); background: var(--card);
  padding: 13px 18px; border-radius: 0 8px 8px 0; margin: 16px 0; font-size: 0.96rem;
  box-shadow: var(--shadow);
}
.notice.warn { border-left-color: var(--cta); }

/* ---------- map pins ---------- */
/* Simple teardrop, colored by tier. The interior glyph is the second channel:
   the age fade drains color to gray by design, so color alone cannot carry
   credibility — and never could for colorblind readers. */
.pin2 {
  position: relative; display: block; width: 22px; height: 22px; box-sizing: border-box;
  background: var(--c); border: 2.5px solid var(--paper);
  border-radius: 50% 50% 50% 0; transform: rotate(-45deg);
  box-shadow: 0 2px 4px rgba(0, 0, 0, .35);
}
.pin2 > i {
  position: absolute; inset: 0; display: grid; place-items: center;
  transform: rotate(45deg); color: var(--paper); font-style: normal;
  font-size: 10px; font-weight: 800; line-height: 1;
}
.pin2.sm { width: 17px; height: 17px; border-width: 2px; }
.pin2.sm > i { font-size: 8px; }

/* Report picker pin. `pick-pin` was referenced by JW.pickerIcon() and styled
   nowhere, so the marker was an empty zero-size span: the pin existed, held the
   coordinate and blocked submission when unset, but nobody could see it.
   Deliberately ink rather than any map-pin colour — those encode standing on the
   public map and must not also mean "the spot you picked". */
.pick-pin {
  position: relative; display: block; width: 26px; height: 26px; box-sizing: border-box;
  background: var(--ink); border: 3px solid var(--paper);
  border-radius: 50% 50% 50% 0; transform: rotate(-45deg);
  box-shadow: 0 2px 6px rgba(0, 0, 0, .4); cursor: grab;
}
.pick-pin::before {
  content: ''; position: absolute; inset: 0; margin: auto;
  width: 7px; height: 7px; border-radius: 50%; background: var(--paper);
}
/* Invisible touch target: keeps the pin visually 26px while giving a thumb
   something nearer 44px to grab. Phone-first, all ages. */
.pick-pin::after { content: ''; position: absolute; inset: -9px; border-radius: 50%; }
.leaflet-marker-draggable:active .pick-pin { cursor: grabbing; }

.legend-row { display: flex; flex-wrap: wrap; gap: 10px 22px; align-items: center; margin: 12px 0; font-size: 0.88rem; }
.legend-row .swatch { display: inline-flex; align-items: center; gap: 9px; font-weight: 600; }

/* ---------- map page ---------- */
#map { height: 62vh; min-height: 380px; border: 1.5px solid var(--line-strong); border-radius: 8px; }
.filters { display: flex; flex-wrap: wrap; gap: 9px 10px; align-items: center; margin: 14px 0; }
.chip {
  border: 1.5px solid var(--line-strong); background: var(--card); color: var(--ink);
  border-radius: 99px; padding: 5px 14px; cursor: pointer;
  font: 600 0.87rem "Public Sans", sans-serif;
}
.chip:hover { border-color: var(--brand); }
.chip[aria-pressed="true"] { background: var(--brand); color: var(--on-cta); border-color: var(--brand); }
.filter-label { font-family: "Archivo", sans-serif; font-weight: 700; font-size: 0.78rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }
.filters-wrap { margin: 4px 0 10px; }
.chip.disclosure::after { content: ' ▾'; }
.chip.disclosure[aria-expanded="true"]::after { content: ' ▴'; }
.filter-panel {
  border: 1.5px solid var(--line); border-radius: 8px; padding: 6px 14px 14px;
  margin: 10px 0; background: var(--card);
}
.empty-note {
  border: 1.5px dashed var(--line-strong); border-radius: 8px;
  padding: 14px 18px; margin: 12px 0; color: var(--muted); font-size: 0.94rem;
}
.empty-note strong { color: var(--ink); }
.empty-card { border-style: dashed; text-align: center; }
.rights-line { display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap; margin-top: 18px; }
.rights-line > a { font-family: "Archivo", sans-serif; font-weight: 700; font-size: 1.02rem; }
/* Report was the smallest text in the header, on the one control that is a
   button. It matches the nav links now and sits on slightly more padding.
   Selector weight matters here: `.btn.small-btn` is two classes and would beat a
   bare `.nav-report`, which is the same trap already noted above for the link
   colour. Matched rather than reached for !important. */
nav.site-nav a.nav-report { padding: 8px 18px; font-size: 0.93rem; }

/* ---------- home ---------- */
/* position: relative so the rights badge can hang off the hero's bottom-right
   corner and overlap what follows. z-index on the badge, not here, so the hero
   itself does not start stacking above the map. */
.hero { padding: 18px 0 6px; position: relative; }

/* The town seal, enormous and nearly invisible, cropped so only the middle of
   the emblem shows. Texture rather than an image: it is the same mark the header
   draws, so the brand repeats without a second asset to keep in step. Its own
   clipping box, because the hero itself must not clip — the rights badge is
   meant to hang past it. */
.hero-seal {
  position: absolute; inset: 0; overflow: hidden;
  pointer-events: none; z-index: 0; color: var(--brand);
}
.hero-seal svg {
  position: absolute; left: -6%; top: -58%;
  width: 420px; height: 420px; opacity: .07;
}
.hero > *:not(.hero-seal):not(.kyr-badge) { position: relative; z-index: 1; }
@media (min-width: 1100px) { .hero-seal svg { width: 520px; height: 520px; top: -46%; } }
:root[data-theme="dark"] .hero-seal svg { opacity: .1; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .hero-seal svg { opacity: .1; }
}
.hero .beat {
  font-family: "Archivo", sans-serif; font-weight: 800; letter-spacing: -0.025em;
  font-size: clamp(1.55rem, 4.6vw, 2.35rem); line-height: 1.14; margin: 0 0 2px;
}
.hero .beat.lead { color: var(--ink); }
.hero .beat.turn { color: var(--muted); }
.hero .beat.act { color: var(--cta); }
.hero-actions {
  display: flex; flex-wrap: wrap; gap: 13px; align-items: stretch;
  margin: 26px 0 8px;
}
/* The two hero buttons were sized by their own text, so "Look up a report" came
   out visibly smaller than the CTA beside it and read as the lesser of the two.
   Two grid columns of minmax(0, 1fr) are exactly equal whatever the labels say;
   flex with a basis of 0 is not, because a track still refuses to shrink below
   its content's minimum and the longer label wins back the difference. The CTA
   leads on colour instead, which is the difference that was meant to be there. */
/* Sized to its own text so the label holds one line, rather than to a grid
   track that had been sharing the row with a second button that no longer
   exists. */
.hero-btn {
  min-height: 58px; display: inline-flex; align-items: center;
  justify-content: center; text-align: center;
  font-size: 1.06rem; padding-left: 26px;
}
@media (max-width: 560px) { .hero-btn { width: 100%; } }
/* On a phone the report button and the rights bar sit one above the other and
   are the same width, so centred text on one and left text on the other read as
   two different components. Both align left. */
@media (max-width: 1099px) {
  .hero-btn { justify-content: flex-start; text-align: left; padding-left: 20px; }
}
/* These pick which view you are looking at; the chips below filter what is in
   it. They were the same component, so the page offered five things that all
   looked equally like filters. A segmented pair reads as a switch instead. */
.home-panel-head {
  display: inline-flex; align-items: stretch; gap: 0; margin: 40px 0 12px;
  border: 2px solid var(--line-strong); border-radius: 10px; overflow: hidden;
}
.home-panel-head .chip {
  border: 0; border-radius: 0; margin: 0; padding: 9px 20px; font-size: 0.95rem;
}
.home-panel-head .chip + .chip { border-left: 2px solid var(--line-strong); }
.home-panel-head .chip[aria-pressed="true"] {
  background: var(--brand); color: var(--on-cta);
}
/* Looking up a receipt is a third way of reading the same record, so it sits
   with the other two rather than as an errand of its own in the hero. */
/* Wide enough for the fuller wording, the fuller wording is better: "recent" is
   real information about what the list shows, and the map tab reads as a
   sentence rather than a stub. Narrow, one line matters more than either. Both
   labels ship and the stylesheet picks, the same way the Report button does, so
   nothing relays out or refetches on resize. */
.tab-short { display: none; }
@media (max-width: 720px) {
  .tab-long { display: none; }
  .tab-short { display: inline; }
}
.view-search { display: inline-flex; align-items: center; gap: 7px; }
.view-search svg { width: 16px; height: 16px; flex: none; }

/* The lookup box takes the place the map and list occupy, rather than opening
   underneath something else and pushing the page around. */
.code-lookup {
  border: 1.5px solid var(--line-strong); border-left: 5px solid var(--brand);
  border-radius: 9px; background: var(--card); box-shadow: var(--shadow);
  padding: 18px 20px; margin: 0 0 8px; max-width: 34rem;
}
.code-lookup > label {
  display: block; font-family: "Archivo", sans-serif; font-weight: 700;
  margin-bottom: 8px;
}
.code-row { display: flex; gap: 10px; flex-wrap: wrap; }
.code-row input { flex: 1 1 12rem; min-width: 0; text-transform: uppercase; letter-spacing: .06em; }
@media (max-width: 560px) { .code-row .btn { width: 100%; } }
#home-map { height: 46vh; min-height: 320px; border: 1.5px solid var(--line-strong); border-radius: 8px; }

/* ---------- incident list ---------- */
.incident-row { display: flex; gap: 12px; align-items: baseline; flex-wrap: wrap; }
.incident-row strong { font-family: "Archivo", sans-serif; font-weight: 700; }
.incident-row time { color: var(--muted); font-size: 0.86rem; font-variant-numeric: tabular-nums; }
.vote-bar { display: flex; gap: 10px; align-items: center; margin-top: 12px; flex-wrap: wrap; }
.vote-bar button {
  border: 1.5px solid var(--line-strong); background: var(--card); color: var(--ink);
  border-radius: 6px; padding: 8px 15px; cursor: pointer;
  font: 600 0.92rem "Public Sans", sans-serif;
}
.vote-bar button:hover:not([disabled]) { border-color: var(--brand); }
.vote-bar button[disabled] { opacity: .55; cursor: default; }

/* ---------- published evidence ---------- */
.evidence {
  border: 1.5px solid var(--line); border-radius: 8px; padding: 14px 16px;
  margin: 18px 0; background: var(--card);
}
.evidence img {
  max-width: 100%; height: auto; border-radius: 6px; display: block;
  margin: 10px 0; border: 1px solid var(--line);
}

/* ---------- footer ---------- */
footer.site {
  border-top: 2px solid var(--line-strong); margin-top: 48px; padding: 24px 18px 44px;
  color: var(--muted); font-size: 0.87rem; background: var(--card);
}
/* Was unconditional, which quietly beat .footer-grid on specificity and left
   the footer as three stacked full-width blocks. Scoped so the footer's own
   layout wins, rather than deleted, in case another footer ever wants blocks. */
footer.site .site-inner:not(.footer-grid) { display: block; }

/* ---------- admin ---------- */
table.admin { border-collapse: collapse; width: 100%; font-size: 0.9rem; }
table.admin th {
  text-align: left; border-bottom: 2px solid var(--line-strong); padding: 7px 9px;
  font-family: "Archivo", sans-serif; font-size: 0.74rem; text-transform: uppercase;
  letter-spacing: 0.07em; color: var(--muted);
}
table.admin td { border-bottom: 1px solid var(--line); padding: 9px; vertical-align: top; }
.tablewrap { overflow-x: auto; }
.tagrow { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0; }
.tag {
  font-size: 0.76rem; font-weight: 600; padding: 3px 10px; border-radius: 99px;
  background: var(--sunk); color: var(--muted); border: 1px solid var(--line);
}
.media-admin { border: 1.5px dashed var(--line-strong); border-radius: 7px; padding: 10px 13px; margin: 12px 0; }
.media-admin-row { display: flex; gap: 11px; align-items: center; flex-wrap: wrap; margin-top: 8px; }

/* ---------- redaction tool ---------- */
.redact-overlay {
  position: fixed; inset: 0; z-index: 5000; background: rgba(20, 14, 10, .8);
  display: flex; align-items: center; justify-content: center; padding: 20px;
}
.redact-panel {
  background: var(--card); border-radius: 10px; padding: 20px 22px;
  max-width: min(980px, 96vw); max-height: 92vh; overflow-y: auto; box-shadow: var(--shadow);
}
.redact-panel h3 { margin-top: 0; }
.redact-stage { margin: 12px 0; }
.redact-canvas {
  max-width: 100%; max-height: 58vh; border: 1.5px solid var(--line-strong);
  border-radius: 6px; cursor: crosshair; touch-action: none; display: block;
}
.redact-video { max-width: 100%; max-height: 52vh; border-radius: 6px; display: block; }
.redact-tools { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-top: 12px; }

@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

/* ---------- ALPR camera layer ---------- */
.cam-dot {
  display: grid; place-items: center; width: 16px; height: 16px;
  color: #6d28d9; font-size: 13px; line-height: 1;
  text-shadow: 0 0 3px var(--paper), 0 0 3px var(--paper);
}
:root[data-theme="dark"] .cam-dot { color: #c4a5ff; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .cam-dot { color: #c4a5ff; }
}

/* ---------- resources ---------- */
/* ---------- the 911 line, at the top of Resources ---------- */
/* The only red on the site, and the only place a phone number is set at display
   size. Big enough to read at arm's length and to dial without reading the
   sentence under it. */
.call911 {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 14px;
  margin: 30px 0 8px; padding: 16px 20px; border-radius: 9px;
  background: var(--card); border: 1.5px solid var(--danger);
  border-left: 8px solid var(--danger); box-shadow: var(--shadow);
  text-decoration: none;
}
.call911:hover { border-color: var(--danger); background: var(--sunk); }
.call911-num {
  font-family: "Archivo", sans-serif; font-weight: 800; font-size: 2.1rem;
  line-height: 1.05; color: var(--danger); letter-spacing: -.01em;
}
.call911-when {
  font-size: 1.02rem; font-weight: 600; color: var(--ink); flex: 1 1 14ch;
}
.call911-note {
  flex: 1 1 100%; font-size: .88rem; color: var(--muted); margin-top: 2px;
}
@media (max-width: 560px) {
  .call911 { padding: 14px 16px; }
  .call911-num { font-size: 1.8rem; }
  .call911-when { font-size: .96rem; flex-basis: 100%; }
}

.res-card { border-left: 4px solid var(--brand); }
.res-card.urgent { border-left-color: var(--cta); }
.res-num {
  font-family: "Archivo", sans-serif; font-weight: 800;
  font-size: clamp(1.35rem, 4vw, 1.8rem); letter-spacing: -0.02em;
  color: var(--cta); text-decoration: none; display: inline-block; margin: 4px 0;
}
.res-num:hover { color: var(--cta-hover); }
/* A number inside a sentence rather than as the headline of a card. Bold and
   dialable, because on a phone every number on this site should be one tap and
   there is no way to tell by looking which ones were links. */
.tel {
  font-weight: 800; color: var(--cta); text-decoration: underline;
  text-decoration-thickness: 1.5px; text-underline-offset: 3px; white-space: nowrap;
}
.tel:hover { color: var(--cta-hover); }
.res-meta { font-size: 0.85rem; color: var(--muted); }

/* ---------- register & vote walkthrough ---------- */
.wizard { margin: 16px 0; }
.wiz-q { padding: 12px 0; border-bottom: 1px solid var(--line); }
.wiz-label { font-family: "Archivo", sans-serif; font-weight: 700; margin: 0 0 8px; }
.wiz-opts { display: flex; gap: 8px; flex-wrap: wrap; }
.wiz-out {
  margin: 16px 0; padding: 14px 18px; border-radius: 8px;
  background: var(--card); border: 1.5px solid var(--line);
}
.wiz-out.final { border-left: 4px solid var(--cta); }
.wiz-steps { padding-left: 22px; margin: 10px 0; }
.wiz-steps > li { margin: 12px 0; }
.wiz-note {
  background: var(--sunk); border-radius: 6px; padding: 10px 14px;
  font-size: 0.9rem; margin-top: 14px;
}
.addr-card {
  border: 1.5px solid var(--line-strong); border-radius: 6px;
  padding: 10px 14px; margin: 8px 0; font-size: 0.94rem; background: var(--paper);
}
.counter-line { color: var(--muted); font-size: 0.92rem; }
.counter-line strong {
  font-family: "Archivo", sans-serif; font-size: 1.25rem; color: var(--brand-ink);
}
.btn.cta-lite { background: var(--cta); }
.btn.cta-lite:hover { background: var(--cta-hover); }

/* ---------- know your rights: priority-ordered blocks ---------- */
/* Numbered because the order IS the advice: door first, silence second.
   The stripe colour encodes urgency, and the number carries it too so the
   ranking survives for anyone who cannot see the colour. */
/* Each right is a collapsed container in the same shape as every other page,
   so the page opens as a scannable list rather than five walls of text. One
   stripe colour for all five: the per-box tone stripe was reading as a ranking
   nobody asked for, and the orange on this page is spent on the offline notice,
   which is the one instruction that expires. */
.faq.kyr { border-left: 6px solid var(--brand); }
.faq.kyr > summary::after { color: var(--brand); }
.faq.kyr > summary:hover .faq-q { color: var(--brand-ink); }
.kyr-lead { font-size: 1.05rem; font-weight: 600; margin: 0 0 10px; }

/* Sources sit inside the answer they support rather than in one footnote list
   at the foot of the page. A citation you have to scroll away to read is a
   citation nobody checks, and repeating one across two boxes costs nothing. */
.kyr-src {
  margin: 16px 0 0; padding: 12px 14px;
  background: var(--sunk); border-radius: 7px;
}
.kyr-src-h {
  margin: 0 0 6px; font-family: "Archivo", sans-serif; font-weight: 700;
  font-size: .78rem; letter-spacing: .09em; text-transform: uppercase;
  color: var(--muted);
}
.kyr-src ul { margin: 0; }
.kyr-src li { font-size: .88rem; color: var(--muted); margin: 6px 0; }
.kyr-src li::before { opacity: .5; }
.sources { font-size: 0.88rem; color: var(--muted); padding-left: 20px; }
.sources li { margin: 8px 0; }
.sources li:target { background: var(--sunk); border-radius: 4px; }

/* ---------- candidate guide ---------- */
/* Each office is a collapsed box in the same shape as every other page on the
   site. The summary carries a preview so the page still answers "who is running
   for what" while every box is shut. */
.race { margin: 14px 0; }
.race > summary { align-items: flex-start; row-gap: 7px; padding: 12px 18px; }
.race > summary > .faq-q { flex: 0 1 auto; min-width: 0; order: 0; }
/* Explicit, because a generated ::after is the last flex item and would
   otherwise fall to the bottom of a wrapped summary instead of sitting on the
   title row where a +/- belongs. Pill and flag orders are set beside .jur. */
.race > summary::after { order: 4; }
.race > summary > .race-brief { order: 5; }
.race > summary > .race-preview { order: 6; }

/* Shut and open are two different views of an office, not one view at two
   heights. Shut: the one line the job comes down to, and the faces standing in
   it. Open: the whole description of the office, and the people under it. Both
   halves of the closed view go when the box opens, or an expanded race is the
   same summary twice. */
/* No 64ch cap here. That measure is for reading paragraphs; this is one line of
   label, and the cap was forcing every one of them onto a second line inside a
   box wide enough to hold it. */
.race-brief {
  flex: 1 1 100%; font-size: .93rem; color: var(--muted);
  line-height: 1.4;
}
.race-preview { flex: 1 1 100%; display: flex; flex-wrap: wrap; gap: 8px; }
.race[open] > summary > .race-brief,
.race[open] > summary > .race-preview { display: none; }
/* ---------- a group of seats in one box ---------- */
/* Five unopposed Justice of the Peace precincts were five boxes in a column
   repeating one sentence with the number changed. They are still five separate
   elections — they are now five boxes inside one, so the column shows the
   office once and the seats stay individually openable underneath it. */
.race-group > .faq-body { background: var(--sunk); }
.race-group .race-child { margin: 10px 0; background: var(--card); }
.race-group .race-child:first-of-type { margin-top: 0; }
.race-group .race-child > summary { background: var(--card); }
.race-group .race-child[open] > summary { background: var(--sunk); }

/* The head's roll-up is a list of seats, not of rivals: the precinct leads, the
   judge sits under it, and there is no face — five faces in one summary is what
   made five elections read as one contest. */
/* A grouped box hides its roster, which left it shorter than every single-seat
   box beside it. This chip holds the row open and says what is behind the lid.
   Its edge takes the jurisdiction's dark tone rather than a fixed colour, so a
   city group does not wear the county's purple. */
.pv.pv-more {
  flex: 0 1 268px; border-left: 5px solid var(--line-strong);
  background: var(--card);
}
.pv.pv-more.e-county { border-left-color: var(--j-county-all-bg); }
.pv.pv-more.e-city { border-left-color: var(--j-city-all-bg); }
.pv.pv-more.e-state { border-left-color: var(--j-state-all-bg); }
.pv-more .pv-name { color: var(--ink); }
.pv-more .pv-meta { color: var(--muted); }

.race-h {
  font-family: "Archivo", sans-serif; font-size: .72rem; letter-spacing: .09em;
  text-transform: uppercase; color: var(--muted);
  margin: 0 0 6px; border: 0; padding: 0;
}
.race-full + .race-h { margin-top: 20px; }

/* One candidate, compressed to the four things worth scanning: face, name,
   party, and whether they hold the seat now. */
/* One width for every chip. Sized to the name, a pair of candidates read as a
   long one and a short one — a difference in the typography that means nothing
   about the two people. */
.pv {
  display: flex; align-items: center; gap: 8px;
  padding: 5px 11px 5px 5px; border-radius: 99px;
  background: var(--card); border: 1.5px solid var(--line-strong);
  flex: 0 1 268px; max-width: 100%; min-width: 0;
}
.pv-face {
  width: 26px; height: 26px; border-radius: 50%; flex: none;
  object-fit: cover; object-position: 50% 28%;
  background: var(--sunk); border: 1px solid var(--line);
}
.pv-txt { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.pv-name {
  font-family: "Archivo", sans-serif; font-weight: 700; font-size: .86rem;
  color: var(--ink); line-height: 1.2;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pv-seat { font-weight: 600; color: var(--muted); }
.pv-face.face-none svg { width: 17px; height: 17px; }
.pv-meta {
  font-size: .68rem; font-weight: 700; letter-spacing: .05em;
  text-transform: uppercase; color: var(--muted); line-height: 1.2;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Party reads as a stripe on the chip's edge rather than a second pill, so the
   preview row stays a row of people instead of a row of labels. */
.pv.p-rep { border-left: 5px solid #b3261e; }
.pv.p-dem { border-left: 5px solid #1b4fa0; }
/* Nonpartisan offices get an edge too, in the neutral tone. Without one they
   read as a chip whose party we failed to look up. */
.pv.p-none { border-left: 5px solid var(--line-strong); }
.party.p-none { background: var(--sunk); }
/* Green only when the person actually holds the seat. A challenger's "first
   election" is a status, not a claim on the office. */
.pv-inc { color: var(--brand-ink); }

/* On a phone every chip takes its own row, so five candidates in one office was
   half a screen for a box that is still shut. Tightened until the whole ballot
   is scannable without opening anything. */
@media (max-width: 720px) {
  .race > summary { padding: 12px 14px; }
  /* A long office name leaves no room beside it for two labels, so it takes the
     row and they share the next one. Three stacked rows before the description
     was most of a phone screen spent on one shut box. */
  .race > summary > .faq-q { flex: 1 1 100%; padding-right: 26px; }
  /* Pinned to the corner rather than flowing last. As a flex item it was being
     pushed onto a row of its own whenever the labels filled the line above,
     leaving a +/- floating under a pill with nothing beside it. */
  .race > summary { position: relative; }
  .race > summary::after { position: absolute; top: 12px; right: 14px; margin-left: 0; }
  .race-preview { gap: 6px; }
  .pv { padding: 3px 9px 3px 3px; gap: 7px; flex: 1 1 100%; }
  .pv-face { width: 26px; height: 26px; }
  .pv-face.face-none svg { width: 16px; height: 16px; }
  .pv-name { font-size: .82rem; }
  .pv-meta { font-size: .63rem; }
}
/* Who votes in this race. Filled, where the status flag is outlined, so the two
   never read as the same kind of label even at a glance. */
.jur {
  font-family: "Archivo", sans-serif; font-size: .66rem; font-weight: 800;
  letter-spacing: .06em; text-transform: uppercase; padding: 3px 10px;
  border-radius: 99px; white-space: nowrap;
}
.j-county { background: var(--j-county-bg); color: var(--j-county); }
.j-city { background: var(--j-city-bg); color: var(--j-city); }
.j-state { background: var(--j-state-bg); color: var(--j-state); }
/* The whole jurisdiction votes: the dark fill. A subdivision of it: the pale
   fill in the same hue. Same colour family answers "whose election is this",
   the weight answers "does it include me". */
.j-county.j-all { background: var(--j-county-all-bg); color: var(--j-county-all); }
.j-city.j-all { background: var(--j-city-all-bg); color: var(--j-city-all); }
.j-state.j-all { background: var(--j-state-all-bg); color: var(--j-state-all); }
.race > summary > .jur { order: 1; }
.race > summary > .race-flag { order: 3; margin-left: auto; }
.race > summary > .race-seats + .race-flag { margin-left: 0; }
.faq.race > summary::after { margin-left: 10px; }

/* One collapsible per future election date. Its children are ordinary race
   boxes, styled as children the way a precinct inside its office group is. */
.cycle-set { margin-top: 26px; }
/* The one-line subtitle came out. Without it the bar collapsed to a strip, so
   the height is set here on purpose rather than left to whatever is inside. */
.cycle-set > summary { padding-top: 20px; padding-bottom: 20px; }
.cycle-set > summary > .faq-q { font-size: 1.08rem; }
.cycle-set > .faq-body { background: var(--sunk); }
.cycle-set .race-child { margin: 10px 0; background: var(--card); }
.cycle-set .race-child:first-of-type { margin-top: 0; }
.cycle-set .race-child > summary { background: var(--card); }
.cycle-set .race-child[open] > summary { background: var(--sunk); }

.race-flag {
  font-family: "Archivo", sans-serif; font-size: 0.68rem; font-weight: 800;
  letter-spacing: .07em; text-transform: uppercase; padding: 3px 10px;
  border-radius: 99px; background: var(--sunk); color: var(--muted);
}
/* One colour per state, filled. Not the tier palette — that means one thing
   only, how well an incident report is corroborated (CLAUDE.md rule 5).
   Contested is the good case: you have a choice. */
.race-flag.f-hot { background: var(--f-hot-bg); color: var(--f-hot); }
.race-flag.f-cold { background: var(--f-cold-bg); color: var(--f-cold); }
.race-flag.f-off {
  background: transparent; color: var(--muted);
  box-shadow: inset 0 0 0 1.5px var(--line-strong);
}
/* Seats are a count, not a state, so they read as text and never compete with
   the pill beside them. margin-left:auto pushes the pair to the right edge. */
.race-seats {
  margin-left: auto; font-family: "Archivo", sans-serif; font-size: .68rem;
  font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  color: var(--muted); white-space: nowrap;
}
.race > summary > .race-seats { order: 2; }
.race-note { font-size: 0.93rem; color: var(--muted); margin: 8px 0 4px; }
.cand-grid { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
.cand { margin: 8px 0 0; }
.cand-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 6px; }
.party {
  font-family: "Archivo", sans-serif; font-size: 0.66rem; font-weight: 800;
  letter-spacing: .06em; text-transform: uppercase; padding: 2px 8px; border-radius: 99px;
  border: 1.5px solid var(--line-strong); color: var(--muted);
}
/* The second pill on every card. Named for what it is now — a status, not
   an incumbency — since a challenger carries one too. */
.party.p-status { background: var(--sunk); }

/* Was --t-reported, which is a tier colour and therefore spoken for (CLAUDE.md
   rule 5) — the same borrowing we took off the Contested flag. A sourced
   explainer is structurally quieter than a card with a phone number on it, so it
   gets the neutral edge rather than a colour of its own. */
.res-card.sourced { border-left-color: var(--line-strong); }
.res-card.sourced ul { margin: 10px 0; padding-left: 20px; }
.res-card.sourced li { margin: 8px 0; }

/* ---------- standing: two honest lines, no verdict badge ---------- */
.pill.st-single { background: var(--t-reported-bg); color: var(--t-reported); }
.pill.st-multiple, .pill.st-record { background: var(--t-verified-bg); color: var(--t-verified); }
.pill.st-disputed { background: var(--t-corroborated-bg); color: var(--t-corroborated); }
.pill.st-record::before { background: currentColor; }
.pill.st-multiple::before { box-shadow: inset 0 0 0 1.5px var(--t-verified-bg), inset 0 0 0 3px currentColor; }
.standing {
  border-left: 3px solid var(--line-strong); padding: 6px 0 6px 12px;
  margin: 10px 0; font-size: 0.88rem; color: var(--muted);
}
.standing-disputed { border-left-color: var(--t-corroborated); }
.standing-record, .standing-multiple { border-left-color: var(--t-verified); }
.st-row { display: flex; gap: 8px; flex-wrap: wrap; }
.st-key {
  font-family: "Archivo", sans-serif; font-size: 0.68rem; font-weight: 800;
  letter-spacing: .07em; text-transform: uppercase; min-width: 108px; color: var(--muted);
}
.code-chip {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.78rem;
  background: var(--sunk); border-radius: 4px; padding: 2px 7px; color: var(--muted);
  letter-spacing: .04em;
}
.att-grid { display: grid; gap: 8px; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); margin: 10px 0; }
.att-btn {
  border: 1.5px solid var(--line-strong); background: var(--paper); color: var(--ink);
  border-radius: 6px; padding: 10px 12px; cursor: pointer; text-align: left;
  font: 600 0.92rem "Public Sans", sans-serif;
}
.att-btn:hover:not([disabled]) { border-color: var(--brand); }
.att-btn[aria-pressed="true"] { background: var(--brand); color: var(--on-cta); border-color: var(--brand); }
.att-btn[disabled] { opacity: .5; cursor: default; }
.att-mismatch[aria-pressed="true"], .att-didnt_happen[aria-pressed="true"] {
  background: var(--t-corroborated); border-color: var(--t-corroborated);
}

table.agreements { border-collapse: collapse; width: 100%; font-size: 0.88rem; margin: 12px 0; }
table.agreements th {
  text-align: left; border-bottom: 2px solid var(--line-strong); padding: 6px 8px;
  font-family: "Archivo", sans-serif; font-size: 0.7rem; text-transform: uppercase;
  letter-spacing: .07em; color: var(--muted);
}
table.agreements td { border-bottom: 1px solid var(--line); padding: 7px 8px; vertical-align: top; }

#detained-note { border-left-color: var(--cta); }

/* ---------- filter groups ---------- */
.filter-group { margin: 10px 0; }
.filter-group-head { display: flex; align-items: baseline; gap: 12px; margin-bottom: 2px; }
.filter-group-head .filter-label { margin-right: auto; }
.filter-group-head .linkish { font-size: 0.8rem; }
.filter-group .filters { margin: 4px 0 0; }
.filter-bar { margin: 10px 0; }
.unrec-row {
  display: flex; align-items: center; gap: 9px; margin: 14px 0 2px;
  font-size: 0.9rem; font-weight: 500; font-family: "Public Sans", sans-serif;
}
.unrec-row input { width: 16px; height: 16px; }

/* ---------- candidate guide page ---------- */
/* Three registers rather than one wall of bold: what the page covers, in
   Archivo; what it tells you, in the body face; and the promise, small and set
   apart. All one weight read as shouting. */
.lede-line { margin: .1em 0 .5em; max-width: 56ch; }
/* The hook, sized to be read first and short enough to hold one line on a
   phone. It is a whole sentence now — the old top line was half of one, which
   is why restyling it never helped. */
.lede-lead {
  font-family: "Archivo", sans-serif; font-weight: 700;
  font-size: clamp(1.22rem, 3.1vw, 1.62rem); line-height: 1.22;
  letter-spacing: -.015em; color: var(--ink); display: block;
}
.lede-body {
  font-size: 1rem; line-height: 1.55; color: var(--muted);
  display: block; margin-top: .5em; max-width: 52ch;
}
.lede-tag {
  display: inline-flex; align-items: center; gap: 8px;
  font-family: "Archivo", sans-serif; font-size: .72rem; font-weight: 800;
  letter-spacing: .09em; text-transform: uppercase; color: var(--brand);
  margin: 0 0 22px;
}
.lede-tag::before {
  content: ''; width: 22px; height: 2px; background: var(--brand); flex: none;
}
.cand-top { display: flex; gap: 14px; align-items: flex-start; margin-bottom: 10px; }
/* object-position rather than the default centre: an official portrait puts the
   face in the upper third, so cropping to a square from the middle cuts the
   chin and keeps the shirt. This is a heuristic that suits every photo we
   currently hold; a portrait framed differently would need its own value. */
.cand-photo {
  width: 64px; height: 64px; border-radius: 8px; flex: none;
  object-fit: cover; object-position: 50% 28%;
  border: 1.5px solid var(--line-strong); background: var(--sunk);
}
/* A head in an empty frame, and the reason for it directly underneath. Initials
   in a photo frame read as a photograph until you look twice, and the label was
   sitting down beside the party pills where it explained nothing. */
.cand-face { display: flex; flex-direction: column; align-items: center; gap: 6px; flex: none; width: 78px; }
.face-none {
  display: grid; place-items: center; color: var(--muted); opacity: .55;
}
.face-none svg { width: 60%; height: 60%; display: block; }
.cand-nophoto {
  font-family: "Archivo", sans-serif; font-size: .6rem; font-weight: 700;
  letter-spacing: .05em; text-transform: uppercase; color: var(--muted);
  text-align: center; line-height: 1.25;
}
.cand-id { display: flex; flex-direction: column; gap: 5px; }
.cand-name { font-family: "Archivo", sans-serif; font-size: 1.08rem; }
.cand-pills { display: flex; gap: 6px; flex-wrap: wrap; }
.party.p-rep { background: #b3261e; border-color: #b3261e; color: #fff; }
.party.p-dem { background: #1b4fa0; border-color: #1b4fa0; color: #fff; }
.cand-more { margin-top: 6px; border-top: 1px solid var(--line); padding-top: 8px; }
.cand-more > summary {
  cursor: pointer; font-family: "Archivo", sans-serif; font-weight: 700;
  font-size: 0.9rem; color: var(--brand-ink); list-style: none;
}
.cand-more > summary::after { content: ' \203a'; }
.cand-more[open] > summary::after { content: ' \2039'; }
.cand-more > summary::-webkit-details-marker { display: none; }
.cand-detail h4 {
  font-family: "Archivo", sans-serif; font-size: 0.72rem; letter-spacing: .08em;
  text-transform: uppercase; color: var(--muted); margin: 14px 0 2px;
}
.cand-detail h4.immig { color: var(--cta); }
/* Source links. Underlined rather than coloured: they sit in a muted line that
   is already quiet, and colour there would compete with the pills above it. */
.src-line a { color: var(--brand-ink); text-decoration: underline; text-underline-offset: 2px; }
.src-line a:hover { color: var(--cta); }
.src-sep { opacity: .55; }

.cand-teaser { border-left: 4px solid var(--brand); }

/* Not a race and not a candidate, so it does not get the green edge or the
   tinted summary that both of those have. An outline is the smallest difference
   that still says "this one is about the page, not about the ballot". */
.faq[data-anchor="method"] { border-color: var(--cta); border-left-width: 1.5px; }
.faq[data-anchor="method"] > summary { background: var(--card); }
.faq[data-anchor="method"] > summary::after { color: var(--cta); }
.faq[data-anchor="method"] > summary:hover .faq-q { color: var(--cta); }
.faq[data-anchor="method"][open] > summary { border-bottom-color: var(--cta); }

/* ---------- Know Your Rights badge ---------- */
/* A circle pinned to the hero's bottom-right, deliberately overlapping whatever
   comes next. It is the one thing on this page a person in trouble needs first,
   and a row of equal-weight cards was not saying so. The pulse stays quiet:
   this sits on a page people open when something is already wrong, and anything
   flashing would be the wrong register entirely.

   Below 900px it stops being a circle. A circle wide enough for three words is
   most of a phone screen, and cropping the words to keep the shape would trade
   the message for the styling. It becomes a full-width bar in the flow instead,
   which is the same prominence by other means. */
/* A seal, not a sticker. The outlined version was a large hollow circle with a
   thin ring and a lot of empty middle, which reads as something stuck on top of
   the page rather than part of it. Filled, it has the weight its size was
   already claiming — and this site's identity is a civic seal to begin with, so
   the shape is borrowed from the brand rather than invented for the corner.
   The mark inside is the same seal the header draws. */
.kyr-badge {
  position: absolute; right: 0; top: 4px; z-index: 3;
  width: 288px; height: 288px; border-radius: 50%;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 5px; padding: 26px; text-align: center; text-decoration: none;
  background: var(--brand); border: 0; color: var(--on-cta);
  box-shadow: 0 10px 30px rgba(0, 0, 0, .22);
  transition: transform .16s ease, box-shadow .16s ease;
}
.kyr-badge:hover {
  transform: translateY(-3px) scale(1.015); color: var(--on-cta);
  box-shadow: 0 14px 36px rgba(0, 0, 0, .26);
}
/* The inset hairline is what makes a disc read as a seal rather than a dot. */
.kyr-badge-ring {
  position: absolute; inset: 11px; border-radius: 50%;
  border: 2px solid var(--cta); opacity: .85; pointer-events: none;
}
.kyr-badge-mark {
  position: absolute; left: 50%; top: 52%; transform: translate(-50%, -50%);
  width: 168px; height: 168px; opacity: .16; margin: 0; z-index: 0;
}
.kyr-badge-mark svg { width: 100%; height: 100%; display: block; }
/* One line is the point of the larger circle. 216px leaves room for the Spanish
   label, which is four characters longer than the English one and was what set
   the width. */
.kyr-badge-text {
  font-family: "Archivo", sans-serif; font-weight: 800; font-size: 1.28rem;
  line-height: 1.15; color: inherit; white-space: nowrap; letter-spacing: -.01em;
  position: relative; z-index: 1;
}
.kyr-badge-hook {
  font-size: .95rem; line-height: 1.3; color: inherit; opacity: .92;
  max-width: none; position: relative; z-index: 1;
}
/* Only drawn where the mark is actually a circle. In the bar layout this was a
   50%-rounded rectangle the width of the screen, which animated as an enormous
   ellipse pulsing across the page — the "flashing circle" on mobile. */
@media (min-width: 1100px) {
  .kyr-badge::after {
    content: ''; position: absolute; inset: -3px; border-radius: 50%;
    border: 2px solid var(--brand); opacity: 0;
    animation: kyrPulse 3.4s ease-out infinite; pointer-events: none;
  }
}
@keyframes kyrPulse {
  0%   { transform: scale(.98); opacity: .55; }
  70%  { transform: scale(1.09); opacity: 0; }
  100% { opacity: 0; }
}
/* A lane on the right kept clear of the copy and the buttons. The badge is
   allowed to hang below the hero and overlap the view switch under it — that is
   the whole idea — but it must never sit on top of the two things this page
   actually asks people to do. */
@media (min-width: 1100px) {
  /* No bottom padding: the circle is meant to hang past the hero and sit over
     the boundary below it. It does that on the right-hand side, where the view
     switch does not reach, so it crosses a section without covering a control. */
  .hero { padding-right: 300px; padding-bottom: 0; }
}
@media (max-width: 1099px) {
  .kyr-badge {
    position: static; width: auto; height: auto; border-radius: 10px;
    flex-direction: row; justify-content: flex-start; align-items: center;
    gap: 13px; flex-wrap: wrap;
    padding: 15px 18px; text-align: left; margin: 30px 0 4px;
    box-shadow: var(--shadow);
  }
  /* A ring inset on a circle; on a bar it was an ellipse three times the height
     of the thing it was supposed to be inside. It becomes the bar's own edge. */
  .kyr-badge-ring { inset: 5px; border-radius: 7px; opacity: .7; }
  .kyr-badge-mark { margin-bottom: 0; width: 30px; height: 30px; }
  .kyr-badge-text { font-size: 1.06rem; }
  .kyr-badge-hook { max-width: none; flex: 1 1 100%; }
}
@media (prefers-reduced-motion: reduce) {
  .kyr-badge::after { animation: none; }
  .kyr-badge { transition: none; }
  .kyr-badge:hover { transform: none; }
}

/* ---------- collapsible FAQ sections ---------- */
/* One accent for every collapsible, and a light edge rather than a slab. An
   earlier version gave each container its own tone by urgency; four colours down
   a page turned the accent into noise and made the set look like a warning
   system rather than a list of answers. The distinction that was worth having is
   between a shut container and the inside of an open one, so that is where the
   colour went. */
.faq {
  border: 1.5px solid var(--line);
  border-left: 3px solid var(--brand);
  border-radius: 8px; margin: 14px 0;
  background: var(--card); overflow: hidden;
  box-shadow: var(--shadow);
}
.faq + .faq { margin-top: 10px; }

/* The summary is tinted whether open or shut, so it reads as a header rather
   than as the first line of the content. Shut, the page becomes a scannable
   column of bars; open, there is an obvious seam between the question and the
   answer — which is what made an expanded box feel like a wall of text. */
.faq > summary {
  cursor: pointer; padding: 15px 18px; list-style: none;
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  background: var(--sunk);
}
.faq > summary::-webkit-details-marker { display: none; }
.faq > summary::after {
  content: '+'; margin-left: auto; font-family: "Archivo", sans-serif;
  font-size: 1.35rem; font-weight: 700; color: var(--brand); line-height: 1;
}
.faq[open] > summary::after { content: '\2013'; }
.faq > summary:hover .faq-q { color: var(--brand-ink); }
.faq[open] > summary { border-bottom: 1.5px solid var(--line); }
.faq-q {
  font-family: "Archivo", sans-serif; font-weight: 700; font-size: 1.05rem;
  color: var(--ink); transition: color .12s ease;
}
@media (prefers-reduced-motion: reduce) { .faq-q { transition: none; } }

/* Inside an open box: a measure short enough to read, room between list items,
   and the lead-in of each point coloured so the eye has somewhere to land.
   Long-form prose set edge to edge at full page width is the wall of text. */
.faq-body { padding: 16px 18px 18px; }
.faq-body > p, .faq-body > ul, .faq-body > ol { max-width: 64ch; }
.faq-body > p:first-child { margin-top: 0; }
/* Custom markers rather than default bullets: a small accent square gives the
   eye a rhythm down a long answer, which is the thing a wall of text lacks. */
.faq-body ul { margin: 12px 0; padding-left: 0; list-style: none; }
.faq-body ol { margin: 12px 0; padding-left: 20px; }
.faq-body ul > li { position: relative; padding-left: 20px; }
/* Positioned from the first line's own box rather than a fixed offset, so the
   marker sits on the text's optical centre whatever the line height is. */
.faq-body ul > li::before {
  content: ''; position: absolute; left: 2px; top: 0.62em;
  width: 7px; height: 7px; border-radius: 2px; background: var(--brand); opacity: .75;
}
.faq-body li { margin: 9px 0; }

/* A card inside an answer is a distinct thing — a phone number, a warning, a
   named organisation — so it sits on the recessed tone while the prose around it
   stays on the card tone. That is the separation the tones were reaching for,
   inside the container where it actually helps. */
.faq-body .card { background: var(--sunk); border-color: var(--line-strong); }
.faq-body .notice { background: var(--sunk); }
.faq-body li > strong:first-child,
.faq-body > p > strong:first-child { color: var(--brand-ink); }
.faq-body > .card:first-child { margin-top: 0; }
.faq-body > .card + .card { margin-top: 14px; }
.res-name { font-family: "Archivo", sans-serif; font-size: 1.02rem; display: block; }
.res-sub { color: var(--muted); font-size: 0.93rem; margin: 4px 0 8px; }
.key-dates { list-style: none; padding: 0; }
.key-dates li { padding: 8px 0; border-bottom: 1px solid var(--line); }
.key-dates li:last-child { border-bottom: 0; }

/* ---------- section head with a counter badge ---------- */
.section-head { display: flex; align-items: baseline; gap: 16px; flex-wrap: wrap; }
.section-head h2 { flex: 1; }
.counter-badge {
  margin: 0; padding: 8px 14px; border-radius: 8px; max-width: 260px;
  background: var(--t-verified-bg); color: var(--t-verified);
  font-size: 0.82rem; line-height: 1.35;
}
.counter-badge strong { font-family: "Archivo", sans-serif; font-size: 1.15rem; display: block; }

/* ---------- review mode ---------- */
/* Only ever rendered for a signed-in reviewer. */
body.rv-on { padding-bottom: 62px; }
/* The notes panel is position:fixed, so it never needed to reserve layout
   space. Reserving it anyway pulled the page off centre the moment review
   mode came on, which made every judgement about spacing and balance wrong
   — in a mode whose entire purpose is judging spacing and balance. The
   panel now floats above the page, and the hide control gets it out of the
   way when it covers something. */
.rv-bar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 6000;
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 9px 14px; background: var(--ink); color: var(--paper);
  font: 500 0.85rem "Public Sans", sans-serif;
}
.rv-badge {
  font-family: "Archivo", sans-serif; font-weight: 800; font-size: 0.7rem;
  letter-spacing: .1em; text-transform: uppercase;
  background: var(--cta); color: #fff; padding: 3px 10px; border-radius: 99px;
}
.rv-bar button, .rv-bar a {
  background: transparent; color: var(--paper); border: 1px solid rgba(255,255,255,.35);
  border-radius: 5px; padding: 5px 11px; cursor: pointer; font: 500 0.82rem "Public Sans", sans-serif;
  text-decoration: none;
}
.rv-bar button:hover, .rv-bar a:hover { background: rgba(255,255,255,.14); color: var(--paper); }
.rv-spacer { flex: 1; }
.rv-who { opacity: .7; font-size: 0.78rem; }
body.rv-picking, body.rv-picking * { cursor: crosshair !important; }
body.rv-picking main *:hover { outline: 2px dashed var(--cta); outline-offset: 2px; }

.rv-panel {
  position: fixed; top: 0; right: 0; bottom: 62px; width: 330px; z-index: 5900;
  overflow-y: auto; padding: 16px 16px 24px; background: var(--card);
  border-left: 2px solid var(--line-strong); font-size: 0.88rem;
  box-shadow: -8px 0 24px rgba(0, 0, 0, .18);
}
@media (max-width: 1099px) { .rv-panel { display: none; } }
.rv-panel h3 { margin-top: 0; font-size: 1rem; display: flex; align-items: center; gap: 8px; }
.rv-count { background: var(--cta); color: #fff; border-radius: 99px; padding: 1px 9px; font-size: .78rem; }
.rv-empty { color: var(--muted); }
.rv-note { border: 1.5px solid var(--line); border-left: 4px solid var(--cta); border-radius: 7px; padding: 10px 12px; margin: 10px 0; }
.rv-note.rv-page { border-left-color: var(--brand); }
.rv-note.rv-orphan { border-left-color: var(--t-reported); opacity: .85; }
.rv-note-head { display: flex; align-items: center; gap: 8px; margin-bottom: 5px; }
.rv-level, .rv-orphan-tag {
  font-family: "Archivo", sans-serif; font-size: .62rem; font-weight: 800;
  letter-spacing: .07em; text-transform: uppercase; color: var(--muted);
}
.rv-orphan-tag { color: var(--t-reported); }
.rv-goto, .rv-note-actions button {
  margin-left: auto; background: none; border: 0; color: var(--brand-ink);
  cursor: pointer; font: 600 .8rem "Public Sans", sans-serif; text-decoration: underline;
}
/* Five controls can sit on one note now, in a 330px panel. Wrapping is the
   whole point: a row that scrolls sideways hides Delete rather than moving it. */
.rv-note-actions { display: flex; flex-wrap: wrap; gap: 6px 12px; margin-top: 6px; }
.rv-note-actions button { margin-left: 0; }
/* The one irreversible control on the panel reads as one. */
.rv-note-actions button[data-del] { color: var(--t-reported); }

/* A reply is subordinate to the note it answers, so it is indented and quieter
   rather than being a second card of equal weight. */
.rv-thread { margin: 8px 0 0; padding-left: 10px; border-left: 2px solid var(--line); }
.rv-comment { padding: 6px 0; }
.rv-comment + .rv-comment { border-top: 1px solid var(--line); }
.rv-comment p { margin: 0 0 3px; font-size: .84rem; white-space: pre-wrap; }
.rv-editor { margin: 6px 0; }
/* min-height is not decoration here: the global textarea rule sets 130px for
   the report form, and a two-row reply box inheriting it eats a third of a
   330px panel. rows="2" is a request the browser ignores while that stands. */
.rv-editor textarea {
  width: 100%; box-sizing: border-box; font: inherit; font-size: .84rem;
  padding: 6px 8px; border: 1.5px solid var(--line-strong); border-radius: 5px;
  background: var(--sunk); color: var(--ink); resize: vertical;
  min-height: 0;
}
.rv-editor-label {
  display: block; margin: 6px 0 2px; color: var(--muted);
  font-family: "Archivo", sans-serif; font-size: .62rem; font-weight: 800;
  letter-spacing: .07em; text-transform: uppercase;
}
.rv-note blockquote {
  margin: 4px 0; padding-left: 9px; border-left: 2px solid var(--line-strong);
  color: var(--muted); font-size: .84rem;
}
.rv-sugg-view { background: var(--t-verified-bg); border-radius: 5px; padding: 6px 9px; font-size: .84rem; }
.rv-meta { color: var(--muted); font-size: .74rem; }
.rv-marked { outline: 2px dashed var(--cta); outline-offset: 3px; }
.rv-flash { animation: rvFlash 1.6s ease-out; }
@keyframes rvFlash { 0%, 100% { background: transparent; } 25% { background: var(--t-reported-bg); } }
@media (prefers-reduced-motion: reduce) { .rv-flash { animation: none; outline: 3px solid var(--cta); } }

.rv-compose {
  position: fixed; inset: 0; z-index: 6100; background: rgba(20,14,10,.55);
  display: flex; align-items: center; justify-content: center; padding: 20px;
}
.rv-compose-inner {
  background: var(--card); border-radius: 10px; padding: 20px 22px;
  width: min(520px, 96vw); box-shadow: var(--shadow);
}
.rv-ctx { margin-top: 0; color: var(--muted); font-size: .9rem; }
.rv-actions { display: flex; gap: 10px; margin-top: 14px; }

/* Basemap fallback. MapLibre needs WebGL; where it is missing we say so rather
   than quietly loading someone else's tiles, and point at the list view. */
.basemap-missing {
  position: absolute; inset: 0; z-index: 400;
  display: flex; align-items: center; justify-content: center;
  padding: 1.5rem; text-align: center;
  background: var(--sunk); color: var(--muted);
  font-size: 1rem; line-height: 1.5;
}

/* Review mode: the page is inert while the notes panel is open, so dragging
   across a sentence inside a link selects it instead of navigating away.
   Hiding the panel hands the site back — which is also how a reviewer checks
   that something still works. */
body.rv-locked { cursor: text; }
.rv-state { font-size: 0.78rem; letter-spacing: 0.06em; text-transform: uppercase; opacity: 0.75; }

/* The button comes to the selection, rather than the reviewer going to a
   toolbar and losing the selection on the way. */
.rv-sel-btn {
  position: absolute; z-index: 10001;
  background: var(--ink); color: var(--paper);
  border: 0; border-radius: 99px; padding: 7px 14px; cursor: pointer;
  font: 600 0.85rem "Public Sans", sans-serif;
  box-shadow: 0 3px 10px rgba(0, 0, 0, .35);
}
.rv-sel-btn:hover { background: var(--brand); }

.rv-panel h3 { display: flex; align-items: center; gap: 8px; }
.rv-hide {
  margin-left: auto; background: none; border: 0; cursor: pointer;
  color: var(--muted); font: 600 0.78rem "Public Sans", sans-serif;
  text-transform: uppercase; letter-spacing: 0.06em;
}
.rv-hide:hover { color: var(--ink); text-decoration: underline; }

/* A capability you do not hold, shown rather than hidden. */
.chip.is-disabled {
  opacity: .45; cursor: not-allowed; pointer-events: auto;
  border-style: dashed;
}
.chip.is-disabled:hover { border-color: var(--line-strong); background: var(--card); }

/* ---------- home page, 2026-08-22 review round ---------- */

/* Register to Vote is the one nav item that is an action with a deadline. The
   underline is decoration, not the text colour, so the link keeps full contrast
   against both grounds and the mark carries the emphasis on its own. */
.site-nav .nav-vote {
  text-decoration: underline;
  text-decoration-color: var(--vote-mark);
  text-decoration-thickness: 2px;
  text-underline-offset: 5px;
}
.site-nav .nav-vote:hover { text-decoration-thickness: 3px; }

/* The recency row now ends in the Advanced filters disclosure, pushed to the
   right edge — the same edge the camera toggle sits on in the map below, so the
   two controls share a vertical line instead of floating independently. */
/* Was keyed off a sibling .filter-group-head, which no longer exists now that
   "Show:" is gone. The disclosure is the only one of these in the row, so it can
   claim the right edge on its own. */
.filters .disclosure { margin-left: auto; }
@media (max-width: 620px) {
  .filters .disclosure { margin-left: 0; }
}

/* ---------- camera layer toggle, on the map ---------- */
/* Leaflet's own controls are white boxes with a black glyph; this one has to
   belong to the site instead, and has to read as pressed or not from across a
   room. Off is not "greyed out" — it is outlined rather than filled, because a
   greyed control usually means disabled. */
.cam-toggle {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2px; width: 62px; padding: 7px 4px 5px; cursor: pointer;
  background: var(--card); color: var(--muted);
  border: 2px solid var(--line-strong); border-radius: 9px;
  box-shadow: 0 1px 4px rgba(0, 0, 0, .22);
  font: 700 .68rem/1 "Archivo", sans-serif; letter-spacing: .04em;
  transition: background .12s ease, color .12s ease, border-color .12s ease;
}
.cam-toggle:hover { border-color: var(--brand); color: var(--ink); }
.cam-toggle[aria-pressed="true"] {
  background: var(--flock); border-color: var(--flock); color: var(--on-cta);
}
.cam-toggle-icon { width: 26px; height: 26px; display: block; }
.cam-toggle-label { display: block; }
@media (prefers-reduced-motion: reduce) { .cam-toggle { transition: none; } }

/* ---------- footer ---------- */
/* Two lines, and symmetric on the first: the mark at one end, the links at the
   other. The brand block used to be a grid with a 1fr column, so it claimed the
   full width and shoved the links onto a row of their own — three stacked lines
   with nothing balancing anything. The tagline is now its own line rather than a
   third thing competing for the first one. */
.footer-grid {
  display: flex; flex-wrap: wrap; align-items: center;
  justify-content: space-between; gap: 10px 32px;
}
.footer-brand { display: flex; align-items: center; gap: 10px; }
.footer-seal { display: block; width: 28px; height: 28px; color: var(--brand); flex: none; }
.footer-seal svg { width: 100%; height: 100%; display: block; }
.footer-name { font-family: "Archivo", sans-serif; font-weight: 700; font-size: 1.05rem; color: var(--ink); }
.footer-links { display: flex; flex-wrap: wrap; gap: 6px 20px; align-items: center; }
.footer-links a { font-size: .9rem; }
.footer-tag {
  flex: 1 0 100%; margin: 0; color: var(--muted); font-size: .88rem;
}
@media (max-width: 560px) {
  .footer-grid { gap: 8px 20px; }
  .footer-links { width: 100%; }
}

/* ---------- Know Your Rights, on the Resources page ---------- */
/* This has to outrank the collapsible headings under it, and a link in a line
   of body text did not. Everything else on that page is a phone number for a
   situation you are already in; this is the one thing worth reading before you
   are in one. Same burnt-orange border as the home page badge, so the two read
   as the same offer in two places rather than two different things. */
.kyr-strip {
  display: flex; align-items: center; gap: 16px;
  margin: 4px 0 26px; padding: 18px 22px; border-radius: 10px;
  text-decoration: none; color: var(--ink);
  background: var(--card); border: 3px solid var(--cta);
  box-shadow: var(--shadow);
  transition: transform .15s ease, box-shadow .15s ease;
}
.kyr-strip:hover {
  transform: translateY(-2px); color: var(--ink);
  box-shadow: 0 8px 24px rgba(0, 0, 0, .15);
}
.kyr-strip-text { display: flex; flex-direction: column; gap: 3px; flex: 1; }
.kyr-strip-text > strong {
  font-family: "Archivo", sans-serif; font-size: 1.28rem; color: var(--cta); line-height: 1.2;
}
.kyr-strip-sub { font-size: .92rem; color: var(--muted); }
.kyr-strip-arrow {
  font-size: 1.6rem; color: var(--cta); flex: none; transition: transform .15s ease;
}
.kyr-strip:hover .kyr-strip-arrow { transform: translateX(4px); }
@media (max-width: 560px) {
  .kyr-strip { padding: 16px 18px; }
  .kyr-strip-text > strong { font-size: 1.12rem; }
  .kyr-strip-arrow { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .kyr-strip, .kyr-strip-arrow { transition: none; }
  .kyr-strip:hover { transform: none; }
}

/* ---------- election day ---------- */
/* The date is the answer to the question that brings people to this page, and
   it was three levels down inside a collapsed section. Sized so it is legible
   from the distance someone holds a phone when they are checking one fact. */
.eday {
  margin: 4px 0 26px; padding: 18px 22px; border-radius: 10px;
  background: var(--card); border: 2px solid var(--line-strong);
  border-left: 7px solid var(--brand); box-shadow: var(--shadow);
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 14px 20px;
}
/* The text column takes what the calendar does not, so the two sit close rather
   than being pushed to opposite edges by space-between. */
.eday-main { flex: 1 1 14rem; min-width: 0; }
.eday-copy { min-width: 0; }
/* One line at every width. The clamp floor keeps it on one line at 320px; the
   ceiling is where it stops growing. */
.eday-h1 {
  margin: 0 0 8px; white-space: nowrap;
  font-size: clamp(1.2rem, 5.4vw, 2.3rem);
}
@media (min-width: 760px) { .eday-h1 { font-size: clamp(1.6rem, 3.4vw, 2.3rem); } }
/* Narrow, the heading cannot share a row — it is one line and that line is wider
   than what is left beside the calendar. display: contents dissolves the wrapper
   so the heading spans the block and the dates sit beside the calendar rather
   than above it, which is both shorter and puts the two date facts next to the
   date they are about. */
@media (max-width: 759px) {
  .eday-main { display: contents; }
  .eday-h1 { flex: 1 0 100%; }
  .eday-copy { flex: 1 1 8rem; }
}

/* A torn-off calendar page rather than an icon set: a red header bar, the month
   above the day, and the days remaining along the foot. Red here is the one
   place on the site it means a deadline rather than an error, which is what a
   calendar face already says before any word is read. */
.eday-cal {
  flex: none; width: 116px; border-radius: 9px; overflow: hidden;
  background: var(--card); border: 2px solid var(--danger);
  box-shadow: var(--shadow); text-align: center;
  display: flex; flex-direction: column;
}
.eday-cal-mon {
  display: block; padding: 4px 0 5px; background: var(--danger); color: #fff;
  font-family: "Archivo", sans-serif; font-weight: 800; font-size: .78rem;
  letter-spacing: .12em;
}
.eday-cal-day {
  display: block; font-family: "Archivo", sans-serif; font-weight: 800;
  font-size: 2.5rem; line-height: 1; color: var(--ink); padding: 8px 0 0;
}
.eday-cal-dow {
  display: block; padding: 1px 0 7px; color: var(--muted);
  font-family: "Archivo", sans-serif; font-weight: 700; font-size: .68rem;
  letter-spacing: .1em;
}
.eday-cal-left {
  display: block; padding: 5px 4px; background: var(--sunk);
  border-top: 1.5px solid var(--line);
  color: var(--muted); font-size: .72rem; font-weight: 600;
}
@media (max-width: 560px) {
  .eday { gap: 12px 16px; padding: 16px 18px; }
  .eday-cal { width: 92px; }
  .eday-cal-day { font-size: 2.2rem; }
  .eday-more { gap: 3px 16px; }
}
.eday-date {
  margin: 2px 0 0; font-family: "Archivo", sans-serif; font-weight: 700;
  font-size: clamp(1.5rem, 4.4vw, 2.1rem); line-height: 1.12; color: var(--ink);
}
.eday-more {
  list-style: none; padding: 0; margin: 0;
  display: flex; flex-wrap: wrap; gap: 4px 22px;
}
.eday-more li { color: var(--muted); font-size: .93rem; }
.eday-more strong { color: var(--ink); }

/* ---------- header submenus ---------- */
/* The wrapper is the hover target, not the link, so the pointer can cross the
   gap into the panel without the menu closing under it. */
.nav-item { position: relative; display: inline-flex; align-items: center; }
.nav-sub {
  position: absolute; top: calc(100% + 8px); left: 0; z-index: 6000;
  min-width: 230px; max-width: min(320px, 92vw); padding: 6px;
  background: var(--card); border: 1.5px solid var(--line-strong);
  border-radius: 9px; box-shadow: 0 10px 28px rgba(0, 0, 0, .18);
  display: flex; flex-direction: column;
}
/* Bridges the gap between link and panel so the pointer never crosses dead
   space — the classic reason a hover menu feels like it is dodging you. */
.nav-sub::before {
  content: ''; position: absolute; left: 0; right: 0; top: -10px; height: 10px;
}
.nav-sub a {
  display: block; padding: 8px 11px; border-radius: 6px; border-bottom: 0;
  font-size: .89rem; font-weight: 600; color: var(--ink); line-height: 1.3;
}
.nav-sub a:hover { background: var(--sunk); color: var(--ink); border-bottom: 0; }
/* Know Your Rights is not a section of Resources, it is the page you want
   before you need any of them. Coloured to say so, and kept off the burnt
   orange, which belongs to Report. */
.nav-sub a.sub-rights {
  color: var(--brand-ink); font-weight: 800;
  background: var(--t-verified-bg);
  border-bottom: 1px solid var(--line); border-radius: 6px 6px 0 0; margin-bottom: 4px;
}
.nav-sub a.sub-rights:hover { background: var(--brand); color: var(--on-cta); }

/* On a narrow screen the header nav already wraps to its own row; a panel
   pinned to one item would hang off the edge. It becomes a plain indented list
   under the link instead. */
@media (max-width: 720px) {
  .nav-item { display: block; width: 100%; }
  .nav-sub {
    position: static; min-width: 0; max-width: none; margin: 4px 0 6px 12px;
    box-shadow: none; border-left-width: 3px;
  }
  .nav-sub::before { display: none; }
}
@media (prefers-reduced-motion: reduce) { .nav-sub { transition: none; } }

/* Deliberately quiet: a plain underlined word, not a button that competes with
   the answer chips it undoes. It only exists once an answer has been given. */
.wiz-reset-row { margin: 14px 0 0; }
.wiz-reset {
  background: none; border: 0; padding: 0; cursor: pointer;
  font: inherit; font-size: .88rem; font-weight: 600;
  color: var(--muted); text-decoration: underline; text-underline-offset: 3px;
}
.wiz-reset:hover { color: var(--brand-ink); }
.wiz-reset:focus-visible { outline: 2px solid var(--brand); outline-offset: 3px; border-radius: 3px; }

/* =========================================================================
   Mobile
   =========================================================================
   One breakpoint at 720px, matching the one the header submenus already use,
   so the nav has exactly two states rather than three overlapping ones. */

.nav-burger { display: none; }
.nav-report-long { display: none; }

@media (min-width: 721px) {
  .nav-report-short { display: inline; }
  .time-toggle { display: none; }
}

@media (max-width: 720px) {
  /* ---- the header collapses to a mark and a button ---- */
  .site-inner { padding: 10px 16px; gap: 12px; flex-wrap: nowrap; }
  header.site .brand { flex: 1 1 auto; min-width: 0; }
  header.site .brand span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

  .nav-burger {
    display: inline-flex; align-items: center; justify-content: center;
    flex: none; width: 44px; height: 44px; padding: 0; cursor: pointer;
    background: var(--card); color: var(--ink);
    border: 1.5px solid var(--line-strong); border-radius: 9px;
  }
  .nav-burger-bars { display: block; width: 20px; }
  .nav-burger-bars i {
    display: block; height: 2.5px; border-radius: 2px; background: currentColor;
    transition: transform .16s ease, opacity .16s ease;
  }
  .nav-burger-bars i + i { margin-top: 4.5px; }
  /* The bars become a cross while the panel is open, so the control says what
     pressing it again will do. */
  header.nav-open .nav-burger-bars i:nth-child(1) { transform: translateY(7px) rotate(45deg); }
  header.nav-open .nav-burger-bars i:nth-child(2) { opacity: 0; }
  header.nav-open .nav-burger-bars i:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
  header.nav-open .nav-burger { border-color: var(--brand); color: var(--brand-ink); }

  /* ---- the panel ---- */
  nav.site-nav {
    display: none; position: absolute; left: 0; right: 0; top: 100%;
    z-index: 6200; flex-direction: column; align-items: stretch; gap: 0;
    padding: 10px 16px 16px; margin: 0;
    background: var(--card); border-top: 1.5px solid var(--line);
    box-shadow: 0 14px 30px rgba(0, 0, 0, .22);
  }
  header.nav-open nav.site-nav { display: flex; }
  header.site { position: relative; }
  body.nav-locked { overflow: hidden; }

  /* Report leads, in the one colour nothing else in the panel uses. */
  nav.site-nav a.nav-report {
    order: -1; width: 100%; margin: 2px 0 10px; padding: 14px 16px;
    background: var(--cta); color: var(--on-cta); border-radius: 9px;
    font-size: 1.02rem; text-align: center; border-bottom: 0;
  }
  nav.site-nav a.nav-report:hover { background: var(--cta-hover); color: var(--on-cta); }
  .nav-report-long { display: inline; }
  .nav-report-short { display: none; }

  nav.site-nav > a, .nav-item > a {
    display: block; width: 100%; padding: 13px 4px; font-size: 1.02rem;
    border-bottom: 1px solid var(--line);
  }
  /* In the panel every row already has a rule under it, so an underline reads
     as a border rather than as a mark. Colour carries it here instead. */
  nav.site-nav a, nav.site-nav a[aria-current="page"] { text-decoration: none; }
  nav.site-nav a[aria-current="page"] { color: var(--brand-ink); font-weight: 800; }
  nav.site-nav a.nav-vote, nav.site-nav a.nav-vote[aria-current="page"] {
    color: var(--vote-mark);
  }
  .nav-item { display: block; width: 100%; }

  /* Language and theme sit together on one row at the foot of the panel. */
  .lang-toggle, .theme-toggle { margin-top: 12px; }
  .lang-toggle { order: 90; align-self: flex-start; }
  .theme-toggle { order: 91; position: absolute; right: 16px; bottom: 16px; }
}

/* ---- the two filter groups become one pair of matching disclosures ---- */
@media (max-width: 720px) {
  .filters-wrap { margin: 10px 0 12px; }
  .filter-group { margin: 0; }
  .filters { gap: 7px; }
  .filters .chip { font-size: .86rem; padding: 7px 12px; }
  /* Time and Advanced are siblings of equal weight, side by side, each opening
     its own panel. Previously one was a row of five chips always on screen and
     the other was a single word at the far right — two different shapes doing
     the same kind of job. */
  /* Time and Advanced: two disclosures of equal weight, side by side, and they
     keep that width whatever else is on screen. The window chips start on the
     line below rather than pushing into the row and squeezing Advanced down to
     its text — a control that changes size depending on what else is open reads
     as broken. */
  .filters { flex-wrap: wrap; row-gap: 9px; }
  .filters > .disclosure {
    order: -1; flex: 1 1 calc(50% - 5px); min-width: 0;
    text-align: center; margin-left: 0;
  }
  /* A zero-height full-width item, ordered with the disclosures and last among
     them, so the line always breaks after the pair. */
  .filters.time-open::after { content: ''; flex: 0 0 100%; height: 0; order: -1; }
  .filters > .win { display: none; }
  .filters.time-open > .win {
    display: inline-block; flex: 0 0 auto;
    padding: 6px 9px; font-size: .82rem;
  }
  .filters.time-open { column-gap: 6px; }
  .home-panel-head { width: 100%; }
  .home-panel-head .chip { flex: 1 1 0; text-align: center; padding: 10px 8px; }
}

/* ---- footer: fewer lines, centred, nothing competing ---- */
@media (max-width: 720px) {
  .footer-grid { flex-direction: column; align-items: center; text-align: center; gap: 10px; }
  .footer-links { width: auto; justify-content: center; gap: 4px 18px; }
  .footer-tag { flex: 0 1 auto; max-width: 34ch; }
  footer.site { padding-bottom: 8px; }
}

/* A big soft glyph on the right of a full-width control. It is texture, not an
   icon: sized past the control's own height, faded, clipped by the control, and
   sitting behind the words. Small icons beside text on a wide bar leave the
   right two-thirds empty and read as decoration nobody chose. */
/* Only the button needs a positioning context here. The badge already has one
   in the circle layout (absolute, in the corner) and setting position: relative
   on it further down the file quietly beat that rule at equal specificity and
   dropped it back into normal flow — which is the drift. Clipping is scoped the
   same way: the circle must not clip, because its pulse ring sits outside it. */
.hero-btn { position: relative; overflow: hidden; }
.btn-glyph, .kyr-badge-mark { pointer-events: none; }
.btn-glyph svg, .kyr-badge-mark svg { width: 100%; height: 100%; display: block; }
.hero-btn > span:not(.btn-glyph) { position: relative; z-index: 1; }
.btn-glyph {
  position: absolute; right: -4px; top: 50%; transform: translateY(-50%);
  width: 96px; height: 96px; opacity: .2; z-index: 0;
}
.hero-btn { padding-right: 84px; }
@media (max-width: 560px) {
  .btn-glyph { width: 84px; height: 84px; right: -6px; }
  .hero-btn { padding-right: 74px; }
}

/* The rights bar, below the circle breakpoint: the mark leaves the left, where
   it was a small check beside the words, and becomes the same oversized wash on
   the right. */
@media (max-width: 1099px) {
  .kyr-badge { position: relative; overflow: hidden; }
  .kyr-badge-mark {
    position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
    width: 84px; height: 84px; opacity: .2; margin: 0; z-index: 0;
  }
  .kyr-badge-text, .kyr-badge-hook, .kyr-badge-ring { position: relative; z-index: 1; }
  .kyr-badge { padding-right: 96px; }
}
@media (max-width: 560px) {
  .kyr-badge-mark { width: 72px; height: 72px; right: 6px; }
  .kyr-badge { padding-right: 82px; }
}

/* Matches the camera toggle rather than Leaflet's default white box, so the two
   controls on this map look like they came from the same place. */
.map-recentre {
  display: flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; padding: 0; cursor: pointer;
  background: var(--card); color: var(--ink);
  border: 2px solid var(--line-strong); border-radius: 8px;
  box-shadow: 0 1px 4px rgba(0, 0, 0, .22);
}
.map-recentre:hover { border-color: var(--brand); color: var(--brand-ink); }
.map-recentre svg { width: 19px; height: 19px; display: block; }

/* ---------- offices that are not on this ballot ---------- */
/* Their own section under the ballot, because the criterion is the County
   Clerk's own list of which offices come up when — not a judgement about which
   officials matter. The same box as a race, with the summary and body swapped
   and the edge dashed, because there is nothing here to vote on yet. */
.race.offballot {
  border-left-style: dashed; border-left-color: var(--line-strong);
  background: var(--sunk);
}
.race.offballot > summary { background: var(--card); }
.race.offballot > summary::after { color: var(--muted); }
.race.offballot > summary:hover .faq-q { color: var(--ink); }
.race.offballot .race-flag {
  background: var(--card); color: var(--muted);
  box-shadow: inset 0 0 0 1.5px var(--line-strong);
}
.race.offballot .pv, .race.offballot .cand { background: var(--paper); }

/* A line, not a section. Rex asked for a couple of words at the foot of the
   page rather than a contact panel, and the address does the work. */
/* The contact line sits in its own container rather than hanging off a hairline
   under the last collapsible, so "do not email reports" reads as a statement of
   its own instead of page furniture. */
.contact-box {
  margin: 30px 0 0; padding: 15px 18px;
  background: var(--card); border: 1.5px solid var(--line-strong); border-radius: 9px;
  box-shadow: var(--shadow);
}
.contact-line {
  margin: 0; color: var(--muted); font-size: .95rem; line-height: 1.6;
}
.contact-line a { font-weight: 700; }
/* "Use the form" is a single instruction; broken across two lines it stops
   looking like the link it is. */
.nowrap { white-space: nowrap; }
