/* =========================================================================
   2026 Senate Forecast — dashboard styles
   No framework, no build step. Dark by default, light via prefers-color-scheme.
   ========================================================================= */

:root {
  --dem: #4f8ef7;
  --dem-soft: rgba(79, 142, 247, 0.18);
  --dem-line: #7fb0ff;
  --rep: #ef5350;
  --rep-soft: rgba(239, 83, 80, 0.18);
  --rep-line: #ff8a80;

  --bg: #0e1116;
  --bg-elev: #161b22;
  --bg-elev-2: #1c232c;
  --border: #262d38;
  --border-strong: #39434f;
  --on-dem: #0e1116;   /* 5.89:1 on --dem */
  --text: #e6edf3;
  --text-dim: #9aa7b4;
  --text-faint: #838f9b;
  --accent: #f0b429;

  /* Diverging rating scale for the map.
   *
   * Five steps, two hues either side of a neutral grey midpoint. Validated
   * against the chart surface with the data-viz six checks: worst normal-vision
   * separation among the four solid steps is dE 19.1 and worst CVD separation
   * 14.5, with every step clearing 3:1 contrast on this surface.
   *
   * Seven steps were tried first and failed: near the midpoint the adjacent
   * pairs fell to dE 4-10, well under the 15 floor, which means readers with
   * ordinary colour vision could not separate Lean from Likely. Five steps is
   * what the colour space actually supports here.
   *
   * scripts/check_palette.py re-measures all of this against these values and
   * runs in the test suite, so the finding is checkable rather than a claim in
   * a comment. Point its ORDER at a seven-step ramp and it fails again.
   *
   * Because five buckets have to cover 80-100%, the top one is labelled
   * "Strong", not "Safe" -- see the RATINGS comment in map.js.
   *
   * Toss-up additionally carries a diagonal hatch, so it never depends on
   * colour alone -- it is the one category that sits between the two hues and
   * therefore the one most at risk of being misread. */
  --safe-r: #e03a34;
  --lean-r: #ff9c96;
  --tossup: #79838f;
  --lean-d: #9cc0ff;
  --safe-d: #3b7ae8;
  --no-race: #232a34;
  --no-race-stroke: #2e3745;
  --map-stroke: #0e1116;
  --map-ink: #0e1116;

  --radius: 10px;
  --wrap: 1180px;

  /* --- Type scale -----------------------------------------------------------
   * Twelve steps, and nothing off them.
   *
   * This stylesheet used to carry 31 distinct font sizes: 13px beside 13.5px
   * beside 12.5px, each individually defensible and collectively arbitrary. A
   * constrained scale is not about having fewer sizes for its own sake -- it is
   * that when the only choice is the step above or below, the relationships
   * between elements stay legible instead of drifting apart half a pixel at a
   * time.
   *
   * `--fs-micro` earns its place for exactly one job: the state initials on the
   * 435-tile cartogram, where the next step up overlaps its neighbours. */
  --fs-micro: 9px;
  --fs-3xs: 10px;
  --fs-2xs: 11px;
  --fs-xs: 12px;
  --fs-sm: 13px;
  --fs-md: 14px;
  --fs-base: 15px;
  --fs-lg: 17px;
  --fs-xl: 21px;
  --fs-2xl: 25px;
  --fs-3xl: 34px;
  --fs-4xl: 46px;

  /* --- Spacing scale --------------------------------------------------------
   * A 4px grid, with 2px half-steps at the small end where the difference
   * between 6 and 8 is genuinely visible.
   *
   * Applied to page rhythm -- card padding, section gaps, prose margins, the
   * spaces a reader perceives as structure. Deliberately NOT applied to
   * component geometry that carries a comment explaining its exact pixel
   * budget: the race-row grid columns at 375px, the market card's 300px flex
   * basis, the cartogram stroke widths. Those numbers were measured against a
   * real constraint, and rounding them to the nearest scale step would quietly
   * undo the measurement. */
  --s-1: 2px;
  --s-2: 4px;
  --s-3: 6px;
  --s-4: 8px;
  --s-5: 12px;
  --s-6: 16px;
  --s-7: 20px;
  --s-8: 24px;
  --s-9: 32px;
  --s-10: 48px;

  /* Comfortable line length for running text.
   *
   * Butterick puts it at 45-90 characters. Measured on the live page, 99 of 146
   * text blocks were over 90 and the worst ran to 175 -- a full-width paragraph
   * on an 1180px card is close to twice the ceiling, and it is the methodology
   * prose, the longest text on the page, that suffered most.
   *
   * Charts, tables and maps still take the full width. Only running text is
   * capped, because only running text is read line after line. */
  --measure: 68ch;

  /* --- Elevation ------------------------------------------------------------
   * Depth on a dark surface comes mostly from the surface step itself
   * (--bg-elev sitting above --bg); a shadow there is a supporting hint, not
   * the mechanism. The light theme redefines all three with real shadows,
   * because on light backgrounds the shadow is doing the whole job. */
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.30);
  --shadow-md: 0 2px 8px rgba(0, 0, 0, 0.35);
  --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.35);
  --mono: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;
  --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, Helvetica, Arial, sans-serif;
}

