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

:root {
  --bg:        #0d0d0d;
  --surface:   #141414;
  --surface-2: #1c1c1c;
  --surface-3: #222;
  --border:    #2a2a2a;
  --border-hl: #3a3a3a;
  --text:      #f0f0f0;
  --text-muted:#888;
  --green:     #4caf7d;
  --green-dim: #1a3a2a;
  --green-glow:#4caf7d22;
  --radius:    10px;
  --tab-h:     44px;
}

body {
  background: var(--bg);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  min-height: 100vh;
  padding: 0 1.5rem 4rem;
}

@media (max-width: 640px) {
  /* narrower page gutters + tighter card padding on phones so summary
     tiles, the money-over-time chart, and data tables all get more
     usable width before anything has to shrink/scroll -- cut further
     (Henry, 2026-09-15) after the first pass still left the
     money-over-time chart's month labels overlapping. */
  body { padding: 0 0.5rem 4rem; }
  .card { padding: 1.1rem 0.5rem; }
  /* .ie-side-body is a separate component from .card (support/oppose IE
     tables) and didn't inherit that trim -- same fixed 1.3rem padding was
     narrowing the IE tables' own width budget on top of the column-width
     squeeze fixed in candidate-profile.html. */
  .ie-side-body { padding: 1.1rem 0.5rem; }
  /* simple (non-breakdown) stat tiles -- Total Raised/Spent, etc. --
     center their label+value instead of hugging the now-tighter left
     edge, to compensate visually for the reclaimed margin above.
     .stat-tile.has-breakdown (the IE Raised/Spent tiles with a
     Supporting/Opposing split beside the number) is left out on
     purpose -- it's a label-left/value-right flex row, not a text
     block, and centering would break that layout. */
  .stat-tile:not(.has-breakdown) { text-align: center; }
  /* donut chart (Contributor Types etc.) -- side-by-side with a fixed
     128px circle left almost no room for the legend, truncating category
     names ("Individual" -> "Ind..."). Stack the circle above the legend
     instead so the legend gets the card's full width on phones. */
  .donut-wrap { flex-direction: column; align-items: center; }
  .legend { width: 100%; }
}

/* ── nav ── */
nav {
  display: flex;
  align-items: center;
  gap: 2rem;
  padding: 1.25rem 0;
  border-bottom: 1px solid var(--border);
  margin-bottom: 4rem;
}
.nav-logo {
  font-size: 15px; font-weight: 600; color: var(--green);
  letter-spacing: -0.3px; text-decoration: none;
}
.nav-section {
  font-size: 13px; color: var(--text-muted);
  text-decoration: none; transition: color 0.15s;
}
.nav-section:hover { color: var(--text); }

/* ── container ── */
.container { max-width: 1584px; margin: 0 auto; }

/* ── footer ── */
footer {
  max-width: 1584px; margin: 3rem auto 0;
  padding: 1.5rem 0;
  border-top: 1px solid var(--border);
  display: flex; gap: 2rem; align-items: center;
  justify-content: space-between;
}
.footer-credit {
  font-size: 12px; color: var(--text-muted);
}
footer a {
  display: flex; align-items: center; gap: 6px;
  font-size: 13px; color: var(--text-muted); text-decoration: none;
  transition: color 0.15s;
}
footer a:hover:not(.footer-link-disabled) { color: var(--text); }
footer .footer-link-disabled { opacity: 0.35; cursor: default; pointer-events: none; }

/* ── data sources (added 2026-09-17) -- a short "where this data came
   from" list at the bottom of a per-state/race/candidate page, between
   the last content section and the footer. Deliberately understated
   (footer-link sizing/color, not a full .card) since it's attribution,
   not content -- most states will only have 1-2 sources; render as many
   as DATA.sources actually has. Shared by candidate-profile.html,
   race-profile.html, and state-profile.html. */
.sources-row { display: flex; justify-content: space-between; align-items: flex-start; gap: 10px 24px; flex-wrap: wrap; }
.sources-updated { font-size: 13px; color: var(--text-muted); margin-left: auto; text-align: right; }
.sources-list { display: flex; flex-direction: column; gap: 10px; }
.source-link {
  display: flex; align-items: center; gap: 6px; width: fit-content;
  font-size: 13px; color: var(--text-muted); text-decoration: none;
  transition: color 0.15s;
}
.source-link:hover { color: var(--text); }
.source-link svg { flex-shrink: 0; opacity: 0.6; }

/* ═══════════════════════════════════════════════════════════════
   DATA PAGE COMPONENTS
   Shared by candidate-profile.html and any future profile-style
   page (e.g. a committee page). NOT yet used by index.html's own
   #profile-modal — that's a separate, older component system
   (.profile-card/.profile-stat/.profile-data-table/etc., defined
   inline in index.html) that does a similar job for committees.
   The plan is for candidate-profile.html to eventually replace the
   modal for candidates specifically (committees/PACs keep using
   the modal), at which point these two systems should get
   reconciled — not done yet, deliberately out of scope for now.

   Two names below were chosen to avoid real collisions with
   index.html, which already owns bare .stat-label/.stat-value for
   its homepage hero stats ("102M records" etc.) — this file's
   stat-tile component uses .tile-label/.tile-value instead. Every
   other name here was checked against index.html/downloads.html/
   data-quality.html and doesn't collide with anything.
   ═══════════════════════════════════════════════════════════════ */

