/* Shared data-component layer for Deflock Altoona.
 *
 * Reuses site-shell.css's color and type tokens (--ink/--muted/--rule/--paper/--surface/
 * --accent[-dark]/--series-1..3, Georgia for reading text, Arial for labels/numerals) and its
 * existing chart primitives (.chart-tooltip, .chart-grid, .chart-axis, .series-legend, etc. --
 * defined once in site-shell.css and NOT duplicated here). This file adds no new font and no
 * chart library; it is plain CSS consumed by hand-written DOM (vote-stats.js-style helpers).
 *
 * Every page that wants these pieces links this stylesheet after site-shell.css:
 *   <link rel="stylesheet" href="/site-shell.css">
 *   <link rel="stylesheet" href="/data-components.css">
 *
 * Sections below, each documented at its own rule block:
 *   1. Role/type accents      -- one color-to-role mapping, reused by cards, chips, table rows,
 *                                 and chart legends so a reader learns it once, site-wide.
 *   2. Stat tile / stat row   -- a big reviewed number with its label and source/date line.
 *   3. Badge / tag            -- small inline labels (role tags, status tags).
 *   4. Data table             -- sticky header, numeric alignment, keyboard-scrollable region.
 *   5. Disclosure card        -- a collapsed summary with a real-number teaser (the /council
 *                                 member-disclosure pattern, generalized).
 *   6. Chart container        -- title + caption + source line + a "Show as a table" toggle.
 *   7. Empty / collecting state, caveat note.
 *
 * See docs/data-components.md for the usage guide and class-by-class examples.
 */

/* --------------------------------------------------------------------------------------------
 * 1. Role/type accents
 *
 * Five hues, one per role, fixed order (Mayor, Council, Police, City official, Public) -- never
 * cycled, never reassigned by a filter or a sort. Chosen to sit apart from every color
 * VoteStats.TONES already uses for vote positions (aye=--tone-positive, nay=--tone-analysis,
 * abstain=--tone-external, absent=--tone-miss) and from the presence-grid tones
 * (present/late/absent), so a role accent never doubles as a vote-position or attendance signal
 * on the same page.
 *
 * --role-mayor / --role-council / --role-official reuse the site's own --series-1/2/3 (already
 * validated for categorical use in site-shell.css). --role-police and --role-public are their own
 * hexes, chosen with scripts/validate_palette.js run with `--pairs all` (every pair, not just
 * adjacent -- the small-multiples remarks-by-role chart puts all five side by side) against both
 * chart surfaces (`#fffdf7` light, `#25231f` dark): light `#2a78d6,#eb6834,#1baf7a,#a6317d,#7322c3`
 * and dark `#3987e5,#d95926,#199e70,#d66aa8,#8c3cdd` both pass every check (two WARNs, mitigated
 * below). The previous --role-police (`#b3453a`/`#cf7256`, the site's --accent rust hue retuned)
 * read as nearly the same orange as --role-mayor once compared against every pair rather than
 * only its neighbor in the old four-color order: normal-vision ΔE 14.0 light / 5.7 dark, both
 * under the 15 floor a full-color reader needs to tell two marks apart. See docs/data-components.md
 * for the full report.
 *
 * These are the CSS names JS keys must match: PeopleViz.roleKey() returns "official" (not the
 * person.category value "city-official") and "public" (a chart-only aggregate row, never a
 * roleKey(person) result) precisely so a class/variable name here always has a matching JS key --
 * a mismatch (the previous "city-official" roleKey against this file's "official" names) leaves
 * `var(--role-city-official)` unresolved with no fallback, which renders black (an SVG bar's
 * `fill`) or the `.chip-swatch` gray fallback, in both themes, not one.
 */