/* Light palette.
 *
 * Declared twice on purpose, because the page has to resolve correctly in three
 * states, not two:
 *   - no explicit choice  -> only prefers-color-scheme separates light from dark
 *   - data-theme="light"  -> an explicit light choice must beat a dark OS
 *   - data-theme="dark"   -> the dark :root above already wins
 *
 * The GitHub Pages deployment never sees a data-theme attribute, so only the
 * media query matters there. The standalone bundle (`midterms bundle`) can be
 * embedded in viewers that do stamp data-theme, and this makes it behave.
 * Components read tokens only, never colours declared inside these blocks. */
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
    --bg: #f6f7f9;
    --bg-elev: #ffffff;
    --bg-elev-2: #f0f2f5;
    --border: #dde1e7;
  --border-strong: #c3cad3;
  --on-dem: #ffffff;   /* 5.74:1 on --dem */
    --text: #12171d;
    --text-dim: #56616d;
    --text-faint: #616b76;

  /* Amber reads at 8.49:1 on the dark surfaces and 1.74:1 on the light ones,
   * which is not a colour -- it is an absence. It carries the 51-seat majority
   * line on the seat histogram, the countdown, `.diag-warn` and the coverage
   * target, so in light mode those were all but invisible. Same hue, walked
   * down in lightness until it clears 4.5:1 on bg, bg-elev and bg-elev-2. */
  --accent: #8a630a;

  --shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06);
  --shadow-md: 0 2px 8px rgba(16, 24, 40, 0.08), 0 1px 2px rgba(16, 24, 40, 0.06);
  --shadow-lg: 0 12px 32px rgba(16, 24, 40, 0.12), 0 2px 6px rgba(16, 24, 40, 0.06);

    --dem: #1d5fd6;
    --dem-line: #1d5fd6;
    --rep: #cc2f2c;
    --rep-line: #cc2f2c;
    --dem-soft: rgba(29, 95, 214, 0.14);
    --rep-soft: rgba(204, 47, 44, 0.14);
  --safe-r: #a01813;
    --lean-r: #d9605a;
    --tossup: #6b7583;
    --lean-d: #5b86d6;
    --safe-d: #123f96;
    --no-race: #e8ebef;
    --no-race-stroke: #d5dae1;
    --map-stroke: #ffffff;
    --map-ink: #ffffff;
  }
}

:root[data-theme="light"] {
  --bg: #f6f7f9;
  --bg-elev: #ffffff;
  --bg-elev-2: #f0f2f5;
  --border: #dde1e7;
  --border-strong: #c3cad3;
  --on-dem: #ffffff;   /* 5.74:1 on --dem */
  --text: #12171d;
  --text-dim: #56616d;
  --text-faint: #616b76;

  /* Amber reads at 8.49:1 on the dark surfaces and 1.74:1 on the light ones,
   * which is not a colour -- it is an absence. It carries the 51-seat majority
   * line on the seat histogram, the countdown, `.diag-warn` and the coverage
   * target, so in light mode those were all but invisible. Same hue, walked
   * down in lightness until it clears 4.5:1 on bg, bg-elev and bg-elev-2. */
  --accent: #8a630a;

  --shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06);
  --shadow-md: 0 2px 8px rgba(16, 24, 40, 0.08), 0 1px 2px rgba(16, 24, 40, 0.06);
  --shadow-lg: 0 12px 32px rgba(16, 24, 40, 0.12), 0 2px 6px rgba(16, 24, 40, 0.06);

  --dem: #1d5fd6;
  --dem-line: #1d5fd6;
  --rep: #cc2f2c;
  --rep-line: #cc2f2c;
  --dem-soft: rgba(29, 95, 214, 0.14);
  --rep-soft: rgba(204, 47, 44, 0.14);
  --safe-r: #a01813;
  --lean-r: #d9605a;
  --tossup: #6b7583;
  --lean-d: #5b86d6;
  --safe-d: #123f96;
  --no-race: #e8ebef;
  --no-race-stroke: #d5dae1;
  --map-stroke: #ffffff;
  --map-ink: #ffffff;
}

* { box-sizing: border-box; }

/* The `hidden` attribute must always win.
 *
 * A class rule that sets `display` is author-origin and silently beats the
 * browser's own `[hidden] { display: none }`. That is what happened to the race
 * drawer: `.drawer { display: flex }` kept a fixed, full-viewport, z-index 50
 * overlay painted over the whole page even while `hidden` was set, so nothing
 * underneath could be clicked. Declaring it globally kills the whole bug class
 * rather than just this one instance. */
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans);
  font-size: var(--fs-base);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

.wrap { max-width: var(--wrap); margin: 0 auto; padding: 0 var(--s-7); }
.subtle { color: var(--text-faint); }
a { color: var(--dem-line); }

/* ---------- header ---------- */
.site-header {
  border-bottom: 1px solid var(--border);
  background: var(--bg-elev);
  padding: var(--s-7) 0;
}
.header-inner {
  display: flex; justify-content: space-between;
  align-items: flex-end; gap: var(--s-8); flex-wrap: wrap;
}
h1 { margin: 0; font-size: clamp(var(--fs-xl), 5vw, var(--fs-2xl)); letter-spacing: -0.02em; }
.tagline { margin: var(--s-2) 0 0; color: var(--text-dim); font-size: var(--fs-md); }
.updated { text-align: right; font-size: var(--fs-sm); }
.updated-label { display: block; color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.06em; font-size: var(--fs-2xs); }
.updated-value { font-family: var(--mono); color: var(--text); }
.days-left { display: block; color: var(--accent); font-size: var(--fs-xs); margin-top: 2px; }

/* ---------- layout ---------- */
main { padding: var(--s-8) var(--s-7) var(--s-10); }
section { margin-bottom: var(--s-9); }

.card {
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--s-8);
}

/* ---------- elevation ----------
 *
 * Every card on this page looked identical: same surface, same 1px border, no
 * shadow anywhere. That is a page with no opinion about what matters -- the
 * headline probability and the pollster reference table asked for the same
 * amount of attention.
 *
 * Three tiers, in the order a reader should meet them:
 *
 *   .hero          the number they came for. Lifted, and given the stronger
 *                  border because on a dark surface a shadow does almost
 *                  nothing and the border is carrying the separation.
 *   .card          the working content. A hint of lift, no more.
 *   .card.method   reference material, read on purpose rather than noticed.
 *   #markets       explicitly not part of the forecast, so it should not
 *                  compete with it for attention.
 *
 * The light theme redefines all three shadow tokens with real, visible values,
 * because there the shadow is doing the whole job rather than assisting. */