.section { margin-bottom: 3rem; }
.section-title {
  font-size: 12px; font-weight: 600; letter-spacing: 1px; text-transform: uppercase;
  color: var(--text-muted); margin-bottom: 1rem; display: flex; align-items: center; gap: 10px;
}
.section-title .conditional-tag {
  font-size: 10px; font-weight: 500; letter-spacing: 0.3px; text-transform: none;
  color: var(--text-muted); background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 4px; padding: 1px 6px;
}
.card {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 1.5rem;
}
.subhead {
  font-size: 13px; font-weight: 600; color: var(--text); margin-bottom: 0.9rem;
}
/* One plain-language line under a section title or a card's subhead:
   what the numbers below cover, or how to read them. */
.section-note {
  font-size: 13px; line-height: 1.45; color: var(--text-muted);
  margin: -0.5rem 0 1rem;
}
.card .section-note { margin: -0.5rem 0 0.9rem; font-size: 12.5px; }
/* head row for a card whose title shares space with a control (a chamber
   toggle, a Largest/Latest toggle, etc.) — subhead grows left, the control
   sits right. Shared by candidate-profile.html's summary chart card and
   state-profile.html's Legislature card. */
.chart-card-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.2rem; }

/* ── stat tiles ── */
.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 12px; }
.stat-tile {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 1.1rem 1.3rem;
}
.tile-label { font-size: 11px; font-weight: 600; letter-spacing: 0.6px; text-transform: uppercase; color: var(--text-muted); margin-bottom: 8px; }
.tile-value { font-size: 1.7rem; font-weight: 700; letter-spacing: -0.5px; }
.tile-value.support { color: var(--green); }
.tile-value.oppose { color: #e66767; }
.tile-value.unknown { color: var(--text-muted); }
/* breakdown lines stack below the big number (not beside it), each term
   on its own line — plain muted text, no per-term color. */
.stat-sub-list { display: flex; flex-direction: column; gap: 2px; margin-top: 4px; }
.stat-sub { font-size: 12px; color: var(--text-muted); line-height: 1.3; }

/* —— in-tile Supporting/Opposing breakdown (2026-09-06) — for a stat
   tile whose one big number is a combined total across two sides (the
   IE Raised/Spent tiles), a compact colored breakdown sits to the RIGHT
   of that number, smaller, rather than stacking a whole second section
   below it or splitting into separate tiles — keeps the "one combined
   total" as the visual headline with the split as supporting detail.
   Opt-in via statTile()'s `breakdown` param — .stat-tile without one
   renders exactly as before (plain block, no flex). */
.stat-tile.has-breakdown { display: flex; align-items: center; justify-content: space-between; gap: 1.2rem; }
.tile-main { flex: 1; min-width: 0; }
.tile-breakdown { flex-shrink: 0; display: flex; flex-direction: column; gap: 4px; }
/* each row is its OWN flex row (label left, value right) rather than one
   right-aligned text run -- the column flex container above stretches
   every row to a shared width, so with justify-content:space-between
   here, both rows' labels start flush at the same left edge and both
   values end flush at the same right edge, regardless of "Supporting"
   vs. "Opposing" being different lengths. */
.tile-breakdown-row {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  font-size: 11.5px; line-height: 1.3; white-space: nowrap;
}
.tile-breakdown-row .tbr-label { color: var(--text-muted); }
.tile-breakdown-row .tbr-value { font-weight: 700; }
.tile-breakdown-row.support .tbr-value { color: var(--green); }
.tile-breakdown-row.oppose .tbr-value { color: #e66767; }
.tile-breakdown-row.unknown .tbr-value { color: var(--text-muted); }

/* ── 2-up / 3-up responsive grids (collapse to 1 column under 900px) ── */
/* auto-fit (not a fixed 1fr-per-column template) so that when a sibling
   card is hidden via `el.hidden = true` (see [hidden] rule above --
   Top Employers / the IE contributor & IE expenditure cards, per their own
   JS comments), CSS Grid drops it from layout entirely and auto-fit
   collapses its now-empty track, letting the remaining card(s) stretch
   to fill the row instead of leaving a gap. minmax()'s min width is
   chosen well under this section's normal per-card width so a full row
   never wraps to fewer columns than before this change. */
.demo-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; }
@media (max-width: 900px) { .demo-grid { grid-template-columns: minmax(0, 1fr); } }
.ie-columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; }
@media (max-width: 900px) { .ie-columns { grid-template-columns: minmax(0, 1fr); } }
.expend-columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 20px; }
@media (max-width: 900px) { .expend-columns { grid-template-columns: minmax(0, 1fr); } }