:root {
  --role-mayor: var(--series-2, #eb6834);
  --role-council: var(--series-1, #2a78d6);
  --role-police: #a6317d;
  --role-official: var(--series-3, #1baf7a);
  --role-public: #7322c3;
}
html[data-theme="dark"] {
  --role-police: #d66aa8;
  --role-public: #8c3cdd;
}

.role-mayor { --role-accent: var(--role-mayor); }
.role-council { --role-accent: var(--role-council); }
.role-police { --role-accent: var(--role-police); }
.role-official { --role-accent: var(--role-official); }
/* Renders on /people's "Remarks by role, per year" chart as the public-comment-speakers row
 * (a chart aggregate, never a per-person profile -- public commenters get no profile or card). */
.role-public { --role-accent: var(--role-public); }

/* Cards: override the existing generic `border-top: 4px solid var(--accent)` with the role hue,
 * and prepend a small role tag above the heading. Works alongside .card (site pages already
 * define that box); this rule only changes color and adds the tag, not layout. */
[class*="role-"].card,
.data-table tbody tr[class*="role-"],
.chip[class*="role-"] {
  border-top-color: var(--role-accent);
}
/* Text stays --ink (never the role color): role-official's green does not clear 4.5:1 text
 * contrast on the paper surface (see docs/data-components.md's validator numbers -- that WARN
 * was scoped to chart marks, which need only 3:1, not to small bold tag text, which needs 4.5:1).
 * The role color still carries identity via the border, the tint, and the swatch dot. */
.role-tag {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin: 0 0 6px;
  padding: 2px 7px;
  border: 1px solid var(--role-accent);
  border-radius: 3px;
  color: var(--ink);
  background: color-mix(in srgb, var(--role-accent) 12%, transparent);
  font: 700 0.65rem/1.4 Arial, Helvetica, sans-serif;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.role-tag::before {
  content: "";
  display: inline-block;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--role-accent);
}
/* Filter chips: same class as the cards they filter, so the pressed chip visibly matches. */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 24px;
  /* Compact (rule 17): filters stay up front without outweighing the data they filter. */
  padding: 4px 10px;
  border: 1px solid var(--rule, #b8ae9d);
  border-left: 4px solid var(--role-accent, var(--rule, #b8ae9d));
  background: transparent;
  color: var(--ink);
  cursor: pointer;
  font: 700 0.72rem/1.3 Arial, Helvetica, sans-serif;
  letter-spacing: 0.04em;
}
.chip[aria-pressed="true"] {
  border-color: var(--role-accent, var(--accent-dark));
  background: color-mix(in srgb, var(--role-accent, var(--accent-dark)) 16%, var(--surface));
  /* Explicit, not inherited: a page's own generic `button[aria-pressed="true"] { color: ...
   * }` (e.g. /people's roster-filter buttons) has higher specificity than this rule's base
   * `.chip { color: var(--ink); }` and would otherwise win, painting this light 16%-tint
   * background with that rule's light/paper text -- axe caught this as a ~1:1 contrast failure
   * on /people's role-colored Facets chips (2026-09-27, "Who was in the room"/"Time in the
   * record" merge). `.chip[aria-pressed="true"]`'s own specificity already beats a plain
   * `button[aria-pressed="true"]`, so restating `--ink` here is enough to win on every page. */
  color: var(--ink);
}
.chip-swatch {
  display: inline-block;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--role-accent, var(--muted));
}
/* Former: a modifier, not a sixth hue. Stack it (role-council role-former): dash the rule and
 * mute the tag, but keep the role's own color so the legend still holds. */
.role-former {
  border-top-style: dashed;
}
.role-former .role-tag {
  opacity: 0.72;
  border-style: dashed;
}
/* Chart legend swatches share the same variables so a chart's key matches the cards exactly. */
.role-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 16px;
  margin: 6px 0 10px;
  color: var(--muted);
  font: 0.74rem/1.4 Arial, Helvetica, sans-serif;
}
.role-legend span { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
.role-legend .chip-swatch { width: 10px; height: 10px; }
/* PeopleViz.tenureTimelineChart's "documented span" segments (a police/city official's first-to-
 * last "City Officials Present" listing, not a sourced term): an outline in the row's own role
 * color instead of a solid fill (the fill/stroke color itself is set inline per row, same as
 * every other role-colored bar in this file's charts) -- a style difference, not a color
 * difference, so it still reads under a color-vision test and in a printed/forced-colors view. */
.tenure-documented { stroke-width: 2px; }

/* --------------------------------------------------------------------------------------------
 * 2. Stat tile / stat row -- a reviewed number, its label, and where it came from.
 */
.stat-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
  gap: 10px;
  margin: 14px 0;
}
.stat-tile {
  padding: 12px 14px;
  border: 1px solid var(--rule, #b8ae9d);
  border-top: 3px solid var(--role-accent, var(--accent, #7d2e24));
  background: var(--surface, #fffdf7);
}
.stat-tile .stat-value {
  display: block;
  color: var(--ink);
  font: 700 clamp(1.5rem, 4vw, 2.1rem)/1.1 Georgia, "Times New Roman", serif;
  font-variant-numeric: tabular-nums;
}
.stat-tile .stat-label {
  display: block;
  margin-top: 4px;
  color: var(--muted);
  font: 700 0.68rem/1.3 Arial, Helvetica, sans-serif;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
/* "This number changed" (src/changed-marker.js): a recent correction to the section a tile sits in.
 * Words, date and an edit glyph carry it, with a dashed outline; ink colors only, never gold
 * (gold marks Altoona) and never color alone (rule 27). Opens the correction on /whats-new. */
.stat-corrected {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-top: 6px;
  padding: 1px 7px;
  border: 1px dashed var(--ink, #1f1a14);
  border-radius: 3px;
  color: var(--ink, #1f1a14);
  font: 700 0.68rem/1.5 Arial, Helvetica, sans-serif;
  letter-spacing: 0.04em;
  text-decoration: none;
}
.stat-corrected:hover, .stat-corrected:focus-visible { text-decoration: underline; text-underline-offset: 2px; }
.stat-corrected:focus-visible { outline: 2px solid var(--accent-dark, #7a2e24); outline-offset: 2px; }
.stat-corrected-glyph { font-weight: 400; }
@media (pointer: coarse) { .stat-corrected { min-height: 24px; } }
@media print { .stat-corrected { border-color: #000; color: #000; } }
.stat-tile .stat-source {
  display: block;
  margin-top: 6px;
  color: var(--muted);
  font: 0.72rem/1.4 Arial, Helvetica, sans-serif;
}

/* --------------------------------------------------------------------------------------------
 * 3. Badge / tag -- small inline labels, distinct from .role-tag (which always names a role).
 */
.tag {
  display: inline-block;
  padding: 2px 8px;
  border: 1px solid var(--rule, #b8ae9d);
  border-radius: 999px;
  color: var(--muted);
  font: 700 0.68rem/1.4 Arial, Helvetica, sans-serif;
  letter-spacing: 0.03em;
}
.tag.is-current { border-color: var(--tone-positive, #2b6b3a); color: var(--tone-positive, #2b6b3a); }
.tag.is-former { border-style: dashed; }
/* A record-kind badge (Memo, Report, Order): a quiet filled tag in ink on the deep paper (rule 27:
 * gold is Altoona's alone). The class name stays for the pages that set it. */
.tag.is-gold { border-color: var(--rule, #b8ae9d); background: var(--paper-deep, transparent); color: var(--ink, #20211e); }
/* An Altoona badge or tag: the one place a tag wears the city's gold. */
.tag.is-altoona { border-color: var(--altoona, #9c7300); background: var(--altoona-wash, transparent); color: var(--altoona-ink, #6e5200); }

/* --------------------------------------------------------------------------------------------
 * 4. Data table -- sticky header, numeric alignment, keyboard-scrollable region.
 * Wrap the <table> in a `.data-table-scroll` div with tabindex="0" role="region"
 * aria-label="..." so a keyboard user can scroll a wide table without a mouse.
 */
.data-table-scroll {
  max-width: 100%;
  margin: 10px 0 16px;
  overflow-x: auto;
}
.data-table-scroll:focus-visible {
  outline: 3px solid var(--blue, #315b68);
  outline-offset: 2px;
}
.data-table {
  width: 100%;
  min-width: 560px;
  border-collapse: collapse;
  font: 0.85rem/1.4 Arial, Helvetica, sans-serif;
}
/* A narrow table (three or four short columns) that should wrap on a phone rather than scroll. */
.data-table.is-compact { min-width: 0; }
.data-table caption {
  padding-bottom: 8px;
  color: var(--muted);
  font-size: 0.8rem;
  text-align: left;
  caption-side: top;
}
.data-table thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: 8px 10px;
  border-bottom: 2px solid var(--ink);
  background: var(--paper, #f5f0e6);
  color: var(--ink);
  text-align: left;
  font-weight: 700;
}
.data-table thead th[aria-sort] { cursor: pointer; }
.data-table thead th button {
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: inherit;
  cursor: pointer;
}
.data-table thead th button:hover,
.data-table thead th button:focus-visible { text-decoration: underline; }
.data-table thead th[aria-sort="ascending"] button::after { content: " \2191"; }
.data-table thead th[aria-sort="descending"] button::after { content: " \2193"; }
.data-table tbody td, .data-table tbody th {
  padding: 7px 10px;
  border-bottom: 1px solid var(--rule, #b8ae9d);
  vertical-align: top;
  text-align: left;
}
.data-table tbody tr[class*="role-"] {
  border-left: 4px solid var(--role-accent, transparent);
}
.data-table .num, .data-table td.num {
  font-variant-numeric: tabular-nums;
  text-align: right;
}
/* Figures never break mid-number; a numeric column's header ("Minutes posted") still wraps, so a
 * compact table fits a 320px screen instead of scrolling sideways. */
.data-table td.num { white-space: nowrap; }

/* --------------------------------------------------------------------------------------------
 * 5. Disclosure card -- a collapsed summary with a real-number teaser, generalized from
 * /council's .member-disclosure so any page can reuse the same interaction.
 */
.disclosure-card {
  border: 1px solid var(--rule, #b8ae9d);
  background: var(--surface, #fffdf7);
}
.disclosure-card[open] { border-left: 6px solid var(--role-accent, var(--accent)); }
.disclosure-card > summary {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 8px 14px;
  padding: 14px 16px;
  cursor: pointer;
  font-family: Arial, Helvetica, sans-serif;
}
.disclosure-card > summary::marker { color: var(--role-accent, var(--area-accent, var(--accent))); }
.disclosure-card .disclosure-title {
  color: var(--ink);
  font: 700 1.05rem/1.2 Georgia, "Times New Roman", serif;
}
.disclosure-card .disclosure-teaser {
  /* --ink, not --role-accent: a role-official (green) teaser would fail 4.5:1 text contrast on
   * the paper surface (docs/data-components.md). The role color is still carried by the open
   * card's left border and marker. */
  color: var(--ink);
  font: 700 0.8rem/1.3 Arial, Helvetica, sans-serif;
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.disclosure-card .disclosure-body {
  padding: 0 16px 16px;
  border-top: 1px solid var(--rule, #b8ae9d);
}

/* --------------------------------------------------------------------------------------------
 * 6. Chart container -- title, caption, source line, table toggle. The chart itself (an SVG
 * built by vote-stats.js or people-viz.js helpers) mounts into `.chart-figure`.
 */
.chart-container {
  margin: 18px 0;
  padding: 16px 18px;
  border: 1px solid var(--rule, #b8ae9d);
  background: var(--surface, #fffdf7);
}
.chart-container h3, .chart-container h4 {
  margin: 0;
  font-size: 1rem;
}
.chart-container .chart-caption {
  margin: 6px 0 0;
  color: var(--muted);
  font: 0.82rem/1.5 Arial, Helvetica, sans-serif;
}
.chart-container .chart-source {
  margin: 6px 0 0;
  color: var(--muted);
  font: 0.72rem/1.4 Arial, Helvetica, sans-serif;
}
.chart-container .chart-figure { margin-top: 10px; }
/* .chart-csv-button (a figure's "Download CSV") and .chart-expand-button (a "Show all N" row-cap
 * expander, e.g. /people's "Time in the record") share this look but are never the SAME class as
 * .chart-table-toggle: scripts/check_people_table.mjs finds the overview band's "Show as a table"
 * toggles by that exact class, one per .chart-container, and pairs each with its container by
 * list index -- a second same-class button in one container would shift every later index out of
 * alignment with its own container. */
/* Quiet secondary controls (docs/design-guidelines.md rule 17): sentence case, small, a hairline
 * border in the rule color; the accent only on hover/focus, so the figure above stays the loudest
 * thing in the frame. */
.chart-table-toggle, .chart-csv-button, .chart-expand-button {
  display: inline-block;
  margin-top: 8px;
  min-height: 24px;
  padding: 3px 9px;
  border: 1px solid var(--rule, #b8ae9d);
  border-radius: 999px;
  background: transparent;
  color: var(--muted, #68635a);
  cursor: pointer;
  font: 700 0.7rem/1.3 Arial, Helvetica, sans-serif;
  letter-spacing: 0.01em;
}
.chart-table-toggle:hover, .chart-table-toggle:focus-visible,
.chart-csv-button:hover, .chart-csv-button:focus-visible,
.chart-expand-button:hover, .chart-expand-button:focus-visible {
  border-color: var(--accent-dark, #61221a);
  background: color-mix(in srgb, var(--accent-dark, #61221a) 10%, var(--surface, transparent));
  color: var(--ink);
}
.chart-container .data-table-scroll { margin-top: 12px; }
/* VoteStats.chartTable: the table toggle and CSV button every chart carries. */
.chart-actions { display: flex; flex-wrap: wrap; gap: 6px; }
.chart-table-panel .data-table-scroll { margin-top: 10px; }
/* VoteStats.outcomeBreakdown: summary cards that filter the chart below them, and its captions. */
.is-filter[role="button"] { position: relative; cursor: pointer; transition: border-color .12s, transform .12s; }
.is-filter[role="button"]:hover { border-color: var(--accent-dark, #61221a); transform: translateY(-1px); }
.is-filter[role="button"]:focus-visible { outline: 3px solid var(--blue, #315b68); outline-offset: 2px; }
.is-filter[aria-pressed="true"] { border-color: var(--accent-dark, #61221a); box-shadow: inset 0 0 0 2px var(--accent-dark, #61221a); }
/* The card's "Chart this" / "Charted" hint: a small pill in the site's chip style, so every
   filter card reads as a button, not only the pressed one. */
.is-filter .filter-hint {
  display: inline-flex; align-items: center; gap: 4px; width: fit-content; margin-top: 8px; padding: 2px 8px; white-space: nowrap;
  border: 1px solid var(--rule, #b8ae9d); border-radius: 999px; color: var(--muted);
  font: 700 .62rem/1.4 Arial, Helvetica, sans-serif; letter-spacing: .05em; text-transform: uppercase;
}
.is-filter .filter-hint::after { content: "\25BE"; font-size: .8em; }
.is-filter:hover .filter-hint, .is-filter:focus-visible .filter-hint { border-color: var(--accent-dark, #61221a); color: var(--accent-dark, #61221a); }
.is-filter[aria-pressed="true"] .filter-hint { border-color: var(--accent-dark, #61221a); background: var(--accent-dark, #61221a); color: var(--paper, #f5f0e6); }
.is-filter[aria-pressed="true"] .filter-hint::after { content: "\2713"; }
@media (prefers-reduced-motion: reduce) { .is-filter[role="button"] { transition: none; } .is-filter[role="button"]:hover { transform: none; } }
.outcome-breakdown { margin: 12px 0 16px; }
.outcome-breakdown h3, .outcome-breakdown h4 { margin: 0; font-size: 1rem; }
.outcome-breakdown .chart-caption, .profile-chart .chart-caption { margin: 4px 0 0; color: var(--muted); font: .8rem/1.45 Arial, Helvetica, sans-serif; }
.outcome-breakdown .chart-table-panel, .profile-chart .chart-table-panel { margin-bottom: 14px; }

/* --------------------------------------------------------------------------------------------
 * 7. Empty / collecting state, caveat note.
 */
.empty-state {
  padding: 14px 16px;
  border: 1px dashed var(--rule, #b8ae9d);
  color: var(--muted);
  font: 0.88rem/1.5 Georgia, "Times New Roman", serif;
}
.empty-state.is-collecting { border-style: dotted; }
.caveat-note {
  margin: 10px 0 0;
  padding: 8px 10px;
  border-left: 3px solid var(--accent, #7d2e24);
  background: var(--paper, #f5f0e6);
  color: var(--muted);
  font: 0.78rem/1.5 Arial, Helvetica, sans-serif;
}

/* Figure switch: one checkbox that turns a figure or series on or off beside the figures it sits
 * with (the /alpr/tracker switches for figures that must be off by default). The label says what
 * it shows; the page keeps any required wording in its own markup. docs/design-guidelines.md. */
.figure-switch {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 12px 0 0;
  color: var(--ink, #1f1c17);
  font: 0.86rem/1.4 Arial, Helvetica, sans-serif;
  cursor: pointer;
}
.figure-switch input { width: 18px; height: 18px; min-height: 0; margin: 0; padding: 0; accent-color: var(--accent-dark, #7a2e24); cursor: pointer; }
.figure-switch[hidden] { display: none; }

/* Map base: the shared three-way choice under every map (src/map-basemap.js builds it): None,
 * Street lines, OpenStreetMap. Radios in a row that wraps on a phone; 24px tap targets.
 * docs/design-guidelines.md, "Map base". */
.map-base {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 14px;
  margin: 12px 0 0;
  padding: 0;
  border: 0;
  color: var(--ink, #1f1c17);
  font: 0.86rem/1.4 Arial, Helvetica, sans-serif;
}
/* Screen-reader-only text (a legend a page already labels, rule 32). */
.visually-hidden { position: absolute !important; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0; }
.map-base legend { float: left; margin: 0 4px 0 0; padding: 0; font-weight: 700; }
.map-base label { display: inline-flex; align-items: center; gap: 6px; min-height: 24px; cursor: pointer; }
.map-base input { width: 16px; height: 16px; min-height: 0; margin: 0; padding: 0; accent-color: var(--focus, #61221a); cursor: pointer; }

@media (max-width: 700px) {
  .stat-row { grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); }
  .disclosure-card > summary { grid-template-columns: 1fr; }
  .disclosure-card .disclosure-teaser { text-align: left; }
}

/* --------------------------------------------------------------------------------------------
 * Stack list: one run of similar cards (a roster, a card grid) that scales with the header's
 * "Detail: Brief | Full" control (site-shell.js sets :root[data-detail]). Brief, the default,
 * joins the run into one bordered list of compact rows divided by hairlines; Full keeps each item
 * as its own card. The page keeps its own card markup and adds only `class="stack-list"` to the
 * container. Brief is written as :not([data-detail="full"]) so the page reads Brief before the
 * deferred shell sets the attribute. docs/data-components.md section 14, "Density cases".
 */
/* Full (and any page without its own grid): separate cards with a gap. A page's own grid rule wins. */
.stack-list { display: grid; gap: 12px; }
:root:not([data-detail="full"]) .stack-list {
  gap: 0;
  border: 1px solid var(--rule, #b8ae9d);
  background: var(--surface, #fffdf7);
}
:root:not([data-detail="full"]) .stack-list > * {
  margin: 0;
  border: 0;
  border-bottom: 1px solid var(--rule, #b8ae9d);
  background: transparent;
  box-shadow: none;
}
:root:not([data-detail="full"]) .stack-list > :last-child { border-bottom: 0; }
/* A fold (data-show-more) copies the list's classes onto its body: the body sits inside the outer
 * list's border, so it draws none of its own. */
:root:not([data-detail="full"]) .stack-list .stack-list { border: 0; background: transparent; }
/* An opened row keeps the open-card cue as an inset rule, so the row doesn't shift sideways. */
:root:not([data-detail="full"]) .stack-list > [open] { box-shadow: inset 4px 0 0 var(--role-accent, var(--accent)); }
/* Member names in a Brief row carry their tags on the name's own line: the term countdown and a
 * "Former member" badge stay; "Current council"/"Current mayor" (the row's role already says it)
 * and the term's month range (Full, and the profile, show it) drop out. A grid of name links that
 * stays a grid (not a stack list) takes `.tag-compact` for the same tag treatment. */
:root:not([data-detail="full"]) :is(.stack-list, .tag-compact) .member-tags { display: inline-flex; margin: 0 0 0 8px; vertical-align: 0.15em; }
:root:not([data-detail="full"]) :is(.stack-list, .tag-compact) .member-status.is-current,
:root:not([data-detail="full"]) :is(.stack-list, .tag-compact) .term-dates { display: none; }

/* --------------------------------------------------------------------------------------------
 * 8. Charts (src/charts.js, window.Charts): reference/context lines, shaded gap bands, the
 * generic cumulative-series line/dot (role- or series-colored), the detail card the shared
 * hover/keyboard/tap readout pins open, and the Chart | Table + CSV controls row. Reuses
 * site-shell.css's .chart-grid/.chart-axis/.chart-tooltip*/.chart-dot/.chart-line/.range-*/
 * .multiples-*/.series-* primitives; this section adds only what those don't already define.
 */
/* A labeled dotted/dashed context line sharing a chart's own axis (e.g. "in tenure", "meetings
 * held" drawn behind a member's own cumulative line) -- always muted, never a data series' own
 * color, so it never competes with the colored line it gives context to. */
.chart-reference-line {
  fill: none;
  stroke: var(--muted, #68635a);
  stroke-width: 1.4;
  opacity: 0.85;
}
.chart-reference-label {
  fill: var(--muted, #68635a);
  font: 11px Arial, Helvetica, sans-serif;
}
/* A shaded band marking a gap in the record (no readable minutes, no data that period) --
 * distinct from .chart-gap/.cumulative-gap only in name, so charts.js does not depend on
 * vote-stats.js's class names; same visual weight. */
.chart-gap-band { fill: var(--rule, #b8ae9d); opacity: 0.3; }
/* An era zone: a lighter band over the years before a chart's declared `era.cut` (the pre-2020
 * Council record), tagged at its top-left; the era's note goes in the chart's caption. */
.chart-era-zone { fill: var(--rule, #b8ae9d); opacity: 0.12; }
.chart-era-tag { fill: var(--muted, #68635a); font: italic 11px Arial, Helvetica, sans-serif; }
/* The cumulative chart's own series: a role- or series-colored step line and its single-meeting
 * dots (`--series` is set inline for a role color, or via the shared .series-1/2/3 classes). */
path.cumulative-line { fill: none; stroke: var(--series, var(--accent-dark, #7a2e24)); stroke-width: 2; }
circle.cumulative-line { fill: var(--series, var(--accent-dark, #7a2e24)); stroke: var(--chart-surface, var(--paper, #fffdf8)); stroke-width: 1.5; }
/* Stacked bar segments get a 2px surface-colored seam so adjacent fills never bleed together. */
.chart-bar-segment { stroke: var(--chart-surface, var(--paper, #fffdf8)); stroke-width: 2; }

/* The detail card: a hover/focus readout that PINS open on click, Enter, or Space -- a real,
 * focusable, keyboard-closable panel (role="dialog"), not just a tooltip that vanishes on blur. */
.chart-detail-card {
  position: absolute;
  z-index: 6;
  min-width: 190px;
  max-width: 300px;
  padding: 10px 30px 10px 12px;
  border: 1px solid var(--rule, #b8ae9d);
  background: var(--paper, #fffdf8);
  box-shadow: 0 8px 22px rgba(0, 0, 0, 0.22);
  color: var(--ink, #1f1a14);
  font: 0.8rem/1.5 Arial, Helvetica, sans-serif;
}
.chart-detail-card:focus-visible, .chart-detail-card:focus { outline: 3px solid var(--accent-dark, #7a2e24); outline-offset: 2px; }
/* On touch, a card lets taps through to the chart under it (its links and Close button still
 * take taps), so a card covering the next cell never swallows the reader's next tap. */
/* A card that is only showing (hover or keyboard, not pinned) never takes the pointer: one that
   appeared under the cursor between press and release swallowed the click, so the first click
   after load could open nothing (/pay#history, 2026-09-28). A pinned card takes clicks again. */
.chart-detail-card:not(.is-pinned) { pointer-events: none; }
.chart-hover.is-touch .chart-detail-card { pointer-events: none; }
.chart-hover.is-touch .chart-detail-card a, .chart-hover.is-touch .chart-detail-card button { pointer-events: auto; }
/* placeCard caps a tall card's height to its figure (and 60% of a phone screen); it scrolls
   inside, and a pinned card that scrolls takes the pointer so a finger can scroll it. */
.chart-detail-card, .chart-tooltip { overflow-y: auto; overscroll-behavior: contain; }
.chart-detail-card.is-pinned.is-scrollable, .chart-hover.is-touch .chart-detail-card.is-pinned.is-scrollable { pointer-events: auto; }
.chart-detail-card.is-pinned { border-color: var(--accent-dark, #7a2e24); box-shadow: 0 10px 26px rgba(0, 0, 0, 0.3); }
.chart-detail-title { margin: 0 0 4px; color: var(--muted, #68635a); font-weight: 700; }
.chart-detail-figure { margin: 2px 0; }
.chart-detail-link { margin: 6px 0 0; }
.chart-detail-link a, .chart-detail-card .chart-tooltip-action { color: var(--accent-dark, #7a2e24); font-weight: 700; }
.chart-detail-close {
  position: absolute;
  top: 4px;
  right: 4px;
  padding: 2px 6px;
  border: 0;
  background: transparent;
  color: var(--muted, #68635a);
  font: 700 0.68rem Arial, Helvetica, sans-serif;
  cursor: pointer;
}
.chart-detail-close:hover, .chart-detail-close:focus-visible { color: var(--ink, #1f1a14); }

/* Chart | Table toggle and CSV-download controls row under every Charts.figure(). */
.chart-figure-controls { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.chart-figure-controls .chart-table-toggle, .chart-figure-controls .chart-csv-button { margin-top: 0; }

/* --------------------------------------------------------------------------------------------
 * 9. Facets (src/facets.js, window.Facets): the selected state must never be color alone -- a
 * pressed chip gets a check mark (a real child whose display flips) on top of the existing
 * .chip[aria-pressed="true"] tint/border from section 1, so it reads unmistakably in both
 * themes and under a color-vision test, not just to a full-color viewer comparing hues.
 */
.chip-check { display: none; margin-right: 3px; }
.chip[aria-pressed="true"] .chip-check {
  display: inline-block;
  color: var(--ink, #1f1a14);
  font: 700 0.72rem/1 Arial, Helvetica, sans-serif;
}
.chip[aria-pressed="true"] .chip-check::before { content: "\2713"; }
.chip:disabled { opacity: 0.4; cursor: default; }
/* Overflow: Facets.mount hides the chips past two wrapped lines behind a "More (N)" toggle
 * (src/facets.js fitRow). `hidden` must win over `.chip`'s own display. The toggle is a chip with
 * no count or check, set apart by a dashed border so it never reads as a filter value. */
.chip[hidden] { display: none; }
.chip.chip-more { border-style: dashed; font-weight: 700; }
.chip-row { display: flex; flex-wrap: wrap; align-items: center; gap: calc(var(--space-2, 8px) * var(--density, 1)); margin: calc(var(--space-2, 10px) * var(--density, 1)) 0; }
.chip-row-label { margin-right: 2px; color: var(--muted, #68635a); font: 700 var(--step--2, 0.68rem) var(--font-ui, Arial, Helvetica, sans-serif); letter-spacing: 0.08em; text-transform: uppercase; }

/* Section 10 (row drilldown) has no rules here: src/row-drilldown.js (landed independently by a
 * /local page agent before this release's own draft of the same idea -- see docs/data-
 * components.md section 10) injects its own <style id="row-drilldown-style"> at runtime rather
 * than reading this stylesheet, so this file adds nothing for it. */

@media (max-width: 700px) {
  .chart-detail-card { max-width: calc(100vw - 48px); }
}

/* --------------------------------------------------------------------------------------------
 * 13. DataView (src/data-view.js, window.DataView): the controls strip, its own form list, the
 * heatmap/list/stat/timeline forms it draws directly (every other form is a Charts.* kind, styled
 * already), and its notes panel (reuses .caveat-more/.caveat-lead/.caveat-cue/.caveat-body as-is,
 * so site-shell.js's print-open-all and hash-reveal handling apply with no new code). No new
 * colors: series/role coloring reuses section 1's --role-* tokens and the shared --series-1/2/3
 * the way src/charts.js already does; the heatmap ramp below is the one new set of steps this
 * release adds, and it is a sequential (single-hue, light -> dark) ramp per the dataviz skill,
 * not a categorical palette, so it carries no identity meaning of its own.
 */
.dataview .chart-caption { margin-bottom: 4px; }
.dataview-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 12px;
  margin: 8px 0 6px;
}
.dataview-control-group { display: flex; flex-wrap: wrap; gap: 4px; }
/* The Form switch reads as one segmented control, not five separate buttons. */
.dataview-form-control { gap: 0; }
.dataview-form-control .dataview-control-btn + .dataview-control-btn { margin-left: -1px; }
.dataview-form-control .dataview-control-btn:first-child { border-radius: 4px 0 0 4px; }
.dataview-form-control .dataview-control-btn:last-child { border-radius: 0 4px 4px 0; }
.dataview-form-control .dataview-control-btn[aria-pressed="true"] { position: relative; z-index: 1; }
/* The Compare-with group names itself in text (its buttons are place names, not verbs). */
.dataview-control-label {
  align-self: center;
  color: var(--muted, #68635a);
  font: 700 0.68rem/1.3 Arial, Helvetica, sans-serif;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}
/* Years (rule 23): a compact native select in the view bar and, inside Options, a From/To pair.
 * Native selects keep the phone's own picker and need no extra widget code. */
.dataview-range-control, .dataview-custom-range label { display: inline-flex; align-items: center; gap: 6px; }
.dataview-range-select {
  min-height: 24px;
  max-width: 100%;
  padding: 2px 6px;
  border: 1px solid var(--rule, #b8ae9d);
  border-radius: 4px;
  background: var(--surface, transparent);
  color: var(--ink, #1f1a14);
  font: 700 0.7rem/1.3 Arial, Helvetica, sans-serif;
}
.dataview-range-select:hover, .dataview-range-select:focus-visible { border-color: var(--accent-dark, #61221a); }
.dataview-custom-range { gap: 10px; }
/* The view picker: buttons on a wide figure, one dropdown on a narrow one (rule 23). The controls
 * strip is its own size container (never the figure: containment would re-anchor hover cards), so a
 * split or quarter screen gets the dropdown too. */
.dataview-controls { container-type: inline-size; container-name: dataview; }
.dataview-form-switch { display: contents; }
.dataview-form-select { display: none; }
@container dataview (max-width: 479px) {
  .dataview-form-switch > .dataview-form-control { display: none; }
  .dataview-form-switch > .dataview-form-select { display: inline-flex; }
}
.dataview-range-note { flex-basis: 100%; color: var(--muted, #68635a); font: 0.72rem/1.35 Arial, Helvetica, sans-serif; }
.dataview-control-btn {
  min-height: 24px;
  padding: 3px 8px;
  border: 1px solid var(--rule, #b8ae9d);
  background: var(--surface, transparent);
  color: var(--ink, #1f1a14);
  cursor: pointer;
  font: 700 0.7rem/1.3 Arial, Helvetica, sans-serif;
}
.dataview-control-btn:hover, .dataview-control-btn:focus-visible {
  border-color: var(--accent-dark, #61221a);
  background: color-mix(in srgb, var(--accent-dark, #61221a) 10%, var(--surface, transparent));
}
.dataview-control-btn[aria-pressed="true"] {
  border-color: var(--accent-dark, #7a2e24);
  background: color-mix(in srgb, var(--accent-dark, #7a2e24) 14%, var(--surface, transparent));
  /* Explicit: a page's generic button[aria-pressed="true"] { color: var(--paper) } (e.g. /people)
     would otherwise paint paper-colored text on this light tint (axe measured 1.16:1). */
  color: var(--ink, #20211e);
  font-weight: 700;
}
/* A pressed control is never color alone: it also gets a bold weight and border above, plus this
 * check glyph, exactly like a Facets chip (section 9). */
.dataview-control-btn[aria-pressed="true"]::before { content: "\2713\00a0"; }
/* The picked form carries an accent underline, so the segmented switch reads at a glance. */
.dataview-form-control .dataview-control-btn[aria-pressed="true"] { box-shadow: inset 0 -3px 0 var(--accent-dark, #61221a); }
/* Series chips: small pills with the series' own swatch; the swatch carries the color, so the
 * chip needs no thick colored edge. */
.dataview-series-chip { min-height: 24px; padding: 3px 8px; border-left-width: 1px; border-radius: 999px; font-size: 0.7rem; letter-spacing: 0; }
/* An on series chip stays quiet: the check and the swatch say it is on; the border and fill
 * only darken a step, so a row of chips never outweighs the lines they control. */
.dataview-series-chip[aria-pressed="true"] { border-color: var(--rule, #b8ae9d); background: var(--surface, transparent); }
.dataview-series-chip[aria-pressed="false"] { opacity: .75; border-style: dashed; }
.dataview-series-chip:hover, .dataview-series-chip:focus-visible { border-color: var(--accent-dark, #61221a); }
/* Options: the advanced controls (scale, sort, clipping, compare) one click away. */
.dataview-more > summary {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  box-sizing: border-box;
  min-height: 24px;
  padding: 3px 8px;
  border: 1px dashed var(--rule, #b8ae9d);
  border-radius: 999px;
  color: var(--muted, #68635a);
  cursor: pointer;
  font: 700 0.7rem/1.3 Arial, Helvetica, sans-serif;
  list-style: none;
}
.dataview-more > summary::-webkit-details-marker { display: none; }
.dataview-more > summary::before { content: "\2699"; font-weight: 400; }
.dataview-more > summary:hover, .dataview-more > summary:focus-visible { border-color: var(--accent-dark, #7a2e24); color: var(--ink, #1f1a14); }
.dataview-more[open] > summary { border-style: solid; color: var(--ink, #1f1a14); }
.dataview-more-body { display: flex; flex-wrap: wrap; gap: 6px 12px; margin-top: 6px; }
/* Under the figure: the source locator and the one "Cite & download" control, in small type. */
.dataview-foot { margin-top: 4px; }
.dataview-foot .comment-cite { margin: 0; font-size: 0.72rem; }
/* The numbered-line key folds behind a one-line summary (VoteStats.markerFold, Charts). */
.chart-marker-fold { margin: 4px 0 0; }
.chart-marker-fold > summary { cursor: pointer; color: var(--muted, #68635a); font: 700 0.72rem/1.4 Arial, Helvetica, sans-serif; }
.chart-marker-fold > summary:hover, .chart-marker-fold > summary:focus-visible { color: var(--ink, #1f1a14); }
.chart-marker-fold[open] > .chart-marker-key { margin-top: 4px; }
/* Series chips keep their own height and read left to right; they never stretch to the tallest
 * chip in the row or center a wrapped name. */
.dataview-series-toggle { align-items: flex-start; }
.dataview-series-toggle .chip { text-align: left; }
/* Many-series fold reuses src/page-contents.js's sitewide data-show-more/.show-more, not a second
 * disclosure; no rule needed here beyond letting a <details class="show-more"> sit as a flex item. */

.dataview-figure { margin-top: 8px; }
.dataview-list { list-style: none; margin: 8px 0; padding: 0; }
.dataview-list li {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 12px;
  padding: 6px 0;
  border-bottom: 1px solid var(--rule, #b8ae9d);
  font: 0.85rem/1.5 Arial, Helvetica, sans-serif;
}
.dataview-list-label { font-weight: 700; min-width: 9em; }
.dataview-list-value { color: var(--muted, #68635a); }
.dataview-stat { max-width: 220px; }
.dataview-box-summary { font: 0.9rem/1.6 Georgia, "Times New Roman", serif; }

/* Sequential ramp for every value shade (the heatmap form, table heat cells, the tracker's nearby
 * map): monotone lightness (light = low, dark = high in light mode; the same order in dark mode),
 * never a rainbow, never reused for categorical identity, and never gold (design rule 27: gold
 * marks Altoona only; editor, 2026-10-03, "visualability is paramount before design"). Two
 * schemes, picked in Settings > Value colors (html[data-value-colors], site-shell.js):
 *   neutral (the default here): one gray ramp, no good-or-bad reading in a plain count;
 *   greenred: green (low) through a pale neutral to red (high), still monotone in lightness so it
 *   reads in grayscale and for red-green color blindness.
 * Each step keeps the ink split below (light mode: steps 1-2 dark ink, 3-5 light; dark mode:
 * steps 1-4 black, 5 the light --ink) at 4.5:1 or better, and the low end clears 2:1 against the
 * chart surface so it never reads as blank (tests/test_heat_tokens.py checks every scheme). A gap
 * cell (no data) is hatched, not colored, so "no data" is never confused with "low value". */
:root {
  --dataview-heat-1: #b9b2a4;
  --dataview-heat-2: #9a9487;
  --dataview-heat-3: #6f6a61;
  --dataview-heat-4: #504c45;
  --dataview-heat-5: #2e2c28;
}
html[data-theme="dark"] {
  --dataview-heat-1: #e2ddd2;
  --dataview-heat-2: #c3bdb0;
  --dataview-heat-3: #a39d90;
  --dataview-heat-4: #888275;
  --dataview-heat-5: #5c5850;
}
html[data-value-colors="greenred"] {
  --dataview-heat-1: #86c27c;
  --dataview-heat-2: #b3a68a;
  --dataview-heat-3: #b84a3d;
  --dataview-heat-4: #922a20;
  --dataview-heat-5: #5e1610;
}
html[data-theme="dark"][data-value-colors="greenred"] {
  --dataview-heat-1: #9fd496;
  --dataview-heat-2: #cbbfa6;
  --dataview-heat-3: #e58a7c;
  --dataview-heat-4: #d9604f;
  --dataview-heat-5: #a8352a;
}
/* Count text inside a heatmap cell: dark on the lighter steps, light on the darker ones, each
 * pair at least 4.5:1 against its step in its own mode (step 0 is an unshaded "none" cell). */
:root {
  --dataview-heat-ink-dark: var(--ink, #20211e);
  --dataview-heat-ink-light: #fffdf8;
}
.dataview-heat-value { font: 700 11px Arial, Helvetica, sans-serif; pointer-events: none; }
.dataview-heat-ink-0, .dataview-heat-ink-1, .dataview-heat-ink-2 { fill: var(--dataview-heat-ink-dark); }
.dataview-heat-ink-3, .dataview-heat-ink-4, .dataview-heat-ink-5 { fill: var(--dataview-heat-ink-light); }
html[data-theme="dark"] .dataview-heat-ink-1, html[data-theme="dark"] .dataview-heat-ink-2,
html[data-theme="dark"] .dataview-heat-ink-3, html[data-theme="dark"] .dataview-heat-ink-4 { fill: #000; }
html[data-theme="dark"] .dataview-heat-ink-0 { fill: var(--ink); }
html[data-theme="dark"] .dataview-heat-ink-5 { fill: var(--ink); }
.dataview-heatmap svg { display: block; max-width: 100%; height: auto; overflow: visible; }
.dataview-timeline svg { display: block; width: 100%; height: auto; overflow: visible; }
.dataview-heat-cell { stroke: var(--chart-surface, var(--paper, #fffdf8)); stroke-width: 2; }
.dataview-heat-step-0 { fill: transparent; stroke: var(--rule, #b8ae9d); stroke-width: 1; }
.dataview-heat-step-1 { fill: var(--dataview-heat-1); }
.dataview-heat-step-2 { fill: var(--dataview-heat-2); }
.dataview-heat-step-3 { fill: var(--dataview-heat-3); }
.dataview-heat-step-4 { fill: var(--dataview-heat-4); }
.dataview-heat-step-5 { fill: var(--dataview-heat-5); }
.dataview-heat-cell.is-gap {
  fill: transparent;
  stroke: var(--rule, #b8ae9d);
  stroke-dasharray: 3 2;
}
/* Heat off (site-wide preference, below): every non-gap cell goes plain -- the same bordered,
 * unfilled look step 0 already uses for "no value", so a reader can tell a real zero from "not
 * shown" only by its printed number, never by a missing fill only. The count text stays legible
 * by falling back to the ordinary ink color instead of whichever step's ink class it carries. */
:root[data-heat="off"] .dataview-heat-step-1, :root[data-heat="off"] .dataview-heat-step-2,
:root[data-heat="off"] .dataview-heat-step-3, :root[data-heat="off"] .dataview-heat-step-4,
:root[data-heat="off"] .dataview-heat-step-5 { fill: transparent; stroke: var(--rule, #b8ae9d); stroke-width: 1; }
:root[data-heat="off"] .dataview-heat-ink-0, :root[data-heat="off"] .dataview-heat-ink-1,
:root[data-heat="off"] .dataview-heat-ink-2, :root[data-heat="off"] .dataview-heat-ink-3,
:root[data-heat="off"] .dataview-heat-ink-4, :root[data-heat="off"] .dataview-heat-ink-5 { fill: var(--ink, #20211e); }
.dataview-timeline .range-bar { fill: var(--series-1, #2a78d6); }

/* 13a. Site-wide "Heat" preference (docs/data-components.md section "Heat"): a reader's saved
 * on/off choice (site-shell.js, deflock-heat/?heat=on|off, same pattern as Detail) for
 * value-intensity coloring anywhere a figure has a natural ramp -- the DataView heatmap form (the
 * validated --dataview-heat-1..5 ramp above), the DataView table view's numeric cells, and the
 * /alpr/tracker nearby-jurisdictions map. `--heat-1..5` alias that SAME ramp under a plain name so
 * a plain HTML background-color (a table <td>, an SVG map fill) can share it -- one validated
 * ramp, never a second palette to keep in sync. Default is ON (`:root:not([data-heat="off"])`
 * matches when the attribute is absent, so a page with no JS yet, or before site-shell.js runs,
 * still shows color -- the toggle only ever removes it). */
:root {
  --heat-1: var(--dataview-heat-1);
  --heat-2: var(--dataview-heat-2);
  --heat-3: var(--dataview-heat-3);
  --heat-4: var(--dataview-heat-4);
  --heat-5: var(--dataview-heat-5);
  --heat-ink-dark: var(--dataview-heat-ink-dark);
  --heat-ink-light: var(--dataview-heat-ink-light);
}
.heat-cell { background: transparent; }
/* Same dark/light ink split as the SVG .dataview-heat-ink-N rules above (steps 0-2 dark ink,
 * steps 3-5 light ink), so the two never drift apart. */
:root:not([data-heat="off"]) .heat-step-1 { background: var(--heat-1); color: var(--heat-ink-dark); }
:root:not([data-heat="off"]) .heat-step-2 { background: var(--heat-2); color: var(--heat-ink-dark); }
:root:not([data-heat="off"]) .heat-step-3 { background: var(--heat-3); color: var(--heat-ink-light); }
:root:not([data-heat="off"]) .heat-step-4 { background: var(--heat-4); color: var(--heat-ink-light); }
:root:not([data-heat="off"]) .heat-step-5 { background: var(--heat-5); color: var(--heat-ink-light); }
html[data-theme="dark"]:not([data-heat="off"]) .heat-step-1,
html[data-theme="dark"]:not([data-heat="off"]) .heat-step-2,
html[data-theme="dark"]:not([data-heat="off"]) .heat-step-3,
html[data-theme="dark"]:not([data-heat="off"]) .heat-step-4 { color: #000; }
/* A number is never the only reader of a heat cell's value -- it is always printed (this rule
 * only sets the fill and its matching ink color); .heat-cell keeps its own border so the cell's
 * shape survives forced-colors (below) or a step-1 fill close to the surface color. */
.heat-cell[class*="heat-step-"] { border: 1px solid var(--rule, #b8ae9d); }

.dataview-compare { display: flex; gap: 16px; margin-top: 10px; }
.dataview-notes-host .caveat-more { margin-top: 10px; }
.dataview-cite-fallback { margin-top: 8px; }
.dataview-cite-fallback summary { cursor: pointer; font: 700 0.72rem Arial, Helvetica, sans-serif; }

@media (max-width: 480px) {
  .dataview-controls { gap: 6px 8px; }
  .dataview-control-btn { padding: 3px 6px; font-size: 0.66rem; }
}

@media print {
  .dataview-controls, .dataview-foot .cite-disclosure { display: none; }
  /* Charts.figure()'s own bare chart chrome (pre-DataView; still the only wrapper on /people's
   * overview band, /votes' cumulative chart, /alpr/tracker's two charts, and /design's demos):
   * the toggle and download button are inert on paper and were never covered by the
   * .dataview-controls rule above. Hiding them never hides content -- whichever form (chart or
   * table) is already on screen keeps rendering; .chart-expand-button is deliberately left alone
   * since it gates a folded data-show-more list with no other print-time way to reveal it. */
  .chart-table-toggle, .chart-csv-button { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .dataview-control-btn { transition: none; }
}

/* forced-colors (Windows High Contrast): keep a visible border on every control regardless of the
 * system's flattened palette, and give heat cells a border so a shape/edge still separates them
 * when fill colors are overridden -- meaning never depends on the ramp alone. */
@media (forced-colors: active) {
  .dataview-control-btn { border: 1px solid ButtonText; forced-color-adjust: none; background: ButtonFace; color: ButtonText; }
  .dataview-control-btn[aria-pressed="true"] { border-width: 2px; }
  .dataview-heat-cell { forced-color-adjust: none; stroke: CanvasText; }
  /* No fill at all under forced-colors -- a heat step's chosen color is meaningless once the
   * system flattens the palette, so every heat surface (table cell, map fill) falls back to no
   * fill plus a visible border instead of an unreadable or misleading color. */
  .heat-cell, .nearby-map-city[class*="heat-step-"] path { forced-color-adjust: none; background: Canvas; fill: Canvas; color: CanvasText; border-color: CanvasText; stroke: CanvasText; }
}

/* A table caption spans the whole table, so inside a sideways-scrolling wrapper on a phone its
 * text ran past the visible edge and was cut off. The table's own wrapper becomes a size
 * container and the caption wraps to the width a reader can actually see. */
main div:has(> table > caption) { container-type: inline-size; }
main div:has(> table > caption) > table > caption { max-width: 100cqi; box-sizing: border-box; }

/* A sideways-scrolling table wrapper (.table-wrap, .data-table-scroll) had a working
 * overflow-x:auto region but nothing telling a reader more columns exist off-screen at first
 * paint -- the wrapper just looked clipped (/pay's contract tables, reported 2026-09-28). CSS-only
 * "scroll shadow": two cover gradients scroll WITH the content (hiding the shadow behind them
 * near an edge with nothing more to see) and two shadow gradients stay fixed under them, so a
 * shadow only actually shows on the side that still has unscrolled content -- no JS, no scroll
 * listener, keeps working after Heat/theme/Detail state changes it wasn't written to know about. */
main div:has(> table > caption) {
  background:
    linear-gradient(to right, var(--surface, #fffdf7) 30%, transparent),
    linear-gradient(to left, var(--surface, #fffdf7) 30%, transparent) 100% 0,
    linear-gradient(to right, color-mix(in srgb, var(--ink, #20211e) 16%, transparent), transparent),
    linear-gradient(to left, color-mix(in srgb, var(--ink, #20211e) 16%, transparent), transparent) 100% 0;
  background-repeat: no-repeat;
  background-color: var(--surface, #fffdf7);
  background-size: 32px 100%, 32px 100%, 10px 100%, 10px 100%;
  background-attachment: local, local, scroll, scroll;
  background-position: 0 0, 100% 0, 0 0, 100% 0;
}
@media (prefers-contrast: more), (forced-colors: active) {
  /* A faint gradient can vanish under a forced palette or a high-contrast override; the region's
   * own focus ring and border still carry the "more content" affordance there. */
  main div:has(> table > caption) { background: var(--surface, #fffdf7); }
}

/* Glossary tooltips (docs/data-components.md §14, src/glossary.js): the first reader-visible use
 * of a site term gets a dotted underline (never color alone) and a keyboard-reachable focus ring;
 * clicking, tapping, or tab-focusing it opens a short plain-language popover right after it. */
.glossary-term {
  border-bottom: 1px dotted var(--muted, #68635a);
  text-decoration: none;
  cursor: help;
  font-style: normal;
}
.glossary-term:hover,
.glossary-term:focus-visible {
  border-bottom-style: solid;
  background: var(--accent-dark, #61221a);
  color: var(--paper, #f5f0e6);
}
.glossary-popover {
  display: inline-block;
  max-width: 320px;
  margin-left: 6px;
  padding: 8px 10px;
  border: 1px solid var(--rule, #b8ae9d);
  background: var(--paper, #f5f0e6);
  box-shadow: 0 8px 22px rgba(52, 43, 29, 0.16);
  font: 400 0.8rem/1.35 Georgia, "Times New Roman", serif;
  vertical-align: top;
}
@media print { .glossary-popover { display: none; } }

/* Dense-context chip (docs/data-components.md §14, ChartNotes.contextChip): a compact stand-in for
 * a repeated "As of <date>, source: <feed>" sentence under a section heading. Activating it opens
 * that section's own .caveat-more/ChartNotes panel rather than duplicating its text. */
.data-context-chip {
  display: inline-flex;
  align-items: baseline;
  gap: 0.4em;
  margin: 2px 0 10px;
  padding: 4px 10px;
  border: 1px solid var(--rule, #b8ae9d);
  background: transparent;
  color: var(--muted, #68635a);
  cursor: pointer;
  font: 700 0.66rem/1.3 Arial, Helvetica, sans-serif;
  letter-spacing: 0.03em;
  text-transform: none;
}
.data-context-chip:hover,
.data-context-chip:focus-visible {
  color: var(--ink, inherit);
  border-color: var(--ink, #34291d);
}
.data-context-chip .data-context-cue { text-decoration: underline; text-underline-offset: 2px; }

/* --------------------------------------------------------------------------------------------
 * Patterns for upcoming pages (docs/design-guidelines.md rule 13; live on /design). Every one
 * sits in a .figure-frame and uses only the existing tokens: no new colors.
 */
/* Small-multiples grid: one Charts.figure() per measure, each on its own scale. */
.chart-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
  gap: 12px;
}
.chart-grid > .chart-container { margin: 0; padding: 10px 12px; border: 1px solid var(--rule, #b8ae9d); }
/* Sparkline inside a stat tile (VoteStats.sparkline, given the class stat-spark). */
.stat-tile .stat-spark { display: block; max-width: 100%; height: auto; margin-top: 8px; overflow: visible; }
/* Presence dot strip: a compact table, one glyph per period (up to 24), meaning in text too. */
.presence-strip th[scope="col"] { padding: 4px 3px; font-size: 0.66rem; white-space: nowrap; }
.presence-strip th[scope="col"] abbr { text-decoration: none; }
.presence-strip td { position: relative; padding: 4px 3px; text-align: center; line-height: 1; }
.presence-strip td span[aria-hidden] { font-size: 0.95rem; }
.presence-strip td.is-present, .presence-strip td.is-late { color: var(--role-accent, var(--ink)); }
.presence-strip td.is-absent { color: var(--ink); }
.presence-strip td.is-unlisted, .presence-strip td.is-norecord { color: var(--muted); }
.presence-strip td.is-norecord { background: repeating-linear-gradient(135deg, transparent 0 4px, color-mix(in srgb, var(--rule, #b8ae9d) 45%, transparent) 4px 5px); }
.presence-strip .strip-text { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.presence-strip-key { display: flex; flex-wrap: wrap; gap: 4px 14px; margin: 8px 0; color: var(--muted); font: 0.76rem/1.4 Arial, Helvetica, sans-serif; }
.presence-strip-key b { color: var(--ink); font-weight: 400; }
/* Before/after delta table: the sign carries the direction; no good/bad color. */
.delta-table td.num { font-variant-numeric: tabular-nums; white-space: nowrap; }
.delta-table .delta-date { display: block; color: var(--muted); font-size: 0.72rem; }
.delta-table td.delta { font-weight: 700; }
/* Annotated timeline: numbered markers, one record per step. */
.timeline { margin: 4px 0 0; padding: 0; list-style: none; }
.timeline > li { position: relative; display: grid; grid-template-columns: 26px minmax(0, 1fr); gap: 0 12px; padding-bottom: 14px; }
.timeline > li:not(:last-child)::before { content: ""; position: absolute; top: 24px; bottom: 0; left: 12px; border-left: 2px solid var(--area-accent, #7d2e24); opacity: .55; }
/* A step's supporting lines (names, the next meeting, cross-links) fold under its quote. */
.timeline-more { margin: 4px 0 0; }
.timeline-more > summary { cursor: pointer; color: var(--muted); font: 700 0.76rem/1.5 Arial, Helvetica, sans-serif; }
.timeline-more > summary:hover, .timeline-more > summary:focus-visible { color: var(--ink); }
.timeline-marker { display: grid; place-items: center; width: 24px; height: 24px; border: 0; border-radius: 50%; background: var(--area-accent, #7d2e24); color: var(--surface, #fffdf7); font: 700 0.72rem/1 Arial, Helvetica, sans-serif; }
.timeline-label { margin: 2px 0 6px; line-height: 1.4; }
.timeline-label .tag { margin-left: 4px; vertical-align: 1px; }
.timeline-note { margin: 4px 0 0; color: var(--muted); font: 0.8rem/1.5 Arial, Helvetica, sans-serif; }
/* Quote-and-position pair (the shared copy of the /police pattern, which still carries its own
 * until that page moves onto this one): an exact quote, then the site's labeled position. */
.report-quote { margin: 0 0 10px; padding: 0 0 0 12px; border-left: 3px solid var(--rule, #999); font-style: italic; }
.timeline .report-quote p { margin: 0; }
.report-quote cite { display: block; margin-top: 4px; font-size: 0.85rem; font-style: normal; }
.counterpoint { margin: 14px 0 0; padding: 8px 12px; border-left: 3px solid var(--tone-analysis); background: var(--paper); color: var(--ink); font: 0.88rem/1.5 Arial, Helvetica, sans-serif; }
.counterpoint strong { color: var(--tone-analysis); }
.counterpoint a { color: var(--accent-dark); text-underline-offset: 3px; }
/* Print-friendly citation block: full text on screen, the link's address written out on paper. */
.cite-block { margin: 12px 0; padding: 10px 14px; border: 1px solid var(--rule, #b8ae9d); border-left: 4px solid var(--ink); background: var(--surface, #fffdf7); font: 0.86rem/1.5 Arial, Helvetica, sans-serif; }
.cite-block-label { margin: 0 0 4px; color: var(--muted); font-weight: 700; font-size: 0.68rem; letter-spacing: 0.06em; text-transform: uppercase; }
.cite-block-text { margin: 0; overflow-wrap: anywhere; user-select: all; }
.cite-block-meta { margin: 4px 0 0; color: var(--muted); font-size: 0.78rem; }
@media print {
  .cite-block { border: 1px solid #000; border-left-width: 4px; background: none; break-inside: avoid; }
  .cite-block a[href^="http"]::after { content: " <" attr(href) ">"; font-size: 0.8em; overflow-wrap: anywhere; }
}
/* External report card (/records-requests, /alpr/tracker): an outside outlet's headline, byline, date,
 * outside link and short summary (the site does not check its claims), one .disclosure-card per report (src/alpr/external-reports.js). The
 * label is the shared "External report" tag (/why uses the same class). */
.external-label { display: inline-block; margin-right: 6px; padding: 1px 6px; border: 1px solid var(--tone-external); color: var(--tone-external); font: 700 .68rem/1.4 Arial, Helvetica, sans-serif; letter-spacing: .08em; text-transform: uppercase; vertical-align: 2px; }
.external-report { margin: 14px 0; --role-accent: var(--tone-external); }
.external-report-meta, .external-report-records, .external-report-left-out { color: var(--muted); font: 0.86rem/1.5 Arial, Helvetica, sans-serif; }
.external-report-table th[scope="row"], .side-by-side-table th[scope="row"] { min-width: 7em; text-align: left; vertical-align: top; }
.external-report-table td, .side-by-side-table td { min-width: 14em; vertical-align: top; }
.external-report-status { font: 700 0.72rem/1.4 Arial, Helvetica, sans-serif; letter-spacing: .06em; text-transform: uppercase; }
.external-report-status.status-matches { color: var(--tone-positive); }
.external-report-status.status-partly-matches { color: var(--tone-analysis); }
.external-report-status.status-differs { color: var(--tone-miss); }
.external-report-status.status-not-in-our-records { color: var(--muted); }
/* Phone: each fact stacks (topic, then the outlet's account, then ours) instead of a 3-column
 * table scrolling sideways; the column name rides on each cell. .side-by-side-table is the same
 * layout for any topic-by-topic comparison of two sources (the ALPR tracker's Statements and records). */
@media (max-width: 650px) {
  .external-report-table, .external-report-table tbody, .external-report-table tr, .external-report-table th[scope="row"], .external-report-table td,
  .side-by-side-table, .side-by-side-table tbody, .side-by-side-table tr, .side-by-side-table th[scope="row"], .side-by-side-table td { display: block; min-width: 0; }
  .external-report-table caption, .side-by-side-table caption { display: block; text-align: left; }
  .external-report-table thead, .side-by-side-table thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
  .external-report-table tr, .side-by-side-table tr { padding: 10px 0; border-top: 1px solid var(--rule, #b8ae9d); }
  .external-report-table th[scope="row"], .external-report-table td, .side-by-side-table th[scope="row"], .side-by-side-table td { padding: 4px 0; border: 0; }
  .external-report-table td::before, .side-by-side-table td::before { content: attr(data-label); display: block; color: var(--muted); font: 700 0.68rem/1.4 Arial, Helvetica, sans-serif; letter-spacing: .06em; text-transform: uppercase; }
}
.external-report-cite { display: block; margin-top: 6px; color: var(--muted); font-size: 0.82rem; }
.external-report-status.status-source-checked { color: var(--tone-external); }
/* People summary line (design rule 14): under a record, story or card that names officials, one
 * "People:" line lists each profiled person once, linked. The names in the text above link too
 * (people-links.js); this line is the at-a-glance summary, never the only link. */
.people-line { margin: 10px 0 0; color: var(--muted); font: 0.84rem/1.5 Arial, Helvetica, sans-serif; overflow-wrap: anywhere; }
.people-line a { color: var(--accent-dark); text-underline-offset: 3px; }

/* --------------------------------------------------------------------------------------------
 * Upcoming agenda -- the next meeting's posted agenda on the homepage and /local, built by
 * src/local-heads-up.js's renderUpcomingAgenda: a teaser line, then a .disclosure-card with every
 * item in the agenda's own wording.
 */
.next-agenda { margin: 14px 0 0; }
.next-agenda-teaser { margin: 0 0 10px; overflow-wrap: anywhere; }
.next-agenda-items { margin: 12px 0 8px; padding-left: 1.6em; }
.next-agenda-items li { margin: 0 0 6px; overflow-wrap: anywhere; }
.next-agenda-source { margin: 8px 0 0; font-size: 0.9rem; }
/* Data stamp (design rule 15; src/data-stamp.js, window.DataStamp): every block that shows records
 * says how fresh they are, "Records through <date> · Updated <date>", inside its figure frame or
 * right under its heading. Muted small type like a source line; the parts wrap as units on a phone. */
.data-stamp { margin: 6px 0 10px; color: var(--muted, #68635a); font: 0.8rem/1.5 Arial, Helvetica, sans-serif; }
.data-stamp > span { display: inline-block; }
.data-stamp > span:not(:last-child)::after { content: "\00B7"; margin: 0 0.45em; }
.data-stamp time { color: var(--ink, inherit); font-weight: 700; }

/* --------------------------------------------------------------------------------------------
 * Data section (docs/design-guidelines.md rule 18): one order for every section that presents a
 * dataset, so a reader always knows where to look -- head (kicker, title, one-line dek and its
 * caveat), headline figures (.stat-row), the chart (.figure-frame), browse (chips, filters, the
 * list or table), then notes. Each section keeps its own identity through its tone: the band
 * above the head, the swatch beside the kicker and the stat tiles' top rules take
 * --section-accent. Tones reuse existing tokens only (no new colors); text stays --ink/--muted. */
.data-section { --section-accent: var(--area-accent, var(--accent, #7d2e24)); margin-top: 18px; }
.data-section[data-tone="blue"] { --section-accent: var(--blue, #315b68); }
.data-section[data-tone="public"] { --section-accent: var(--role-public, #7322c3); }
.data-section[data-tone="positive"] { --section-accent: var(--tone-positive, #2f6b45); }
/* "gold" once meant the gold structure tone; it now follows the area tone (rule 27, 2026-10-04).
 * A section about Altoona beside other cities takes data-tone="altoona". */
.data-section[data-tone="gold"] { --section-accent: var(--area-accent, var(--accent, #7d2e24)); }
.data-section[data-tone="altoona"] { --section-accent: var(--altoona, #9c7300); }
.data-section-head {
  padding: 16px 18px 14px;
  border: 1px solid var(--rule, #b8ae9d);
  border-top: 4px solid var(--section-accent);
  background: var(--surface, #fffdf7);
}
.data-section-kicker {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 6px;
  color: var(--accent-dark, var(--accent));
  font: 700 0.72rem/1.3 Arial, Helvetica, sans-serif;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
.data-section-kicker::before { content: ""; width: 10px; height: 10px; background: var(--section-accent); flex: none; }
.data-section-head h2 { margin: 0; }
.data-section-dek { max-width: 72ch; margin: 6px 0 0; color: var(--muted, #68635a); font: 0.9rem/1.5 Arial, Helvetica, sans-serif; }
.data-section-head .caveat-more { margin-top: 6px; }
.data-section > .stat-row { margin: 12px 0 0; }
.data-section .stat-tile { border-top-color: var(--section-accent); }
.data-section > .figure-frame { margin-top: 14px; }
/* A section's folded layers (rule 18) carry its tone on their left rule, so the head and every
 * disclosure beneath it read as one section. */
.data-section .data-disclosure { border-left-color: var(--section-accent); }
.data-section .data-disclosure > summary::before { color: var(--section-accent); }
.data-section > .data-disclosure:first-of-type { margin-top: 12px; }
.data-section-browse { margin-top: 20px; padding-top: 14px; border-top: 1px solid var(--rule, #b8ae9d); }
.data-section-browse > :first-child { margin-top: 0; }
.data-section-notes { margin-top: 16px; padding-top: 10px; border-top: 1px solid var(--rule, #b8ae9d); color: var(--muted, #68635a); font: 0.8rem/1.5 Arial, Helvetica, sans-serif; }
.data-section-notes p { max-width: 80ch; margin: 0 0 6px; }
@media (max-width: 560px) {
  .data-section-head { padding: 14px 14px 12px; }
  .data-section > .stat-row { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
}

/* --------------------------------------------------------------------------------------------
 * 9. The 4.0 coat (design rule 31): the shared pieces above read the 4.0 tokens in
 * site-shell.css (--space-*, --step-*, --radius, --density, --row-alt, --row-hover), so every
 * page that uses them takes the same paint at once. Quiet on purpose: a soft corner, a faint
 * lift, zebra rows and a hover row, figures in tabular numerals, and padding that scales with
 * Settings > Density. Nothing here adds a color: gold still marks only Altoona (rule 27).
 */
.stat-tile, .disclosure-card, .figure-frame, .chart-container, .empty-state, .data-section-head {
  border-radius: var(--radius);
}
.stat-tile, .figure-frame { box-shadow: var(--lift); }
.stat-row { gap: calc(var(--space-3) * var(--density)); }
.stat-tile { padding: calc(var(--space-3) * var(--density)) calc(14px * var(--density)); }
.stat-tile .stat-value { overflow-wrap: anywhere; }
.figure-frame { padding: calc(14px * var(--density)) calc(var(--space-4) * var(--density)) calc(var(--space-4) * var(--density)); }
.data-table tbody td, .data-table tbody th { padding: calc(7px * var(--density)) calc(10px * var(--density)); }
.data-table thead th { padding: calc(var(--space-2) * var(--density)) calc(10px * var(--density)); }
/* Zero specificity (:where), so a page's own cell color (heat steps, presence strips) still wins. */
:where(.data-table tbody tr:nth-child(even) > td, .data-table tbody tr:nth-child(even) > th) { background: var(--row-alt); }
:where(.data-table tbody tr:hover > td, .data-table tbody tr:hover > th) { background: var(--row-hover); }
.data-table :is(td, th) { font-variant-numeric: tabular-nums; }
.disclosure-card > summary { padding: calc(14px * var(--density)) calc(var(--space-4) * var(--density)); }
.disclosure-card .disclosure-body { padding: 0 calc(var(--space-4) * var(--density)) calc(var(--space-4) * var(--density)); }
.record-list > li { padding-block: calc(var(--space-2) * var(--density)); }
.empty-state { padding: calc(14px * var(--density)) calc(var(--space-4) * var(--density)); }
:root[data-density="compact"] .data-table, .density-compact .data-table { font-size: var(--step--1); }
:root[data-density="compact"] .stat-tile .stat-value, .density-compact .stat-tile .stat-value { font-size: clamp(1.25rem, 3vw, 1.6rem); }
:root[data-density="compact"] .record-list > li, .density-compact .record-list > li { font-size: .84rem; }
.density-comfortable .data-table { font-size: .85rem; }
.density-comfortable .stat-tile .stat-value { font-size: clamp(1.5rem, 4vw, 2.1rem); }

/* A small stat tile for a dense row of secondary figures (the headline row keeps the full tile). */
.stat-tile.is-compact { padding: calc(var(--space-2) * var(--density)) calc(10px * var(--density)); }
.stat-tile.is-compact .stat-value { font-size: var(--step-2); }

/* A figure the record cannot give. Three cases, each in words (never a blank cell or a zero):
 *   .count-small  a count of 1 or 2 shown as "<3" (small-cell rule); its title spells it out.
 *   .value-missing  "Not recorded" or "No minutes": the record exists but does not say.
 *   .value-pending  "Not yet posted": the City has not published it yet.
 * Each is muted ink, never a color alone; a screen reader hears the words. */
.count-small { color: var(--muted); font-variant-numeric: tabular-nums; text-decoration: underline dotted; text-underline-offset: 3px; cursor: help; }
.value-missing, .value-pending { color: var(--muted); font-style: italic; font-variant-numeric: tabular-nums; }
/* Pending is a status: the expected amber, beside its word and its glyph (rule 27). */
.value-pending { color: var(--status-warn-ink, var(--muted)); }
.value-pending::before { content: "\25CC\00A0"; font-style: normal; }
.stat-tile .stat-value :is(.count-small, .value-missing, .value-pending) { font-size: .7em; }

/* Loading and error states join the empty state (section 7): one box, three moods. Loading draws
 * placeholder lines (still under reduced motion); an error names what failed and what to try. */
.empty-state.is-loading { border-style: solid; border-color: color-mix(in srgb, var(--rule) 60%, transparent); }
.empty-state.is-error { border-style: solid; border-left: 4px solid var(--status-bad, #b3261e); background: var(--status-bad-wash, transparent); color: var(--ink); }
.empty-state.is-error strong { color: var(--status-bad-ink, #9b2018); }
.skeleton-line { display: block; height: .7em; margin: .55em 0; border-radius: var(--radius); background: linear-gradient(90deg, var(--row-alt), var(--row-hover), var(--row-alt)); background-size: 200% 100%; animation: skeleton-sweep 1.6s ease-in-out infinite; }
.skeleton-line:nth-child(2n) { width: 82%; }
.skeleton-line:nth-child(3n) { width: 64%; }
@keyframes skeleton-sweep { from { background-position: 100% 0; } to { background-position: -100% 0; } }
@media (prefers-reduced-motion: reduce) { .skeleton-line { animation: none; } }

/* Rule 30's pieces (a point that lands, from the record, in neutral words), on the 4.0 coat.
 *   .stat-compare  one line under a tile's figure that sets it against something: a share of a
 *                  total, the year before, the median. Ink text, states its denominator.
 *   .point-strip   "What this shows": the one telling fact a section leads with, in a sentence.
 *                  .is-analysis when it is the site's reading, with the kicker "Analysis" (rule 21).
 *   .report-card   a compact list of measures, each with its figure and the comparison that
 *                  gives it weight. No grades, no colors that judge; the comparison does the work. */
.stat-tile .stat-compare {
  display: block;
  margin-top: 6px;
  padding-top: 6px;
  border-top: 1px dotted var(--rule, #b8ae9d);
  color: var(--ink);
  font: 700 var(--step--1)/1.35 var(--font-ui);
}
.point-strip {
  display: grid;
  gap: 2px;
  margin: var(--space-4) 0;
  padding: calc(var(--space-3) * var(--density)) calc(var(--space-4) * var(--density));
  border: 1px solid var(--rule, #b8ae9d);
  border-left: 4px solid var(--ink);
  border-radius: var(--radius);
  background: var(--surface, #fffdf7);
  box-shadow: var(--lift);
}
.point-strip.is-analysis { border-left-style: double; border-left-width: 5px; }
.point-strip-kicker { margin: 0; color: var(--muted); font: 700 var(--step--2)/1.4 var(--font-ui); letter-spacing: .08em; text-transform: uppercase; }
.point-strip-text { max-width: 70ch; margin: 0; color: var(--ink); font: var(--step-1)/1.4 var(--font-read); }
.point-strip-text strong { font-variant-numeric: tabular-nums; }
.point-strip-source { margin: 4px 0 0; color: var(--muted); font: var(--step--1)/1.4 var(--font-ui); }
.report-card { margin: 0; padding: 0; list-style: none; border-top: 2px solid var(--ink); }
.report-card > li {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1.3fr);
  gap: 2px 14px;
  align-items: baseline;
  padding: calc(10px * var(--density)) 0;
  border-bottom: 1px solid var(--rule, #b8ae9d);
}
.report-card > li:last-child { border-bottom: 0; }
.report-card-measure { color: var(--ink); font: 700 var(--step--1)/1.35 var(--font-ui); }
.report-card-figure { color: var(--ink); font: 700 var(--step-2)/1.1 var(--font-read); font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }
.report-card-context { color: var(--muted); font: var(--step--1)/1.4 var(--font-ui); }
@media (max-width: 560px) {
  .report-card > li { grid-template-columns: minmax(0, 1fr) auto; }
  .report-card-context { grid-column: 1 / -1; }
}

/* --------------------------------------------------------------------------------------------
 * 10. The tool pattern (design rule 32): one experience across /alpr/camera-check, /alpr/coverage,
 * /alpr/tracker and the home widget. A tool page is: the shell header, .tool-nav (the sibling
 * tools in one fixed order), a landing .data-section that opens with a .point-strip (the answer
 * before any control is touched), the input, the figure in a .figure-frame, a .stat-row, then one
 * .tool-settings card holding every control under the same names on every tool. The spec with the
 * order and the shared control names: docs/design-guidelines.md rule 32.
 */
.tool-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 18px;
  margin: 0;
  padding: calc(var(--space-3) * var(--density)) 0;
  border-bottom: 1px solid var(--rule, #b8ae9d);
  font: 700 var(--step--2)/1.4 var(--font-ui);
  letter-spacing: .08em;
  text-transform: uppercase;
}
.tool-nav a { color: inherit; text-decoration: none; padding: 2px 0; }
.tool-nav a:hover, .tool-nav a:focus-visible { text-decoration: underline; text-underline-offset: 4px; }
.tool-nav a[aria-current="page"] { color: var(--accent-dark); box-shadow: inset 0 -2px 0 var(--accent-dark, #61221a); }
@media (pointer: coarse) { .tool-nav a { min-height: 24px; display: inline-flex; align-items: center; } }

/* .tool-settings is a .disclosure-card whose body is a grid of control groups; each group is a
 * label, one plain sentence, then its choices (the site's .dataview-control-btn row, a select or
 * a range). The same names and order on every tool: Area, Distance rings, Cameras that count,
 * Units, Value colors. Reader-wide choices read and write SitePrefs; page-local ones never persist. */
.tool-settings > summary .disclosure-title::before { content: "\2699\00A0"; font-weight: 400; }
.tool-settings-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr));
  gap: calc(var(--space-4) * var(--density)) calc(var(--space-5) * var(--density));
  padding-top: calc(var(--space-3) * var(--density));
}
.tool-control { min-width: 0; margin: 0; padding: 0; border: 0; }
.tool-control-label, .tool-control > legend { display: block; margin: 0 0 2px; padding: 0; color: var(--ink); font: 700 var(--step--1)/1.35 var(--font-ui); }
.tool-control-help { margin: 0 0 6px; color: var(--muted); font: var(--step--2)/1.4 var(--font-ui); }
.tool-control select, .tool-control input[type="text"], .tool-control input[type="search"] {
  width: 100%; min-height: 32px; padding: 4px 8px; border: 1px solid var(--rule, #b8ae9d); border-radius: var(--radius);
  background: var(--paper, #f5f0e6); color: var(--ink); font: var(--step--1)/1.4 var(--font-ui);
}
.tool-control input[type="range"] { width: 100%; accent-color: var(--focus, #61221a); }
.tool-control-choices { display: flex; flex-wrap: wrap; gap: 6px; }
.tool-control-choices label { display: inline-flex; align-items: center; gap: 5px; min-height: 24px; font: var(--step--1)/1.4 var(--font-ui); }

/* A band meter: a short bar beside a figure in a table cell, filled to the figure's share of the
 * column's largest value. Altoona gold only when the row is Altoona; otherwise the ink at 55%.
 * The figure is always printed beside it (rule 27). Moved here from /alpr/camera-check. */
.band-cell { display: flex; align-items: center; gap: 8px; }
.band-meter { flex: 0 0 90px; height: 8px; border: 1px solid var(--rule, #b8ae9d); border-radius: var(--radius); background: var(--paper, #f5f0e6); overflow: hidden; }
.band-meter > span { display: block; height: 100%; background: color-mix(in srgb, var(--ink) 55%, transparent); }
.band-meter.is-altoona > span, tr.is-altoona .band-meter > span { background: var(--altoona, #9c7300); }
@media (max-width: 560px) { .band-meter { display: none; } }

/* Status, in words first (rule 27, 2026-10-04): a pill or a line that says good, warning, bad or
 * info wears the expected color beside its word or glyph, never the brand gold. */
.status { display: inline-block; padding: 1px 8px; border: 1px solid var(--status-color, var(--rule)); border-radius: 999px; background: var(--status-wash, transparent); color: var(--status-ink, var(--ink)); font: 700 var(--step--2, .7rem)/1.6 var(--font-ui, Arial, Helvetica, sans-serif); letter-spacing: .02em; white-space: nowrap; }
.status.is-good { --status-color: var(--status-good); --status-ink: var(--status-good-ink); --status-wash: var(--status-good-wash); }
.status.is-warn { --status-color: var(--status-warn); --status-ink: var(--status-warn-ink); --status-wash: var(--status-warn-wash); }
.status.is-bad { --status-color: var(--status-bad); --status-ink: var(--status-bad-ink); --status-wash: var(--status-bad-wash); }
.status.is-info { --status-color: var(--status-info); --status-ink: var(--status-info-ink); --status-wash: var(--status-info-wash); }
.status.is-altoona { --status-color: var(--altoona); --status-ink: var(--altoona-ink); --status-wash: var(--altoona-wash); }

/* Also recorded as (rule 33, 2026-10-04): a grouped name's fold. One card stands for every
 * spelling the record prints; the fold lists each exact spelling with its count and dates, and says
 * in one line why they share the card. Used on the /votes business cards. */
.alias-fold { margin: var(--space-2, 8px) 0 0; font: var(--step--1)/1.4 var(--font-ui); }
.alias-fold > summary { cursor: pointer; color: var(--ink); font-weight: 700; min-height: 24px; }
.alias-fold-list { display: grid; gap: var(--space-1, 4px); margin: var(--space-2, 8px) 0 0; padding: 0; list-style: none; }
.alias-fold-list li { display: grid; gap: 2px; }
.alias-fold-list strong { color: var(--ink); overflow-wrap: anywhere; }
.alias-fold-list span, .alias-fold-why { color: var(--muted); }
.alias-fold-why { margin: var(--space-2, 8px) 0 0; }

/* --------------------------------------------------------------------------------------------
 * 11. Reading guide (design rule 34): "How to read it" shows the mark it explains.
 * Each item is the figure's own mark (a swatch, a ramp, a line, a dash, a dot, a glyph or a
 * sample of the text) beside a bold term and one sentence of meaning, so the guide reads like the
 * figure's key rather than a list of prose. Items sit in a responsive grid of quiet cards on the
 * area tone; `.is-limits` (what a figure cannot tell you) rules its cards in the warning amber.
 *   <dl class="read-guide">
 *     <div class="read-guide-item"><span class="read-guide-mark" style="--swatch: ..."></span>
 *       <dt class="read-guide-term">Red</dt><dd class="read-guide-meaning">...</dd></div>
 *   </dl>
 * Marks: default a filled square in --swatch; .is-ramp (--ramp-from to --ramp-to); .is-line;
 * .is-dash; .is-dot; .is-hollow (an open dot); .is-limit (an open warning ring); .is-glyph (a
 * character in --swatch, e.g. a digit or a sign); .is-text (a short sample of styled text). */
.read-guide { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr)); gap: calc(var(--space-2, 8px) * var(--density, 1)) calc(var(--space-3, 12px) * var(--density, 1)); max-width: 100ch; margin: calc(var(--space-3, 12px) * var(--density, 1)) 0 0; padding: 0; }
.read-guide-item { display: grid; grid-template-columns: 24px minmax(0, 1fr); gap: 0 10px; align-items: start; margin: 0; padding: calc(var(--space-2, 8px) * var(--density, 1)) calc(var(--space-3, 12px) * var(--density, 1)); border: 1px solid var(--rule, #b8ae9d); border-left: 3px solid var(--area-accent, var(--accent, #7d2e24)); border-radius: var(--radius, 4px); background: var(--surface, #fffdf7); box-shadow: var(--lift, none); }
.read-guide-mark { grid-row: 1 / span 2; display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; margin-top: 1px; color: var(--swatch, var(--muted, #68635a)); --swatch: var(--muted, #68635a); }
.read-guide-mark::before { content: ""; display: block; width: 16px; height: 16px; border-radius: 3px; background: var(--swatch); }
.read-guide-mark.is-ramp::before { width: 22px; border-radius: 2px; background: linear-gradient(90deg, var(--ramp-from, var(--dataview-heat-1)), var(--ramp-to, var(--dataview-heat-5))); }
.read-guide-mark.is-line::before, .read-guide-mark.is-dash::before { width: 22px; height: 0; border-top: 3px solid var(--swatch); border-radius: 0; background: none; }
.read-guide-mark.is-dash::before { border-top-style: dashed; }
.read-guide-mark.is-dot::before { width: 10px; height: 10px; border-radius: 50%; }
.read-guide-mark.is-hollow::before { width: 10px; height: 10px; border: 2px solid var(--swatch); border-radius: 50%; background: var(--surface, #fffdf7); }
.read-guide-mark.is-limit::before { width: 12px; height: 12px; border: 2px solid var(--status-warn, #a85a00); border-radius: 50%; background: transparent; }
.read-guide-mark.is-glyph::before { content: none; }
.read-guide-mark.is-glyph { font: 700 var(--step-0, 1rem)/1 var(--font-ui, Arial, Helvetica, sans-serif); }
.read-guide-mark.is-text { grid-row: 1; width: auto; max-width: 9ch; height: auto; justify-content: flex-start; font: 700 var(--step--2, .7rem)/1.4 var(--font-ui, Arial, Helvetica, sans-serif); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.read-guide-mark.is-text::before { content: none; }
.read-guide-item:has(> .read-guide-mark.is-text) { grid-template-columns: minmax(0, auto) minmax(0, 1fr); }
.read-guide-item:has(> .read-guide-mark.is-text) .read-guide-meaning { grid-column: 1 / -1; }
.read-guide-term { margin: 0; color: var(--ink, #20211e); font: 700 var(--step--1, .86rem)/1.35 var(--font-ui, Arial, Helvetica, sans-serif); }
.read-guide-meaning { margin: 2px 0 0; color: var(--muted, #68635a); font: var(--step--1, .86rem)/1.5 var(--font-read, Georgia, "Times New Roman", serif); }
.read-guide-meaning a { color: var(--accent-dark, #61221a); }
.read-guide.is-limits .read-guide-item { border-left-color: var(--status-warn, #a85a00); }
.read-guide-note { max-width: 80ch; margin: calc(var(--space-2, 8px) * var(--density, 1)) 0 0; color: var(--muted, #68635a); font: var(--step--2, .78rem)/1.5 var(--font-ui, Arial, Helvetica, sans-serif); }
@media (forced-colors: active) { .read-guide-mark::before { forced-color-adjust: none; } }