.card { box-shadow: var(--shadow-sm); }
.hero { box-shadow: var(--shadow-lg); border-color: var(--border-strong); }
.card.method,
#markets { box-shadow: none; }

.card h2 { margin: 0 0 var(--s-2); font-size: var(--fs-lg); letter-spacing: -0.01em; }
.card h3 { margin: 0 0 var(--s-3); font-size: var(--fs-md); color: var(--text); }
/* The newsroom heading stack: kicker, finding, explanation.
 *
 * A chart headed "Distribution of Senate seats" tells a reader what they are
 * looking at and nothing about what it says -- they have to do the reading
 * themselves, and most will not. The Financial Times and Datawrapper both put
 * the finding in the headline and demote the description, which is the same
 * information in the order people actually consume it.
 *
 * The chart's name does not disappear, it becomes the kicker. This is a
 * dashboard rather than an article: someone scanning for "the seat chart" still
 * has to be able to find it, and a purely editorial headline would lose them.
 *
 * Every headline in this stack is written from the forecast in app.js, so it
 * cannot drift out of step with the numbers underneath it. */
.chart-kicker {
  margin: 0 0 var(--s-1);
  font-size: var(--fs-2xs);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 600;
  color: var(--text-faint);
}

.card-sub { margin: 0 0 var(--s-6); color: var(--text-dim); font-size: var(--fs-sm); }

.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-8); }
@media (max-width: 900px) { .grid-2 { grid-template-columns: 1fr; } }

/* ---------- measure ----------
 *
 * Running text stops at --measure; everything else keeps the full 1180px.
 *
 * The distinction is what is read line after line. A paragraph of methodology
 * has to carry the eye back to the start of the next line ~40 times, and at 175
 * characters that return sweep is where readers lose their place. A chart axis,
 * a map, a table of races -- those are scanned, not read, and narrowing them
 * would cost information for no gain.
 *
 * `.prob-note` is centred, so it needs the auto margins to stay centred once it
 * is narrower than its container. */
.tagline,
.card-sub,
.alert,
.commentary p,
.commentary ul,
.method-grid p,
.method-panel p,
.method-list,
.primer-item p,
.term-list dd,
.site-footer p { max-width: var(--measure); }

.prob-note { max-width: var(--measure); margin-left: auto; margin-right: auto; }

/* ---------- hero ---------- */
.hero { padding: var(--s-9) var(--s-8); }
.hero-probs {
  display: grid;
  grid-template-columns: 170px 1fr 170px;
  align-items: center; gap: var(--s-8);
}
@media (max-width: 760px) {
  .hero-probs { grid-template-columns: 1fr 1fr; }
  .prob-bar-wrap { grid-column: 1 / -1; order: 3; }
}
.prob-block { text-align: center; }
.prob-party { font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-dim); }
.prob-value { font-size: clamp(var(--fs-3xl), 11vw, var(--fs-4xl)); font-weight: 650; line-height: 1.05; letter-spacing: -0.03em; font-variant-numeric: tabular-nums; }
.prob-block.dem .prob-value { color: var(--dem); }
.prob-block.rep .prob-value { color: var(--rep); }
.prob-caption { font-size: var(--fs-xs); color: var(--text-faint); }

.prob-bar {
  display: flex; height: 34px; border-radius: 6px;
  overflow: hidden; border: 1px solid var(--border);
}
.prob-bar-dem { background: var(--dem); }
.prob-bar-rep { background: var(--rep); }
.prob-note { margin: var(--s-5) 0 0; text-align: center; font-size: var(--fs-sm); color: var(--text-dim); }

/* ---------- charts ---------- */
.chart { width: 100%; }
.chart svg { display: block; width: 100%; height: auto; overflow: visible; }
.flex-1 { flex: 1; min-width: 0; }

.axis-label { fill: var(--text-faint); font-size: var(--fs-3xs); font-family: var(--mono); }
/* A series's name, drawn at the end of its line.
 *
 * Sans rather than the mono the axis labels use: this is a name, not a number,
 * and it should not read as part of the data. The halo is what makes direct
 * labelling safe -- a label sits wherever its series happens to end, which may
 * be over a gridline, the uncertainty band, or the other line. */
.series-label {
  font-size: var(--fs-2xs);
  font-weight: 650;
  font-family: var(--sans);
  paint-order: stroke;
  stroke: var(--bg-elev);
  stroke-width: 3px;
  stroke-linejoin: round;
}
.grid-line { stroke: var(--border); stroke-width: 1; }
.threshold-line { stroke: var(--accent); stroke-width: 1.5; stroke-dasharray: 4 3; }
/* Haloed for the same reason .series-label is: a reference label sits wherever
   its reference value falls, which on a seat histogram is directly over the
   tallest bars in the distribution. */
.threshold-text {
  fill: var(--accent);
  font-size: var(--fs-3xs);
  font-family: var(--mono);
  paint-order: stroke;
  stroke: var(--bg-elev);
  stroke-width: 3px;
  stroke-linejoin: round;
}