/* ── info-icon tooltips -- a small "i" glyph next to a widget title with
   a short explainer on hover/focus, for the handful of widgets whose
   label alone doesn't make the design decision behind it obvious (IE
   Supporting/Opposing, Non-Individual Donors, Top Employers). Plain CSS,
   no JS -- the tooltip is `content: attr(data-tooltip)` on ::after,
   shown on :hover AND :focus-visible so it's reachable by keyboard/tap,
   not just mouse hover (the icon is a real <button>, so tapping it on
   mobile focuses it the same way).

   Deliberately simple: the tooltip is centered under the icon, with no
   JS-computed repositioning -- a very narrow window could in theory clip
   an edge, an acceptable tradeoff for a handful of short explainer
   strings rather than pulling in a positioning library for this. ── */
.info-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 15px; height: 15px; flex-shrink: 0; margin-left: 6px;
  border: 1px solid currentColor; border-radius: 50%;
  background: transparent; color: inherit;
  font: inherit; font-size: 10px; font-weight: 700; line-height: 1;
  text-transform: none; letter-spacing: normal;
  opacity: 0.5; cursor: help; position: relative;
  -webkit-appearance: none; appearance: none; padding: 0;
}
.info-icon:hover, .info-icon:focus-visible { opacity: 1; }
.info-icon:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
.info-icon::after {
  content: attr(data-tooltip);
  position: absolute; top: calc(100% + 8px); left: 50%; transform: translateX(-50%);
  width: max-content; max-width: 200px;
  background: var(--surface-3); color: var(--text); border: 1px solid var(--border-hl);
  border-radius: 6px; padding: 8px 10px;
  font-size: 11.5px; font-weight: 400; line-height: 1.4; text-align: left;
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity 0.12s ease; z-index: 30;
  box-shadow: 0 4px 16px rgba(0,0,0,0.45);
}
.info-icon::before {
  content: ''; position: absolute; top: calc(100% + 3px); left: 50%; transform: translateX(-50%);
  border: 5px solid transparent; border-bottom-color: var(--surface-3);
  opacity: 0; visibility: hidden; transition: opacity 0.12s ease; z-index: 30;
}
.info-icon:hover::after, .info-icon:hover::before,
.info-icon:focus-visible::after, .info-icon:focus-visible::before {
  opacity: 1; visibility: visible;
}
/* ── side cards — a colored-header card for a "side" of a comparison
   (e.g. supporting/opposing/neutral); pairs with .ie-columns/.expend-columns ── */
.ie-side-card { border-radius: var(--radius); border: 1px solid var(--border); overflow: hidden; }
.ie-side-head {
  /* justify-content: flex-start (2026-09-06, was space-between) -- the
     info-icon now sits right next to the "Supporting"/"Opposing" text
     instead of pushed to the header's far right edge, for visibility --
     user found the far-right placement easy to miss. */
  padding: 12px 16px; font-size: 13px; font-weight: 700; display: flex; align-items: center; justify-content: flex-start;
}
.ie-side-head.support { background: var(--green-dim); color: var(--green); border-bottom: 1px solid #2a5a3a; }
.ie-side-head.oppose { background: #3a1a1a; color: #e88; border-bottom: 1px solid #5a2a2a; }
.ie-side-head.neutral { background: var(--surface-2); color: var(--text); border-bottom: 1px solid var(--border); }
.ie-side-body { background: var(--surface); padding: 1.3rem; }

/* ── toggle "slider" (Largest/Latest, Raised/Spent) + data tables ──
   A track with a sliding thumb behind the labels, rather than two
   separate pill buttons — the thumb is a real element (.toggle-thumb)
   that persists across clicks so its `transform` can transition; only
   the table/chart below gets re-rendered on click, not the toggle
   itself (see renderToggleTable/renderTimeline in candidate-profile.html).
   Assumes a small fixed number of equal-width options (2 today), sized
   via the --n custom property set inline on .table-toggle. Grey, not
   green — the active state here isn't a semantic "good" signal like a
   support/oppose color would be, just "which one is selected". */
.table-toggle {
  position: relative; display: flex; width: fit-content;
  background: var(--surface-2); border: 1px solid var(--border); border-radius: 8px;
  padding: 3px; margin-bottom: 10px;
}
.toggle-thumb {
  position: absolute; top: 3px; bottom: 3px; left: 3px;
  width: calc((100% - 6px) / var(--n, 2));
  background: #565656; border-radius: 6px;
  transition: transform 0.2s ease;
}
.toggle-btn {
  position: relative; z-index: 1; min-width: 70px; text-align: center;
  background: transparent; border: none; border-radius: 6px;
  color: var(--text-muted); font-size: 11.5px; font-family: inherit; font-weight: 500;
  letter-spacing: 0.3px; padding: 4px 10px; cursor: pointer; transition: color 0.15s;
}
.toggle-btn:hover { color: var(--text); }
.toggle-btn.active { color: var(--text); font-weight: 600; }
table.dtable { width: 100%; border-collapse: collapse; font-size: 12.8px; table-layout: fixed; }
table.dtable th {
  text-align: left; font-size: 10.5px; font-weight: 600; letter-spacing: 0.5px; text-transform: uppercase;
  color: var(--text-muted); padding: 0 10px 8px; border-bottom: 1px solid var(--border);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; /* narrow columns (e.g. committeecol) must not spill into the next header */
}
table.dtable th.num, table.dtable td.num { text-align: right; }
/* fixed row height (not just padding) so rows line up across tables with
   different content shapes — e.g. Individual Donors' two-line donor+
   subtext cell next to PAC Donors' one-line PAC name, side by side in
   the same row of cards. vertical-align:middle centers a shorter cell's
   content within that fixed height instead of pinning it to the top. */
table.dtable td { height: 52px; padding: 9px 10px; border-bottom: 1px solid var(--border); vertical-align: middle; }
table.dtable tr:last-child td { border-bottom: none; }
table.dtable td.amount { font-weight: 600; color: var(--text); white-space: nowrap; }
table.dtable td.subtext { color: var(--text-muted); font-size: 11.5px; }
table.dtable td.datecol, table.dtable td.datecolshort { color: var(--text-muted); white-space: nowrap; }
/* fixed layout + explicit widths on the narrow, short-content columns
   (date, amount, small counts, committee) so whichever column holds the
   long primary label — donor name, payee, committee — gets whatever's
   left and can safely ellipsis instead of wrapping or blowing out
   narrower cards (e.g. a 3-up row). Class-based (not positional) since
   some tables (IE expenditures) add a Committee column ahead of Payee,
   so "primary label" isn't always column 1. */
table.dtable th.datecol, table.dtable td.datecol { width: 92px; white-space: nowrap; }
/* wider variant for a header that needs more than a bare "Date" -- added 2026-09-10 for race-profile.html's Non-Individual Donors "Most Recent" column (the header text itself carries the caveat that this is a per-donor rollup's most recent gift date, since a subtext under each row wasn't visible enough at 92px). */
table.dtable th.datecol-wide, table.dtable td.datecol-wide { width: 118px; white-space: nowrap; }
table.dtable th.datecolshort, table.dtable td.datecolshort { width: 62px; white-space: nowrap; } /* short date form, e.g. "Aug 5" */
table.dtable th.amount, table.dtable td.amount { width: 82px; white-space: nowrap; }
/* percentage, not a fixed px like its neighbors (2026-09-06) -- a
   committee name (e.g. "V-PAC: VICTORS, NOT VICTIMS") was ellipsizing
   down to almost nothing at a flat 66px, regardless of how much room
   the card actually had. Since these IE tables live in a card that can
   be either half a row (both Supporting/Opposing present) or the FULL
   row (its sibling hidden, see .ie-columns/.expend-columns' auto-fit
   comment above), a percentage grows with the card instead of staying
   stuck at the two-cards-visible width. */
table.dtable th.committeecol, table.dtable td.committeecol { width: 15%; white-space: nowrap; }
table.dtable th.donorscol, table.dtable td.donorscol { width: 70px; white-space: nowrap; }
table.dtable th.districtcol, table.dtable td.districtcol { width: 76px; white-space: nowrap; } /* e.g. "SD 12" / "HD 84" — state-profile.html's chamber table */
table.dtable .subtext {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block;
}
/* clamps whichever column's primary label (name/payee/committee span) —
   not tied to column position, since Committee can precede Payee. */
table.dtable td > *:first-child {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block;
}
/* ── tap-to-reveal truncated cell text (Henry, 2026-09-15) -- table
   cells' primary name/payee/committee and their subtext lines used to
   rely on the native title="" tooltip, which mobile browsers mostly
   don't surface on tap at all -- a truncated name on a phone was just a
   dead end. Same trick as .info-icon above (a real <button>, tooltip
   shown on :hover AND :focus-visible so tapping it on mobile, which
   focuses it, works with no JS), just reset to look like plain cell
   text instead of a small icon, and left-aligned under the trigger's
   own start edge rather than centered, since triggers here span a
   whole (often wide) cell instead of a small fixed-size icon. */
.cell-tooltip {
  display: block; width: 100%; text-align: inherit;
  background: transparent; border: none; color: inherit; font: inherit;
  padding: 0; margin: 0; cursor: pointer;
  position: relative; -webkit-appearance: none; appearance: none;
}
/* the ellipsis truncation lives on an INNER span, not the button itself
   (Henry, 2026-09-15, real bug found live testing this) -- overflow:
   hidden is required to clip long text with "...", but the popup below
   is generated content INSIDE the button's own box, so overflow:hidden
   on the button clipped its own tooltip into invisibility too. Splitting
   them means the button can host the popup with nothing clipping it,
   while this inner span alone handles the truncated display text. */
.cell-tooltip-text { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* re-asserts overflow: visible against `table.dtable td > *:first-child`
   below, which would otherwise re-impose overflow: hidden on the button
   itself (same specificity, .cell-tooltip's own base rule above would
   lose that fight) and reintroduce the exact clipping this is fixing. */
table.dtable td > .cell-tooltip { overflow: visible; }
.cell-tooltip:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
.cell-tooltip::after {
  content: attr(data-tooltip);
  position: absolute; top: calc(100% + 8px); left: 0;
  width: max-content; max-width: 240px;
  background: var(--surface-3); color: var(--text); border: 1px solid var(--border-hl);
  border-radius: 6px; padding: 8px 10px;
  font-size: 12px; font-weight: 400; line-height: 1.4; text-align: left; white-space: normal;
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity 0.12s ease; z-index: 30;
  box-shadow: 0 4px 16px rgba(0,0,0,0.45);
}
/* :focus (not :focus-visible) is load-bearing here -- tested live in
   mobile emulation: a tap sets :focus on the button but Chrome's
   :focus-visible heuristic does NOT match a pointer/touch-triggered
   focus, only a keyboard one, so the tooltip never showed on tap when
   this used :focus-visible (confirmed via document.activeElement
   .matches(':focus-visible') === false right after a tap). :focus-visible
   is kept for the outline below, which only matters for keyboard users
   anyway. */
.cell-tooltip:hover::after, .cell-tooltip:focus::after { opacity: 1; visibility: visible; }
/* Added 2026-09-15 (Henry): only a name/payee/etc. whose text is
   ACTUALLY clipped by the ellipsis above should be tappable at all --
   tapping a full, untruncated name and popping up a copy of the exact
   same text is confusing, not helpful. pcell()/subcell() mark every
   button they build with .cell-tooltip-auto; each page's
   applyTooltipTruncation() then measures the real rendered width and
   adds .cell-tooltip--static to the ones that don't need it, demoting
   them back to plain, inert text (see candidate-profile.html's script).
   The ieExpendCols Date column intentionally opts OUT of this (no
   .cell-tooltip-auto class) since its whole point is revealing a longer
   full date on tap, not un-truncating -- it stays tappable regardless of
   whether the short form happens to fit. */
.cell-tooltip--static { cursor: default; pointer-events: none; }
.cell-tooltip--static::after { content: none; }

.no-data {
  color: var(--text-muted); font-size: 13px; padding: 1.2rem 0; text-align: center;
}

/* ── page-header shell ── shared by candidate-profile.html and
   state-profile.html: a top-right "back" link, a mockup-data pill badge,
   and a flex row for the header's main content (name/office block, plus
   whatever sits opposite it — a photo on the candidate page, nothing yet
   on the state page). Promoted here once state-profile.html needed the
   exact same shell candidate-profile.html already had. */
.page-header { margin-bottom: 2.5rem; }
.back-link {
  display: block; font-size: 13px; color: var(--text-muted); text-decoration: none;
  text-align: right; margin-bottom: 1.2rem;
}
.back-link:hover { color: var(--text); }
.header-row {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 2rem; flex-wrap: wrap;
}
/* Site-wide fix (2026-09-05): without this, any element that toggles
   `hidden` via JS (el.hidden = true/false) but ALSO has a class rule
   setting `display` -- .mockup-badge, .incumbent-badge, both below --
   stays visible, because that class selector ties [hidden]'s specificity
   and wins on source order. Caught when a real incumbent:false candidate
   still showed the INCUMBENT badge, and candidate-profile.html's
   Mockup/Loading badge stayed visible even after a successful real fetch.
   !important guarantees hidden always wins regardless of what else
   targets the element, on this page and any other that uses .hidden. */
[hidden] { display: none !important; }

.mockup-badge {
  display: inline-block; font-size: 11px; font-weight: 600;
  letter-spacing: 1.2px; text-transform: uppercase;
  color: var(--text-muted); background: var(--surface-2);
  border: 1px solid var(--border); border-radius: 20px;
  padding: 4px 14px; margin-bottom: 1rem;
}
/* ── full-page loading gate (2026-09-06, replaces the earlier
   render-mockup-immediately-then-swap approach from the same day) --
   user didn't want to see placeholder/mockup content (a fake candidate
   name, fake numbers) flash before the real page is ready, so
   #pageContainer starts `hidden` right in the HTML and nothing renders
   into it until bootstrap()'s reveal() runs -- see that function's own
   comment in candidate-profile.html. This is the ONLY thing visible
   (besides <nav>) until then. ── */
.page-loader {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 18px; min-height: 60vh; padding: 4rem 0;
}
.page-loader-spinner {
  width: 34px; height: 34px; border: 3px solid var(--border-hl); border-top-color: var(--green);
  border-radius: 50%; animation: spin-loader 0.7s linear infinite;
}
@keyframes spin-loader { to { transform: rotate(360deg); } }
/* just the spinner animates now -- user found spinner + animated dots
   together too busy (2026-09-06), so this is a static "Loading…" and
   the ring above is the only moving thing. */
.page-loader-text { font-size: 13px; color: var(--text-muted); letter-spacing: 0.3px; }

/* ── donut chart ── conic-gradient ring + a legend (swatch/label/$/%),
   shared by candidate-profile.html (Contributor Types) and
   state-profile.html (Money by Race Type, Money by Party). Sits inside a
   .card, usually within a .demo-grid 3-up (or narrower) row. */
.donut-wrap { display: flex; align-items: center; gap: 1.4rem; }
.donut {
  width: 128px; height: 128px; border-radius: 50%; flex-shrink: 0;
  position: relative;
}
.donut::after {
  content: ""; position: absolute; inset: 20px; border-radius: 50%; background: var(--surface);
}
/* .donut-center/-value/-label removed 2026-09-06 -- candidate-profile.html's
   Contributor Types donut used to show a dollar total in the hole
   (state-profile.html's Money by Race Type/Party donuts never did); user
   asked to drop it, which also brings this donut in line with the
   shared, center-text-free style state-profile.html already uses. */
.legend { display: flex; flex-direction: column; gap: 8px; flex: 1; min-width: 0; }
.legend-row { display: flex; align-items: center; gap: 8px; font-size: 12.5px; }
.legend-swatch { width: 9px; height: 9px; border-radius: 2px; flex-shrink: 0; }
.legend-label { flex: 1; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.legend-value { font-weight: 600; white-space: nowrap; }
.legend-pct { color: var(--text-muted); font-size: 11.5px; width: 38px; text-align: right; flex-shrink: 0; }

/* ── party color system ── shared by candidate-profile.html (one big badge
   next to a single candidate's name) and state-profile.html (many
   candidates at once, in race cards and a legislative table — hence the
   compact .party-chip variant below, same three colors, letter instead of
   full party name). Promoted here from candidate-profile.html once a
   second page needed the same colors. */
.party-badge {
  display: inline-block; font-size: 11px; font-weight: 600; letter-spacing: 0.4px;
  border-radius: 20px; padding: 3px 11px; border: 1px solid transparent;
}
.party-dem { color: #7db3f2; background: #3987e522; border-color: #3987e544; }
.party-rep { color: #f29a9a; background: #e6676722; border-color: #e6676744; }
.party-other { color: #b8b0f2; background: #9085e922; border-color: #9085e944; }
/* Libertarian gets its own color (added 2026-09-10, at Henry's request --
   general site rule now) rather than falling into .party-other's purple
   alongside independents/no-party -- same light-on-dark pastel convention
   as the three colors above it (text color, background at ~13% alpha,
   border at ~27%). */
.party-lib { color: #e8c93c; background: #e8c93c22; border-color: #e8c93c44; }
/* Green Party: a yellow-green, kept apart from the site's own accent green (#4caf7d) */
.party-green { color: #8fd16a; background: #8fd16a22; border-color: #8fd16a44; }
/* sits under an office/party line — amber/orange to read as a status flag
   distinct from the party badge's blue/red pairing */
.incumbent-badge {
  display: inline-block; margin-top: 10px; font-size: 11px; font-weight: 700;
  letter-spacing: 0.5px; text-transform: uppercase;
  color: #f2a93c; background: #f2a93c1c; border: 1px solid #f2a93c4a;
  border-radius: 20px; padding: 3px 11px;
}
/* ── off-ballot status (added 2026-09-09) ──
   For a race participant race-profile.html knows for certain is NOT on
   the current ballot (races.onBallot === false -- Green Papers-sourced,
   e.g. a term-limited incumbent who didn't file to run again). Distinct
   from .incumbent-badge's amber "currently holds this office" meaning:
   this is "not contesting THIS race", so it gets its own neutral gray
   treatment rather than reusing that color. Never applied when onBallot
   is null (no signal either way, e.g. every legislative race and any
   executive race Green Papers hasn't covered yet) -- see
   race-profile.html's renderCandidateCards(). */
.not-on-ballot-badge {
  display: inline-block; margin-top: 10px; font-size: 11px; font-weight: 700;
  letter-spacing: 0.5px; text-transform: uppercase;
  color: var(--text-muted); background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 20px; padding: 3px 11px;
}
.candidate-card.off-ballot { opacity: 0.55; }
.candidate-card.off-ballot:hover { opacity: 0.85; }
/* compact single-letter version of the same three party colors, for tight
   spaces (race cards, a legislative table's Candidates column) where a
   full "Democratic"-length pill would wrap or crowd the row. */
.party-chip {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; flex-shrink: 0;
  font-size: 10px; font-weight: 700; border-radius: 5px; border: 1px solid transparent;
}
.party-chip.party-dem { color: #7db3f2; background: #3987e522; border-color: #3987e544; }
.party-chip.party-rep { color: #f29a9a; background: #e6676722; border-color: #e6676744; }
.party-chip.party-other { color: #b8b0f2; background: #9085e922; border-color: #9085e944; }
.party-chip.party-lib { color: #e8c93c; background: #e8c93c22; border-color: #e8c93c44; }
.party-chip.party-green { color: #8fd16a; background: #8fd16a22; border-color: #8fd16a44; }

/* ── pagination (opt-in via renderToggleTable's opts.pageSize) ── */
.table-pagination {
  display: flex; align-items: center; justify-content: center; gap: 12px;
  margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border);
}
.page-btn {
  display: flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 6px; color: var(--text-muted); font-size: 14px; line-height: 1;
  cursor: pointer; transition: all 0.15s;
}
.page-btn:hover:not(:disabled) { color: var(--text); border-color: var(--border-hl); }
.page-btn:disabled { opacity: 0.3; cursor: default; }
.page-indicator { font-size: 11.5px; color: var(--text-muted); min-width: 40px; text-align: center; }

/* ── search/filter box ── a plain text input for client-side filtering
   (state-profile.html's district/candidate search, index.html's state
   directory search). Promoted here once a second page needed it. */
.table-search-row { margin-bottom: 1rem; }
.table-search {
  width: 100%; max-width: 340px; padding: 8px 12px; font: inherit; font-size: 13px;
  background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text);
}
.table-search::placeholder { color: var(--text-muted); }
.table-search:focus { outline: none; border-color: var(--border-hl); }
.table-search-empty { padding: 2rem 0; text-align: center; color: var(--text-muted); font-size: 13px; }

/* ── polling ── one row per poll, a labeled bar per candidate (plus
   Undecided) inside it. Originally built for candidate-profile.html
   (a single candidate vs. one opponent) but moved here unused when
   polling was pulled off that page — a race has several candidates'
   numbers together, which fits race-profile.html instead. Promoted to
   this shared file now that a second page needs it. */
.poll-row {
  display: flex; align-items: center; gap: 1.2rem; padding: 12px 0;
  border-bottom: 1px solid var(--border);
}
.poll-row:last-child { border-bottom: none; }
.poll-meta { flex: 0 0 190px; }
.poll-source { font-size: 13px; font-weight: 600; }
.poll-date { font-size: 11.5px; color: var(--text-muted); margin-top: 2px; }
.poll-bars { flex: 1; display: flex; flex-direction: column; gap: 6px; }
.poll-bar-line { display: flex; align-items: center; gap: 10px; }
.poll-bar-label { flex: 0 0 150px; font-size: 12.5px; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.poll-bar-track { flex: 1; height: 8px; background: var(--surface-3); border-radius: 4px; overflow: hidden; }
.poll-bar-fill { height: 100%; border-radius: 4px; }
.poll-bar-pct { flex: 0 0 44px; text-align: right; font-size: 12.5px; font-weight: 600; }
/* .poll-row's fixed-width columns (.poll-meta 190px + .poll-bar-label
   150px + .poll-bar-pct 44px, none allowed to shrink -- flex: 0 0 Npx)
   add up to well over a phone card's ~340px inner width on their own,
   before the actual bar track gets anything -- the %s (and everything
   else) end up crushed/overlapping. Stack meta above the bars instead
   of beside them, and shrink the label/pct columns to widths that
   actually fit. Placed here, right after the base rules, on purpose --
   the very first mobile @media block up top is EARLIER in the file, so
   at equal specificity it would lose the cascade tie to these later
   base rules regardless of being inside @media (confirmed live, Henry,
   2026-09-16 -- the first attempt silently did nothing). */
@media (max-width: 640px) {
  .poll-row { flex-direction: column; align-items: stretch; gap: 8px; }
  .poll-meta { flex: 0 0 auto; }
  .poll-bar-label { flex: 0 0 72px; }
  .poll-bar-track { min-width: 0; }
  .poll-bar-pct { flex: 0 0 38px; }
}

/* ── magnitude bars (size breakdown / top states / money-by-candidate) ──
   Promoted here once race-profile.html needed the same labeled-bar-list
   shape candidate-profile.html already used. */
.bar-list { display: flex; flex-direction: column; gap: 10px; }
.bar-row-item { display: flex; align-items: center; gap: 10px; }
.bar-item-label { flex: 0 0 100px; font-size: 12.5px; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.bar-item-track { flex: 1; height: 10px; background: var(--surface-3); border-radius: 4px; overflow: hidden; }
.bar-item-fill { height: 100%; border-radius: 4px; }
.bar-item-value { flex: 0 0 118px; text-align: right; font-size: 12.5px; font-weight: 600; }
/* share of a page's own total (DATA.summary.totalRaised, etc.) — some
   callers pass an exhaustive partition (rows sum to the total exactly),
   others just the top N of a larger set; either way this reads as "share
   of everything," not "share of the rows shown here." */
/* On its own line under the dollar amount rather than inline beside it
   (Henry, 2026-09-15) -- .bar-item-value is a fixed 118px column, and a
   real (non-mockup) 7-figure contributor total plus " 60.8%" doesn't fit
   on one line at that width, so the percent was wrapping underneath on
   its own anyway, just as an accidental line-break instead of a designed
   one. Stacking it on purpose means it no longer depends on how many
   digits the dollar amount happens to have. */
.bar-item-pct { display: block; color: var(--text-muted); font-weight: 500; font-size: 11px; margin-top: 2px; }

/* ── funding sources (2026-09-29, candidate-profile.html) ── two-line donut
   legend, donor-kind badges (same hues as the donut: --c2 PAC, --c3 party,
   --c5 other; dashed border = another candidate's committee), the
   Individual Donors self-funding note, and the collapsible Other receipts
   section. */
.legend-row.fs-row { align-items: flex-start; }
.legend-row.fs-row .legend-swatch { margin-top: 4px; }
.fs-text { min-width: 0; line-height: 1.3; }
.fs-label { color: var(--text-muted); font-size: 12.5px; }
.fs-value { color: var(--text-muted); font-size: 11.5px; font-variant-numeric: tabular-nums; }
.fs-value b { color: var(--text); font-weight: 600; font-size: 12.5px; }
.fs-badge { display: inline-block; margin-top: 3px; font-size: 10px; font-weight: 600; letter-spacing: .3px;
  border-radius: 4px; padding: 1px 6px; border: 1px solid transparent; white-space: nowrap; }
.fs-pac   { color: #f0a07e; background: #d9592622; border-color: #d9592644; }
.fs-cand  { border-style: dashed; border-color: #d9592677; }
.fs-party { color: #6fd3ae; background: #199e7022; border-color: #199e7044; }
.fs-org   { color: #ee9ab8; background: #d5518122; border-color: #d5518144; }
.fs-public { color: #c3c9d5; background: #8a93a622; border-color: #8a93a655; }
.table-note { font-size: 11.5px; color: var(--text-muted); margin: -4px 0 8px; line-height: 1.35; }
details.other-receipts { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); }
details.other-receipts summary { list-style: none; cursor: pointer; padding: 0.9rem 1.3rem; display: flex; gap: 12px;
  justify-content: space-between; flex-wrap: wrap; font-size: 13px; font-weight: 600; color: var(--text); }
details.other-receipts summary::-webkit-details-marker { display: none; }
details.other-receipts summary::before { content: "▸"; color: var(--text-muted); margin-right: 8px; display: inline-block; transition: transform .15s; }
details.other-receipts[open] summary::before { transform: rotate(90deg); }
details.other-receipts summary > span:first-child { margin-right: auto; }
details.other-receipts summary:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
details.other-receipts .or-note { font-weight: 400; color: var(--text-muted); font-size: 12px; margin-left: 6px; }
details.other-receipts .or-sum { font-weight: 400; color: var(--text-muted); font-size: 12px; }
details.other-receipts .or-body { padding: 0 1.3rem 1rem; }
table.dtable td.kindcol, table.dtable th.kindcol { width: 220px; color: var(--text-muted); }
@media (max-width: 640px) {
  table.dtable td.kindcol, table.dtable th.kindcol { width: 110px; }
  details.other-receipts summary, details.other-receipts .or-body { padding-left: 0.5rem; padding-right: 0.5rem; }
}

/* ── race page funding mix (2026-10-01, race-profile.html) ── a 2px line
   above Raised on each candidate card showing that candidate's own mix
   (full card width), and a small legend beside the Candidates title. */
.fmix-line { display: flex; height: 2px; border-radius: 1px; overflow: hidden; margin: 0 0 8px; background: var(--surface-3); }
.fmix-line span { display: block; height: 100%; }
.section-title.with-legend { display: flex; align-items: baseline; gap: 16px; flex-wrap: wrap; }
.fmix-legend { display: inline-flex; flex-wrap: wrap; gap: 4px 12px; font-size: 11px; font-weight: 400; letter-spacing: 0; text-transform: none; color: var(--text-muted); }
.fmix-legend i { display: inline-block; width: 8px; height: 8px; border-radius: 2px; margin-right: 4px; }
.subtext .fs-badge { margin-top: 0; padding: 0 5px; vertical-align: 1px; }
.rs-caption { margin-top: 14px; }

/* ── not-found / unavailable states (2026-10-02) -- rendered by
   page-states.js between <nav> and <footer> in place of the page
   content, and used directly by 404.html. Same footprint as
   .page-loader so the footer doesn't jump when one replaces the other. ── */
.page-state {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  min-height: 60vh; padding: 4rem 1.5rem; text-align: center;
}
.page-state-number {
  font-size: 13px; font-weight: 600; letter-spacing: 2px; color: var(--text-muted);
  border: 1px solid var(--border-hl); border-radius: 999px; padding: 3px 12px; margin-bottom: 20px;
}
.page-state-title { font-size: 26px; font-weight: 600; color: var(--text); margin: 0 0 10px; }
.page-state-text { font-size: 15px; color: var(--text-muted); margin: 0 0 26px; max-width: 46ch; line-height: 1.5; }
.page-state-actions { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
.page-state-btn {
  font: inherit; font-size: 14px; font-weight: 500; cursor: pointer; text-decoration: none;
  color: var(--bg); background: var(--green); border: 1px solid var(--green);
  border-radius: var(--radius); padding: 9px 18px;
}
.page-state-btn:hover { filter: brightness(1.1); }
.page-state-btn--quiet { color: var(--text); background: transparent; border-color: var(--border-hl); }
.page-state-btn--quiet:hover { border-color: var(--text-muted); filter: none; }
.page-state-ref {
  margin-top: 34px; font-size: 12px; color: var(--text-muted); opacity: 0.7;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; user-select: all;
}

/* Footer on narrow screens (2026-10-02): with five items the single row
   squeezed each link onto two lines and pushed the page sideways below
   ~350px. Links wrap as whole words and the credit takes its own line. */
@media (max-width: 600px) {
  footer { flex-wrap: wrap; justify-content: center; gap: 0.7rem 1.4rem; }
  footer a { white-space: nowrap; }
  .footer-credit { flex-basis: 100%; text-align: center; }
}