/* ---------- national ---------- */
.national-row { display: flex; gap: 28px; align-items: center; }
@media (max-width: 760px) { .national-row { flex-direction: column; align-items: stretch; } }
.national-stat { text-align: center; min-width: 150px; }
.national-value { font-size: clamp(var(--fs-2xl), 7vw, var(--fs-3xl)); font-weight: 650; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
.national-label { font-size: var(--fs-xs); color: var(--text-dim); }
.national-range { font-size: var(--fs-xs); color: var(--text-faint); font-family: var(--mono); margin-top: 4px; }

/* ---------- commentary ---------- */
.commentary { font-size: var(--fs-md); }
.commentary .entry { border-left: 3px solid var(--border); padding-left: var(--s-6); margin-bottom: var(--s-7); }
.commentary .entry-date { font-family: var(--mono); font-size: var(--fs-xs); color: var(--text-faint); }
.commentary .headline { font-size: var(--fs-base); font-weight: 600; margin: 4px 0 10px; }
.commentary p { margin: 0 0 9px; color: var(--text-dim); }
.commentary ul { margin: 0 0 10px; padding-left: 18px; color: var(--text-dim); }
.commentary li { margin-bottom: 7px; }
.commentary strong { color: var(--text); font-weight: 600; }
.commentary .poll-line { font-family: var(--mono); font-size: var(--fs-xs); color: var(--text-faint); }
/* A poll line explains the race bullet above it, so indent it instead of
   letting it sit as a sibling bullet at the same level. */
.commentary li.poll-line { list-style: none; margin-left: 0.2em; padding-left: 0.9em; border-left: 2px solid var(--border); }
.archive { margin-top: 8px; }
.archive summary { cursor: pointer; color: var(--dem-line); font-size: var(--fs-sm); }
.archive[open] summary { margin-bottom: 16px; }

/* ---------- races ---------- */
.races-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; flex-wrap: wrap; }
.filters { display: flex; gap: 6px; }
.filter {
  background: var(--bg-elev-2); border: 1px solid var(--border); color: var(--text-dim);
  padding: 6px 12px; border-radius: 6px; font-size: var(--fs-xs); cursor: pointer; font-family: inherit;
}
.filter.active { background: var(--dem); border-color: var(--dem); color: var(--on-dem); }

.race-list { display: flex; flex-direction: column; gap: 1px; background: var(--border); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
.race-row {
  display: grid;
  grid-template-columns: 42px 1fr 96px 150px 92px 64px;
  align-items: center; gap: 12px;
  background: var(--bg-elev); padding: 11px 14px;
  cursor: pointer; border: none; width: 100%; text-align: left;
  font-family: inherit; font-size: var(--fs-md); color: var(--text);
}
.race-row:hover { background: var(--bg-elev-2); }
.race-head-row { background: var(--bg-elev-2); font-size: var(--fs-2xs); text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-faint); cursor: default; }
.race-head-row:hover { background: var(--bg-elev-2); }
@media (max-width: 820px) {
  .race-row { grid-template-columns: 38px 1fr 80px 80px; }
  .race-row .col-spark, .race-row .col-polls { display: none; }
}

/* Phones.
 *
 * At 820px the row still reserves 38 + 80 + 80 px of fixed columns plus gaps and
 * padding. On a 375px screen that leaves the race-name column about 27px, which
 * squeezes the name to nothing and pushes the margin column off the right edge.
 * Reclaim the space: trim the page and card gutters, shrink the fixed columns,
 * and step the type down one notch. */
@media (max-width: 520px) {
  main { padding: var(--s-6) var(--s-5) var(--s-10); }

  /* The header is a flex row that wraps on a phone, and `.updated` keeps the
     right alignment it needs beside the title on a wide screen. Wrapped, that
     leaves the date ragged against nothing -- it reads as an indent rather than
     as alignment. Below this width it stacks, so it should align with the title
     it now sits under. */
  .updated { text-align: left; }
  .card { padding: var(--s-6); }
  .hero { padding: var(--s-7) var(--s-6); }

  .race-row {
    grid-template-columns: 30px 1fr 58px 58px;
    gap: 7px; padding: 10px 9px; font-size: var(--fs-sm);
  }
  .race-prob { font-size: var(--fs-xs); }
  .race-margin { font-size: var(--fs-xs); }
  .race-meta { font-size: var(--fs-3xs); }
  .race-head-row { font-size: var(--fs-3xs); letter-spacing: 0.03em; }


  .drawer-panel { padding: 20px 14px; }
  .drawer-stats { gap: 8px; }
  .dstat { padding: 10px 6px; }
  .dstat-value { font-size: var(--fs-lg); }
  .dstat-label { font-size: var(--fs-3xs); letter-spacing: 0.02em; }

  /* The poll table has five columns and the panel is ~350px wide. Rather than
     let it scroll and hide the margin — the one number a reader came for —
     drop sample size and screen, keeping pollster, date and margin. */
  table.polls th:nth-child(3), table.polls td:nth-child(3),
  table.polls th:nth-child(4), table.polls td:nth-child(4) { display: none; }
}

.race-code { font-family: var(--mono); font-weight: 600; color: var(--text-dim); }
.race-name { font-weight: 500; }
.race-meta { font-size: var(--fs-2xs); color: var(--text-faint); }
.race-prob { font-variant-numeric: tabular-nums; font-weight: 600; text-align: right; font-family: var(--mono); }
.race-margin { font-family: var(--mono); font-size: var(--fs-sm); text-align: right; }
.race-polls { font-family: var(--mono); font-size: var(--fs-xs); color: var(--text-faint); text-align: right; }
.pbar { height: 7px; border-radius: 4px; background: var(--rep-soft); overflow: hidden; }
.pbar > div { height: 100%; background: var(--dem); }
.dem-text { color: var(--dem); }
.rep-text { color: var(--rep); }

/* ---------- drawer ---------- */
.drawer { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex; justify-content: flex-end; z-index: 50; }
.drawer-panel {
  background: var(--bg-elev); width: min(620px, 100%); height: 100%;
  overflow-y: auto; padding: 26px; border-left: 1px solid var(--border); position: relative;
}
.drawer-close { position: absolute; top: 14px; right: 18px; background: none; border: none; color: var(--text-dim); font-size: var(--fs-2xl); cursor: pointer; line-height: 1; }
.drawer h2 { margin: 0 0 16px; font-size: var(--fs-xl); padding-right: 40px; }
.drawer-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 20px; }
.dstat { background: var(--bg-elev-2); border-radius: 8px; padding: 12px; text-align: center; }
.dstat-value { font-size: var(--fs-xl); font-weight: 650; font-variant-numeric: tabular-nums; }
.dstat-label { font-size: var(--fs-2xs); color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.05em; }
.note { background: var(--bg-elev-2); border-left: 3px solid var(--accent); padding: 10px 14px; font-size: var(--fs-sm); color: var(--text-dim); margin-bottom: 14px; border-radius: 0 6px 6px 0; }

.table-scroll { overflow-x: auto; }
table.polls { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
table.polls th { text-align: left; color: var(--text-faint); font-weight: 500; font-size: var(--fs-2xs); text-transform: uppercase; letter-spacing: 0.05em; padding: 6px 8px; border-bottom: 1px solid var(--border); }
table.polls td { padding: 7px 8px; border-bottom: 1px solid var(--border); }
table.polls td.num { font-family: var(--mono); text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.tag { display: inline-block; font-size: var(--fs-3xs); padding: 1px 5px; border-radius: 3px; margin-left: 5px; vertical-align: middle; }
.tag.d { background: var(--dem-soft); color: var(--dem-line); }
.tag.r { background: var(--rep-soft); color: var(--rep-line); }

/* ---------- method / diagnostics ---------- */
.method-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-8); }
@media (max-width: 820px) { .method-grid { grid-template-columns: 1fr; } }
.method-grid p { color: var(--text-dim); font-size: var(--fs-sm); margin: 0 0 9px; }
.formula { font-family: var(--mono); background: var(--bg-elev-2); padding: 9px 12px; border-radius: 6px; color: var(--text) !important; font-size: var(--fs-sm); }

.diagnostics { margin-top: var(--s-8); border-top: 1px solid var(--border); padding-top: var(--s-6); }
.diag-grid { display: flex; flex-wrap: wrap; gap: 22px; font-size: var(--fs-sm); }
.diag-item { font-family: var(--mono); color: var(--text-dim); }
.diag-item b { color: var(--text); font-weight: 600; }
.diag-ok { color: #3fb950; }
.diag-warn { color: var(--accent); }

/* ---------- footer / error ---------- */
.site-footer { border-top: 1px solid var(--border); padding: var(--s-8) 0 var(--s-9); color: var(--text-faint); font-size: var(--fs-sm); }
.site-footer p { margin: 0 0 6px; }
.error-banner { background: var(--rep-soft); border: 1px solid var(--rep); color: var(--text); padding: 16px; border-radius: 8px; margin: 24px auto; font-size: var(--fs-md); }

/* A standing caveat on the forecast itself, not a load failure. Warmer than
   .error-banner because nothing is broken -- the page works and the number is
   real, it is the input that is known to be stale. It must still be impossible
   to scroll past, which is why it sits above the map rather than under it. */
.alert {
  background: var(--tossup-soft, rgba(180, 140, 40, 0.12));
  border: 1px solid var(--tossup, #b48c28);
  color: var(--text);
  padding: 14px 16px;
  border-radius: 8px;
  margin: 0 0 18px;
  font-size: var(--fs-sm);
  line-height: 1.5;
}
.alert strong { display: block; margin-bottom: 4px; }

/* ---------- map ---------- */
.map-block { margin-top: var(--s-8); border-top: 1px solid var(--border); padding-top: var(--s-7); }
.map-title { margin: 0 0 var(--s-2); font-size: var(--fs-lg); letter-spacing: -0.01em; }
.map-holder { position: relative; }
.us-map svg { display: block; width: 100%; height: auto; }

.state {
  stroke: var(--map-stroke);
  stroke-width: 0.9;
  stroke-linejoin: round;
  transition: filter 120ms ease, opacity 120ms ease;
}
.state.no-race { fill: var(--no-race); stroke: var(--no-race-stroke); }
.state.safe-r  { fill: var(--safe-r); }
.state.lean-r  { fill: var(--lean-r); }
.state.tossup  { fill: var(--tossup); }
.state.lean-d  { fill: var(--lean-d); }
.state.safe-d  { fill: var(--safe-d); }
/* Toss-up never relies on colour alone. */
.state.tossup  { fill: url(#hatch-tossup); }

.state.interactive { cursor: pointer; }
.state.interactive:hover,
.state.interactive:focus-visible { filter: brightness(1.18); outline: none; }
/* A halo rather than a stroke change, so the state's own outline stays put. */
.state.is-active { stroke: var(--text); stroke-width: 2; paint-order: stroke; }

.map-label {
  fill: var(--map-ink);
  font-family: var(--mono);
  font-size: var(--fs-2xs);
  font-weight: 600;
  text-anchor: middle;
  dominant-baseline: central;
  pointer-events: none;
  paint-order: stroke;
}
.map-label.on-muted { fill: var(--text-faint); }
.map-leader { stroke: var(--text-faint); stroke-width: 0.8; fill: none; opacity: 0.65; }
.map-chip-box { stroke: var(--map-stroke); stroke-width: 0.9; }

/* ---------------------------------------------------------------------------
   District cartogram (House)
   --------------------------------------------------------------------------- */

/* One square per district. The stroke is thinner than the state map's because
   at 435 tiles a 0.9 stroke is a visible fraction of each square -- the grid
   would read as a mesh with colour trapped inside it rather than as a field of
   colour. */
.district {
  stroke: var(--map-stroke);
  stroke-width: 0.5;
  transition: filter 120ms ease, opacity 120ms ease;
}
.district.no-race { fill: var(--no-race); stroke: var(--no-race-stroke); }
.district.safe-r  { fill: var(--safe-r); }
.district.lean-r  { fill: var(--lean-r); }
.district.tossup  { fill: url(#hatch-tossup-house); }
.district.lean-d  { fill: var(--lean-d); }
.district.safe-d  { fill: var(--safe-d); }

/* Districts with no polling of their own, which is roughly nine in ten. Their
   forecast is real but rests on the district's presidential lean and the
   national swing rather than on anyone having asked voters there, and the eye
   should be able to see how much of the chamber that covers. Opacity rather
   than a separate palette, so the rating still reads at a glance and the two
   pieces of information do not compete for the same channel. */
.district.unpolled { opacity: 0.55; }

.district.interactive { cursor: pointer; }
.district.interactive:hover,
.district.interactive:focus-visible {
  filter: brightness(1.18); opacity: 1; outline: none;
}
.district.is-active { stroke: var(--text); stroke-width: 1.4; paint-order: stroke; }

.district-state-label {
  fill: var(--text-faint);
  font-family: var(--mono);
  font-size: var(--fs-micro);
  font-weight: 600;
  text-anchor: middle;
  pointer-events: none;
}

.legend-swatch.unpolled-swatch { background: var(--lean-d); opacity: 0.55; }
.legend-note { color: var(--text-faint); }

/* ---------------------------------------------------------------------------
   Chamber switcher
   --------------------------------------------------------------------------- */

.chamber-switch {
  display: inline-flex;
  margin-top: 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
}
.chamber-tab {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--text-dim);
  font: inherit;
  font-size: var(--fs-sm);
  font-weight: 600;
  padding: 6px 16px;
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease;
}
.chamber-tab + .chamber-tab { border-left: 1px solid var(--border); }
.chamber-tab:hover { color: var(--text); }
.chamber-tab.active { background: var(--border); color: var(--text); }
.chamber-tab:focus-visible { outline: 2px solid var(--text); outline-offset: -2px; }

.map-tooltip {
  position: absolute;
  z-index: 20;
  min-width: 190px;
  max-width: 260px;
  background: var(--bg-elev-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px 12px;
  box-shadow: var(--shadow-lg);
  pointer-events: none;
  font-size: var(--fs-sm);
}
.tt-name { font-weight: 650; margin-bottom: 2px; }
.tt-meta { color: var(--text-faint); font-size: var(--fs-2xs); margin-bottom: 8px; }
.tt-row { display: flex; justify-content: space-between; gap: 12px; margin-bottom: 3px; }
.tt-row span:first-child { color: var(--text-dim); }
.tt-row span:last-child { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.tt-bar { height: 6px; border-radius: 3px; background: var(--rep-soft); overflow: hidden; margin: 8px 0 6px; }
.tt-bar > div { height: 100%; background: var(--dem); }
.tt-hint { color: var(--text-faint); font-size: var(--fs-2xs); margin-top: 6px; }

.map-legend {
  display: flex; flex-wrap: wrap; gap: 6px 14px;
  margin-top: 14px; font-size: var(--fs-xs); color: var(--text-dim);
  align-items: center;
}
.legend-item { display: inline-flex; align-items: center; gap: 6px; }
.legend-swatch {
  width: 16px; height: 12px; border-radius: 2px;
  border: 1px solid var(--map-stroke); display: inline-block;
}
.legend-swatch.safe-r { background: var(--safe-r); }
.legend-swatch.lean-r { background: var(--lean-r); }
.legend-swatch.tossup {
  background: var(--tossup);
  background-image: repeating-linear-gradient(45deg,
    rgba(255,255,255,0.28) 0 2px, transparent 2px 4px);
}
.legend-swatch.lean-d { background: var(--lean-d); }
.legend-swatch.safe-d { background: var(--safe-d); }
.legend-swatch.no-race { background: var(--no-race); border-color: var(--no-race-stroke); }

@media (max-width: 520px) {
  .map-block { margin-top: 18px; padding-top: 16px; }
  .map-legend { font-size: var(--fs-2xs); gap: 5px 10px; }
  .map-tooltip { min-width: 160px; max-width: 210px; font-size: var(--fs-xs); }
}

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

.map-hint { margin-top: 10px; font-size: var(--fs-xs); }

/* ---------- chart interactivity ---------- */
/* The tooltip is positioned inside the chart host, so the host must anchor it. */
.chart { position: relative; }

.axis-line { stroke: var(--border); stroke-width: 1; }

.crosshair {
  stroke: var(--text-faint);
  stroke-width: 1;
  stroke-dasharray: 3 3;
  pointer-events: none;
}
.hover-dot {
  fill: var(--bg-elev);
  stroke: var(--dem-line);
  stroke-width: 2.5;
  pointer-events: none;
}

.seat-bar { opacity: 0.9; transition: opacity 100ms ease; }
/* Dim the rest rather than brighten one, so the highlighted bar keeps its true
   colour and the reader is not asked to judge a shifted hue.
   Gated on a real hover device: on touch, :hover latches after a tap and would
   leave the whole chart dimmed with no way to clear it. */
@media (hover: hover) {
  .chart:hover .seat-bar { opacity: 0.45; }
}
.chart .seat-bar.is-active { opacity: 1; }

.chart-tip {
  position: absolute;
  z-index: 20;
  min-width: 190px;
  max-width: 270px;
  background: var(--bg-elev-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px 12px;
  box-shadow: var(--shadow-lg);
  pointer-events: none;
  font-size: var(--fs-sm);
}
.chart-tip .tt-name { font-weight: 650; margin-bottom: 2px; }
.chart-tip .tt-meta { color: var(--text-faint); font-size: var(--fs-2xs); margin-bottom: 8px; }
.chart-tip .tt-row { display: flex; justify-content: space-between; gap: 14px; margin-bottom: 3px; }
.chart-tip .tt-row span:first-child { color: var(--text-dim); }
.chart-tip .tt-row span:last-child { font-family: var(--mono); font-variant-numeric: tabular-nums; }

@media (max-width: 520px) {
  .chart-tip { min-width: 158px; max-width: 210px; font-size: var(--fs-xs); padding: 8px 10px; }
  .chart-tip .tt-row { gap: 8px; }
}

@media (prefers-reduced-motion: reduce) {
  .seat-bar { transition: none; }
}

/* ---------- methodology ---------- */
.method-nav {
  display: flex; flex-wrap: wrap; gap: 6px;
  margin: 4px 0 20px; border-bottom: 1px solid var(--border); padding-bottom: 12px;
}
.method-tab {
  background: var(--bg-elev-2); border: 1px solid var(--border); color: var(--text-dim);
  padding: 7px 13px; border-radius: 6px; font-size: var(--fs-sm); cursor: pointer;
  font-family: inherit;
}
.method-tab:hover { color: var(--text); }
.method-tab.active { background: var(--dem); border-color: var(--dem); color: var(--on-dem); }

.method-cols { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-9); }
@media (max-width: 820px) { .method-cols { grid-template-columns: 1fr; gap: 22px; } }
.method-panel p { color: var(--text-dim); font-size: var(--fs-sm); margin: 0 0 10px; }
.method-panel h3 { font-size: var(--fs-md); margin: 0 0 8px; }
.method-subhead { margin-top: 18px !important; }
.method-note { margin-top: 10px !important; font-size: var(--fs-xs); }

.method-list { margin: 0 0 10px; padding-left: 18px; color: var(--text-dim); font-size: var(--fs-sm); }
.method-list li { margin-bottom: 8px; }
.method-list strong, .method-panel p strong { color: var(--text); font-weight: 600; }

/* The equation, folded away.
 *
 * The model panel used to open with it, which meant the first thing a reader
 * met was four Greek letters. The prose above now says the same thing, and this
 * is here for the reader who wants the formal version -- closed by default, so
 * it is an offer rather than a toll gate. */
.method-details {
  margin-top: var(--s-6);
  border-top: 1px solid var(--border);
  padding-top: var(--s-5);
}
.method-details summary {
  cursor: pointer;
  color: var(--dem-line);
  font-size: var(--fs-sm);
  font-weight: 600;
}
.method-details[open] summary { margin-bottom: var(--s-5); }
.method-details p { color: var(--text-dim); font-size: var(--fs-sm); margin: 0 0 var(--s-5); max-width: var(--measure); }

/* Term definitions for the model equation. */
.term-list { margin: 0; display: grid; grid-template-columns: auto 1fr; gap: 6px 14px; }
.term-list dt {
  font-family: var(--mono); color: var(--dem-line); font-size: var(--fs-sm);
  white-space: nowrap; padding-top: 1px;
}
.term-list dd { margin: 0; color: var(--text-dim); font-size: var(--fs-sm); }

.method-table { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); margin-top: 4px; }
.method-table th {
  text-align: right; color: var(--text-faint); font-weight: 500; font-size: var(--fs-2xs);
  text-transform: uppercase; letter-spacing: 0.05em; padding: 5px 8px;
  border-bottom: 1px solid var(--border);
}
.method-table th:first-child { text-align: left; }
.method-table td { padding: 6px 8px; border-bottom: 1px solid var(--border); color: var(--text-dim); }
.method-table td:not(:first-child) {
  text-align: right; font-family: var(--mono); font-variant-numeric: tabular-nums;
  color: var(--text);
}
.method-table .was { color: var(--text-faint); text-decoration: line-through; }

.coverage-rows { margin: 0 0 12px; }
.coverage-row {
  display: grid; grid-template-columns: 74px 1fr 58px; align-items: center;
  gap: 10px; margin-bottom: 6px; font-size: var(--fs-xs); color: var(--text-dim);
}
.coverage-track {
  height: 8px; border-radius: 4px; background: var(--bg-elev-2);
  position: relative; overflow: hidden;
}
.coverage-fill { height: 100%; background: var(--dem); border-radius: 4px; }
/* The target sits as a notch on the track, so over- and under-coverage are
   both visible at a glance rather than needing the numbers read. */
.coverage-target { position: absolute; top: -2px; bottom: -2px; width: 2px; background: var(--accent); }
.coverage-value { font-family: var(--mono); text-align: right; color: var(--text); }

/* ---------- primer ---------- */
.primer { margin-bottom: var(--s-8); }
.primer-summary {
  display: flex; align-items: center; justify-content: space-between;
  cursor: pointer; list-style: none; gap: 12px;
}
.primer-summary::-webkit-details-marker { display: none; }
.primer-title { font-size: var(--fs-lg); font-weight: 650; letter-spacing: -0.01em; }
/* A chevron that turns, rather than a plus/minus — it reads as "there is more
   below" instead of "this is a control you must operate". */
.primer-toggle {
  width: 9px; height: 9px; flex: none;
  border-right: 2px solid var(--text-faint);
  border-bottom: 2px solid var(--text-faint);
  transform: rotate(45deg); transition: transform 160ms ease;
  margin-right: 4px;
}
.primer[open] .primer-toggle { transform: rotate(-135deg); }
.primer-summary:hover .primer-toggle { border-color: var(--text); }

/* Sits in the summary, so it reads as a description even when collapsed. */
.primer-sub {
  display: block; margin-top: 3px; font-size: var(--fs-sm);
  color: var(--text-faint); font-weight: 400;
}
.primer-grid {
  display: grid; grid-template-columns: repeat(2, 1fr);
  gap: var(--s-6) var(--s-9); margin-top: var(--s-6); padding-top: var(--s-6);
  border-top: 1px solid var(--border);
}
@media (max-width: 820px) { .primer-grid { grid-template-columns: 1fr; gap: 18px; } }
.primer-item h3 { margin: 0 0 7px; font-size: var(--fs-md); }
.primer-item p { margin: 0 0 8px; font-size: var(--fs-sm); color: var(--text-dim); line-height: 1.55; }
.primer-item p:last-child { margin-bottom: 0; }
.primer-item strong { color: var(--text); font-weight: 600; }
@media (prefers-reduced-motion: reduce) { .primer-toggle { transition: none; } }

/* ---------- candidate shares chart ---------- */
.hover-dot.rep { stroke: var(--rep-line); }

/* Individual polls behind the fitted line. They are deliberately faint: the
   point is to show the line is answerable to data and how much it smooths,
   not to invite reading any single dot. */
.poll-dot { pointer-events: all; }
@media (hover: hover) {
  .poll-dot:hover { opacity: 0.95 !important; }
}

/* .tt-row's own `span:first-child { color: text-dim }` outranks a bare
   .dem-text on specificity, so a candidate name in the left column would come
   out grey. Name the column explicitly to win it back. */
.chart-tip .tt-row span.dem-text { color: var(--dem); }
.chart-tip .tt-row span.rep-text { color: var(--rep); }

/* Says what the dots are, which nothing on the chart itself can. The two
   series are named on their own lines, so this is a caption, not a legend. */
.chart-note {
  margin: 2px 0 var(--s-4);
  font-size: var(--fs-sm);
  color: var(--text-faint);
  max-width: var(--measure);
}

/* ---------- who is on the ballot ---------- */
.matchup {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px;
  margin: 0 0 14px;
  font-size: var(--fs-lg);
}
.matchup .vs {
  color: var(--text-faint);
  font-size: var(--fs-sm);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.tt-matchup {
  display: flex;
  align-items: baseline;
  gap: 6px;
  margin: 6px 0 2px;
  font-weight: 600;
  font-size: var(--fs-md);
}
.tt-vs { color: var(--text-faint); font-weight: 400; font-size: var(--fs-xs); }

/* ---------- prediction markets carousel ---------- */
.markets-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}
.markets-head h2 { margin-bottom: 4px; }

.carousel-nav { display: flex; gap: 6px; flex: none; }
.carousel-btn {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--bg-elev-2);
  color: var(--text);
  font-size: var(--fs-lg);
  line-height: 1;
  cursor: pointer;
  transition: background 120ms ease, opacity 120ms ease;
}
.carousel-btn:hover:not(:disabled) { background: var(--border); }
.carousel-btn:disabled { opacity: 0.35; cursor: default; }

/* No `scroll-behavior: smooth` here. Combined with mandatory snapping it makes
   EVERY programmatic scroll silently do nothing in some engines -- measured:
   arrows, arrow keys and a direct scrollLeft assignment all left the track at
   0. Smoothness is requested per call in markets.js, which falls back to an
   instant jump when the engine refuses it. Touch scrolling is native and
   unaffected either way. */
.carousel {
  display: flex;
  gap: 14px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  padding: 4px 2px 10px;
  margin-top: 12px;
  scrollbar-width: thin;
  /* Stretch, so three cards of different content end up the same height.
     Ragged card bottoms were most of why the row looked untidy. */
  align-items: stretch;
}
.carousel:focus-visible { outline: 2px solid var(--dem); outline-offset: 3px; }

.market-card {
  /* Grow, never shrink, 300px basis: wide enough and they share the row
     exactly; too narrow and they hold 300px and it scrolls, which is when the
     arrows and dots appear. One rule, both behaviours, no breakpoint. */
  flex: 1 0 300px;
  max-width: 460px;
  scroll-snap-align: center;
  display: flex;
  flex-direction: column;
  background: var(--bg-elev-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 14px 16px 14px;
}
.market-card-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 6px;
}
.market-card-head h3 { font-size: var(--fs-md); margin: 0; line-height: 1.3; }
.market-volume {
  flex: none;
  font-size: var(--fs-2xs);
  color: var(--text-faint);
  font-family: var(--mono);
}

.market-chart { margin: 2px 0 8px; }

/* A compact key instead of a bar and caption per outcome. Eleven of those made
   the seat card four times the height of its neighbours and unreadable. */
.market-key {
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: var(--fs-sm);
  /* Push the caveat to the bottom so every card's footer lines up. */
  flex: 1 1 auto;
}
.market-key li {
  display: grid;
  grid-template-columns: 10px 1fr auto;
  align-items: center;
  gap: 8px;
  padding: 2px 0;
}
.market-key.has-model li { grid-template-columns: 10px 1fr auto auto; }
.market-key i {
  width: 9px;
  height: 9px;
  border-radius: 2px;
  display: block;
}
.market-key-label { color: var(--text-dim); }
.market-key-prob,
.market-mine {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  text-align: right;
  min-width: 3.4em;
}
.market-key-prob { font-weight: 650; }
.market-mine { color: var(--text-faint); }

/* Column headings, so "market" and "model" are named once rather than on
   every row. */
.market-key-head,
.market-key-head span {
  font-size: var(--fs-2xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-faint);
  font-family: inherit;
  font-weight: 500;
}
.market-key-head { border-bottom: 1px solid var(--border); padding-bottom: 3px; margin-bottom: 3px; }

.market-caveat {
  margin: 10px 0 0;
  font-size: var(--fs-xs);
  line-height: 1.45;
  color: var(--text-faint);
  border-top: 1px solid var(--border);
  padding-top: 8px;
}

.carousel-dots { display: flex; gap: 7px; justify-content: center; margin-top: 2px; }
.carousel-dot {
  width: 7px;
  height: 7px;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: var(--border);
  cursor: pointer;
  transition: background 120ms ease, transform 120ms ease;
}
.carousel-dot.is-active { background: var(--text-dim); transform: scale(1.3); }

@media (max-width: 640px) {
  /* Wide enough that the chart is not downscaled, narrow enough that a sliver
     of the next card still shows -- which is what tells a reader the row
     scrolls. At 86% and 16px padding the chart host came out 225px against
     chartWidth's 280 floor, so every label rendered at 80% size. */
  .market-card { flex-basis: 90%; max-width: none; padding: 13px 12px; }
  .carousel-nav { display: none; }   /* swipe is the obvious gesture on touch */
}
