/* ==========================================================================
   theoelections — paper and ink.

   One system across the whole site.

   1. Colour appears only where a datum is. The interface is monochrome; the
      data palette is legal inside figures and nowhere else.
   2. Marking without colour: reverse it, weight it, or rule it. A threshold or
      a mean is always an ink rule.
   3. Labels are bold lowercase. No text-transform, no small caps, no italics.
   4. Radius 0, elevation 0, transition none. Structure comes from rules.
   5. Every figure states its finding as its title and carries its source.
   6. Every novel form carries a one-line decode gate.
   7. Absent is absent, not zero.

   Two faces: Switzer for data (tabular by default), Author for prose.
   ========================================================================== */

/* Both faces are subset to Basic Latin, Latin-1, Latin Extended-A and the
   punctuation the system sets. font-display is swap, not optional: the faces
   carry the tabular figures every number on this site is set in, and a reader
   who never receives them reads a different site. The metric-matched
   fallbacks below make the swap cost close to nothing. */
@font-face{font-family:"Switzer";src:url("fonts/switzer-400.woff2") format("woff2");font-weight:400;font-display:swap;}
@font-face{font-family:"Switzer";src:url("fonts/switzer-500.woff2") format("woff2");font-weight:500;font-display:swap;}
@font-face{font-family:"Switzer";src:url("fonts/switzer-600.woff2") format("woff2");font-weight:600;font-display:swap;}
@font-face{font-family:"Switzer";src:url("fonts/switzer-700.woff2") format("woff2");font-weight:700;font-display:swap;}
@font-face{font-family:"Switzer";src:url("fonts/switzer-800.woff2") format("woff2");font-weight:800;font-display:swap;}
@font-face{font-family:"Switzer";src:url("fonts/switzer-900.woff2") format("woff2");font-weight:900;font-display:swap;}
@font-face{font-family:"Author";src:url("fonts/author-400.woff2") format("woff2");font-weight:400;font-display:swap;}
@font-face{font-family:"Author";src:url("fonts/author-500.woff2") format("woff2");font-weight:500;font-display:swap;}
@font-face{font-family:"Author";src:url("fonts/author-600.woff2") format("woff2");font-weight:600;font-display:swap;}

/* Metric-matched fallbacks. The ratios are measured, not guessed: the string
   xn0Hg sets 2.9440em in Switzer against 3.2486em in the resolved sans
   fallback, and 2.5110em in Author against 2.7222em in the serif. The
   overrides carry each face's own ascent, descent and line gap, so the line
   box does not move when the real face arrives. */
@font-face{
  font-family:"Switzer fallback";
  src:local("Arial"), local("Helvetica"), local("Liberation Sans");
  size-adjust:90.6%;
  ascent-override:98%;
  descent-override:25%;
  line-gap-override:9%;
}
@font-face{
  font-family:"Author fallback";
  src:local("Georgia"), local("Times New Roman"), local("Liberation Serif");
  size-adjust:92.2%;
  ascent-override:83%;
  descent-override:20%;
  line-gap-override:8%;
}

/* ======================================================================
   1 · TOKENS
   ====================================================================== */
:root{
  /* ---- the neutral ramp · the whole interface lives here ---------------
     Contrast against the paper ground, measured: ink 14.58:1, ink2 9.56:1,
     ink3 6.28:1, muted 4.99:1, faint 1.98:1.

     [5.3] Muted was #6E6C61 and 4.29:1, and it carries every label on the
     site at 11 to 13 pixels — the smallest type here. 4.29:1 fails AA for
     normal text, and "it is only a label" is not an argument when the label
     is what tells you what the number is. It moved to #646257: 4.99:1 on
     the ground and 4.58:1 inside a well, so it clears the floor on both of
     the grounds it is ever set on rather than only the lighter one.

     Faint never carries a value. It is for a rule, a swatch edge, a
     separator glyph — things that are read as edges, not as text — and
     scripts/check_colour.py exempts a text node that is punctuation only
     for exactly that reason. */
  --t-paper:#E9E8E0;     /* the ground */
  --t-paper-2:#E0DFD5;   /* the one quieter ground, for a well or a fill */
  --t-ink:#16171A;       /* type, rules, every mark that is not a datum */
  --t-ink2:#3A382F;      /* running prose */
  --t-ink3:#55534A;      /* a caption that must still be read */
  --t-muted:#646257;     /* a label · 4.99:1 on the ground, 4.58:1 in a well */
  --t-faint:#A9A69A;     /* never a value: a rule, an edge, a disabled word */
  --t-hair:#CBC8BC;      /* the hairline */
  --t-row-rule:#DEDCD3;  /* the record's row separation, quieter than a hairline */

  /* ---- the data palette ------------------------------------------------
     Legal inside a figure and nowhere else. The site draws two-way contests
     only, so it carries the two-way pair and the contested overprint. The
     full ramp is red, yellow, turquoise, purple, blue, green in that fixed
     order; the four it does not use are absent on purpose, so nobody reaches
     for an arbitrary one. A multi-series figure re-opens the palette
     deliberately rather than by accident. */
  --t-d1:#D62828;          /* series 1 · Republican          · figures only */
  --t-d2:#FFC300;          /* series 2 · the third position  · figures only */
  --t-d5:#1E6FD9;          /* series 5 · Democratic          · figures only */
  --t-overprint:#191122;   /* d1 over d5 · the contested unit · figures only */
  /* [11.9] The other three of the six. The site's own figures draw two-way
     contests and never reach for these; the standalone analyses draw four
     series each, which is the case §1 reserved them for — a multi-series
     figure re-opens the palette deliberately rather than by accident. */
  --t-d3:#00A6A6;          /* series 3 · turquoise           · figures only */
  --t-d4:#7B4B94;          /* series 4 · purple              · figures only */
  --t-d6:#1B7F4B;          /* series 6 · green               · figures only */
  --t-d1-soft:#F0C0C0;     /* the pale step of the R ramp    · figures only */
  --t-d5-soft:#C3D5F2;     /* the pale step of the D ramp    · figures only */
  --t-d2-soft:#F3E0A8;     /* the pale step of series 2      · figures only */

  /* ---- faces ---------------------------------------------------------- */
  --t-data:"Switzer", "Switzer fallback", ui-sans-serif, system-ui, sans-serif;
  --t-prose:"Author", "Author fallback", Georgia, serif;

  /* ---- rule weights --------------------------------------------------- */
  --t-w-hair:1px;      /* a separation inside a block */
  --t-w-rule:1.5px;    /* a threshold or a mean */
  --t-w-contour:2px;   /* the vessel around a figure or a control */
  --t-w-slab:9px;      /* the masthead's rule */

  /* ---- the type scale ------------------------------------------------- */
  --t-fs-1:.6875rem;   /* the record, the decode gate, the source */
  --t-fs-2:.75rem;     /* a label */
  --t-fs-3:.8125rem;   /* a dek, a control */
  --t-fs-4:.9375rem;   /* a sub-head */
  --t-fs-5:1.0625rem;  /* the wordmark, a figure's number */
  --t-fs-6:1.125rem;   /* a finding */
  --t-fs-7:clamp(1.9rem,4.8vw,3.3rem);   /* a page's claim */
  --t-fs-8:clamp(2.2rem,6vw,3.9rem);     /* a display figure */

  /* ---- the spacing scale ----------------------------------------------- */
  --t-sp-1:4px;
  --t-sp-2:8px;
  --t-sp-3:12px;
  --t-sp-4:16px;
  --t-sp-5:22px;
  --t-sp-6:32px;

  /* ---- the sheet ------------------------------------------------------- */
  /* [3.5] The sheet's maximum measure. Past this the columns stop growing:
     a 1420px sheet is three 460px columns, and a column wider than that is
     a line nobody's eye tracks back from. Above 1480 the sheet centres in
     the window instead. */
  --t-sheet:1480px;
  /* [3.6] One gutter. It is the column's inner padding and the offset every
     rule inside a column is measured from, so the two cannot drift. */
  --t-gutter:16px;
  /* [3.8] The baseline unit. Section heads, figure blocks and the record's
     row height are all multiples of it, so the sheet has a rhythm rather
     than a set of separately-chosen paddings. */
  --t-baseline:8px;

  /* ---- the figure vessel ------------------------------------------------ */
  /* [6.3] A tick's length and the gap between it and its label, once. They
     were 6 and 3 in one module, 4 and 2 in another, and d3's own defaults in
     a third. */
  --t-tick:5px;
  --t-tick-gap:4px;

  /* ---- the mark vocabulary ---------------------------------------------
     [6.12] Three marks, and each one means exactly one thing:

       a hollow ring   one observation — a single poll, drawn once
       a solid line    an average — a claim standing on many observations
       a solid dot     the datum under the cursor, and nothing else

     Every chart on the site draws from these six numbers, so a ring on
     the generic ballot and a ring on a state race are the same size and
     therefore the same claim. --t-mark-nest is what separates two series
     drawn at the same instant: their rings nest rather than overlap, so
     two coincident polls stay two polls instead of blending into a colour
     the palette does not contain. */
  --t-mark-r:2.2px;      /* a hollow ring's radius */
  --t-mark-nest:1.2px;   /* how far apart two series' rings sit */
  --t-mark-w:1.2px;      /* a hollow ring's contour */
  --t-mark-avg:2.5px;    /* the average line */
  --t-mark-cursor:4px;   /* the solid dot under the cursor */
  /* An average with a single poll behind it is drawn on this dash: it is a
     line the data cannot yet hold up, and the reader is told so by the
     mark rather than by a footnote. */
  --t-mark-dash:5 4;

  /* ---- control height -------------------------------------------------- */
  /* [2.12] Every control in the masthead is this tall, and nothing in the
     masthead sets a height any other way. scripts/check-tokens.mjs asserts
     it, because a masthead where the button and the toggle disagree by two
     pixels is the kind of thing nobody sees and everybody feels. */
  --t-control-h:32px;
}

/* Night is the same system inverted: ink ground, paper type, one palette.
   Only the neutrals and the pale steps move. The two-way inks do not: they
   are the palette, and a colour that changes meaning with the ground is not
   one colour. */
:root[data-theme="dark"]{
  --t-paper:#16171A;     /* the light ground's ink, now the ground */
  --t-paper-2:#1E2024;   /* one step up, so a well still reads as a well */
  --t-ink:#E9E8E0;       /* the light ground's paper, now the type */
  --t-ink2:#CFCEC4;      /* prose, one step down from the type */
  --t-ink3:#A8A69B;      /* a caption, still above the label level */
  --t-muted:#9A988C;     /* a label · 4.6:1 on the ink ground */
  --t-faint:#6E6C61;     /* never a value, as on paper */
  --t-hair:#3A3C40;      /* the hairline, lifted off the ground not dropped */
  --t-row-rule:#25272B; /* the record's row separation */
  --t-d1-soft:#4A1C1C;   /* the pale steps invert: pale means nearer the ground */
  --t-d5-soft:#16304F;
  --t-d2-soft:#4A3C10;
  --t-overprint:#C9BFD8; /* the contested unit reverses, so it still reads dark-on-light */
}

/* The ground follows the reader's system so form controls, scrollbars and the
   caret stop arriving from somewhere else. */
:root{color-scheme:light;}
:root[data-theme="dark"]{color-scheme:dark;}

/* More contrast asked for: hairlines become rules, quiet type steps up. */
@media (prefers-contrast: more){
  :root{
    --t-hair:#8E8B80;
    --t-muted:#4A4840;
    --t-faint:#6E6C61;
    --t-w-hair:1.5px;
  }
  :root[data-theme="dark"]{
    --t-hair:#6E6C61;
    --t-muted:#C6C4B8;
    --t-faint:#9A988C;
  }
}

/* Forced colours: keep the figures legible when the ground is not ours. */
@media (forced-colors: active){
  :root:root{
    --t-paper:Canvas; --t-paper-2:Canvas;
    --t-ink:CanvasText; --t-ink2:CanvasText; --t-ink3:CanvasText;
    --t-muted:GrayText; --t-faint:GrayText; --t-hair:GrayText;
  }
  svg path.state.selected, svg path.state.hovered{forced-color-adjust:none;}
}

/* ======================================================================
   2 · REBINDING THE APP'S TOKENS
   Doubled selector so the palette, look and theme variants cannot outrank it.
   ====================================================================== */
:root:root{
  --bg:var(--t-paper);
  --bg-2:var(--t-paper-2);
  --paper:var(--t-paper);
  --ink:var(--t-ink);
  --ink-dim:var(--t-ink2);
  --muted:var(--t-muted);
  --muted-2:var(--t-faint);
  --muted-light:var(--t-faint);
  --rule:var(--t-ink);
  --rule-soft:var(--t-hair);
  --line:var(--t-hair);
  --line-strong:var(--t-ink);
  --neutral-bg:var(--t-paper-2);
  --neutral-border:var(--t-hair);
  --card-bg:var(--t-paper);
  --card-border:var(--t-hair);
  --card-shadow:none;
  --card-shadow-hover:none;
  --bg-app:var(--t-paper);
  --bg-gradient-1:var(--t-paper);
  --bg-gradient-2:var(--t-paper);
  --text-ink:var(--t-ink);
  --text-muted:var(--t-muted);

  --blue:var(--t-d5);  --blue-deep:var(--t-d5);  --blue-dark:var(--t-d5);
  --blue-soft:var(--t-d5-soft); --blue-border:var(--t-d5);
  --red:var(--t-d1);   --red-deep:var(--t-d1);   --red-dark:var(--t-d1);
  --red-soft:var(--t-d1-soft);  --red-border:var(--t-d1);
  --yellow:var(--t-d2); --yellow-soft:var(--t-d2-soft);
  --approve:var(--t-ink); --approve-soft:var(--t-hair);

  --display:var(--t-data);
  --body:var(--t-data);
  --mono:var(--t-data);

  --radius-lg:0; --radius-md:0; --radius-sm:0; --radius-pill:0;
  --focus-ring:0 0 0 1px var(--t-ink);
}

/* ======================================================================
   3 · THE GROUND
   ====================================================================== */
/* The ground is the paper and nothing is painted on it. baseline.html washes
   the body with an indigo and a red radial gradient, swaps in a near-black
   of its own on the ink ground, and carries a `data-bg` texture layer in a
   body::before besides — all leftovers of the sheet this replaced, and all
   declared !important there, which is why they are answered in kind here.
   A ground with a colour in it is a ground competing with the data. */
html, body{color:var(--t-ink);}
/* Doubled :root so this outranks html[data-theme="dark"] body, which is
   where the wash and the near-black are declared. */
:root:root, :root:root body{
  background-color:var(--t-paper) !important;
  background-image:none !important;
}
body::before{content:none !important;}
body{
  font-family:var(--t-data);
  /* Tabular is the default because almost everything here is a figure in a
     column. Prose opts out, and only prose. */
  font-variant-numeric:tabular-nums;
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
}
::selection{background:var(--t-ink);color:var(--t-paper);}

/* ======================================================================
   4 · THE CANON SWEEP
   Radius 0, elevation 0, transition none, and none of the case tricks the
   system does without: no text-transform, no small caps, no synthesised
   italics in a face that has none.
   ====================================================================== */
*, *::before, *::after{
  border-radius:0 !important;
  box-shadow:none !important;
  text-shadow:none !important;
  transition:none !important;
  animation:none !important;
  backdrop-filter:none !important;
  -webkit-backdrop-filter:none !important;
  text-transform:none !important;
  font-variant-caps:normal !important;
  font-style:normal !important;
  letter-spacing:normal;
}
/* The one place a curve is sanctioned: nothing here yet, so nothing is round. */

/* Type steps. The system has two scales, the record and the display figure. */
.tiny, .mono{font-family:var(--t-data);}

/* ======================================================================
   5 · CHROME
   ====================================================================== */
/* The slab: the masthead's rule is a 9px bar of ink, so the two never stack
   and nothing can slide under it. */
/* [2.2] The masthead is one rule, not one per page. The app, the landing
   sheet and the standalone analyses all write .top > .brand + .nav +
   .actions and read this block. The landing sheet used to carry a second
   copy under its own class name, which is how two mastheads drift. The
   layout lives here rather than in the app's own sheet so a page that is
   not the app still gets it. */
.top{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:var(--t-sp-5);
  flex-wrap:wrap;
  border-bottom:var(--t-w-slab) solid var(--t-ink);
  padding-bottom:var(--t-sp-3);
  margin-bottom:var(--t-sp-5);
}
.brand{display:flex;}
.nav{display:flex;align-items:center;}
.actions{display:flex;align-items:center;}
.brand{font-family:var(--t-data);gap:var(--t-sp-2);}
.brand .name{
  font-family:var(--t-data);
  font-weight:700;
  font-size:var(--t-fs-5);
  letter-spacing:-.02em;
  line-height:1;
}
.brand .sep{color:var(--t-hair);font-size:1rem;}
.brand .tag{
  font-family:var(--t-data);
  font-size:var(--t-fs-2);font-weight:500;
  color:var(--t-muted);
}
/* [2.16] Nine labels only fit on one line if each costs what it is worth.
   The padding was 12px a side for a 14.5px label — more air than type. */
.nav a{
  font-family:var(--t-data);
  font-size:var(--t-fs-3);
  font-weight:500;
  color:var(--t-muted);
  padding:6px 9px;
  line-height:1.35;      /* [2.4] the nav's line box, shared with the brand */
  white-space:nowrap;
  /* Every label reserves the active rule's room, so marking one moves none. */
  border-bottom:var(--t-w-contour) solid transparent;
}
/* [2.7] Hover is a change of weight, not of colour — and the label reserves
   its own bold width in a zero-height copy of itself, so going bold moves
   nothing. Colour in this system means a datum; a nav label is not one. */
.nav a{position:relative;}
.nav a::after{
  content:attr(data-label);
  display:block;height:0;overflow:hidden;
  font-weight:700;visibility:hidden;pointer-events:none;
}
.nav a:hover{color:var(--t-ink);font-weight:700;}
/* [2.6] The active tab is marked by a rule, which reads across a room. The
   weight and the ink are what make the rule's label match it. */
.nav a.active{
  color:var(--t-ink);font-weight:700;
  border-bottom-color:var(--t-ink);
}
/* [2.8] The system's separator is the typographic middot, never a drawn
   pictogram — and the markup already carries the character, so this rule
   only has to stop styling it as a dot. It used to add a second middot in
   a ::before, which set every separator twice in every tab. */
.nav .dot{
  width:auto;height:auto;background:transparent;
  color:var(--t-hair);padding:0 1px;
}

.actions{gap:var(--t-sp-2);align-items:center;}
/* [2.9] The ground toggle names the ground it will switch to. It used to be
   a sun and a crescent — two pictograms in a system that has none, and two
   that a reader has to guess at besides. A word does not need a legend. */
.iconbtn{
  border:var(--t-w-contour) solid var(--t-ink);
  color:var(--t-ink);
  background:transparent;
  font-family:var(--t-data);
  font-weight:700;font-size:var(--t-fs-2);
  height:var(--t-control-h);padding:0 10px;line-height:1;
  display:inline-flex;align-items:center;justify-content:center;
}
/* [2.11] Donate is the masthead's one reversed element, at rest and on
   hover alike, so hover here is a weight of rule rather than a fill. */
.iconbtn:hover{background:var(--t-paper-2);}
.donate, .donateBtn{
  background:var(--t-ink);color:var(--t-paper);
  border:1px solid var(--t-ink);
  font-family:var(--t-data);font-weight:700;
  font-size:var(--t-fs-3);height:var(--t-control-h);padding:0 16px;line-height:1;
  /* It is a <button> in the app and an <a> on the landing sheet; it has to
     look the same either way. */
  display:inline-flex;align-items:center;justify-content:center;
  text-decoration:none;
}
.donate:hover, .donateBtn:hover{background:var(--t-ink2);}
:where(button, a, input, select, textarea):focus-visible{
  outline:1px solid var(--t-ink);
  outline-offset:2px;
}
/* [2.14] The skip link is the first thing in the tab order and invisible
   until it is the thing being tabbed to. It is drawn in the system: a
   reversed slab, no radius, no shadow, no animation. */
.skiplink{
  position:absolute;left:var(--t-sp-4);top:var(--t-sp-2);
  z-index:100;
  transform:translateY(-200%);
  background:var(--t-ink);color:var(--t-paper);
  border:var(--t-w-contour) solid var(--t-ink);
  font-family:var(--t-data);font-weight:700;font-size:var(--t-fs-2);
  padding:8px 12px;text-decoration:none;
}
.skiplink:focus{transform:none;}
#main:focus{outline:none;}
#main:focus-visible{outline:var(--t-w-contour) solid var(--t-ink);outline-offset:4px;}

.foot{
  font-family:var(--t-data);
  font-size:var(--t-fs-1);font-weight:500;
  color:var(--t-muted);
  border-top:var(--t-w-hair) solid var(--t-hair);
  padding-top:var(--t-sp-3);
}
.foot b{
  font-family:var(--t-data);font-weight:700;
  color:var(--t-ink);letter-spacing:0;
}
.foot .sep{color:var(--t-hair);}

/* ======================================================================
   6 · BOXES: grade 0 default, grade 1 rule, grade 2 keyline, grade 3 reversed
   A card is a claim about a boundary. One ink rule on the top edge, open on
   the other three. No tints to group, no four-sided borders for emphasis.
   ====================================================================== */
.card, .cardDash, .topCard, .seatsCard, .mapCard, .chartCard, .oddsCard,
.pollListCard, .flrCard, .methCard, .projCard, .projCardA, .swingCard,
.swingSliderCard, .ratingsTopCard, .ratingsSummaryCard, .stateInfoPanel{
  background:transparent;
  border:0;
  border-top:1px solid var(--t-ink);
  padding:15px 0 0;
}
.cardTitle, .panelTitle, .modeTitle, .secTitle, .methH2, .projCardTitle,
.projCardATitle, .swingCardLabel{
  font-family:var(--t-data);
  font-weight:800;
  letter-spacing:-.018em;
  color:var(--t-ink);
}
.panelSub, .modeSub, .secSub, .methDesc, .projCardDesc, .projCardADesc, .tipSub{
  font-family:var(--t-data);
  font-weight:500;
  color:var(--t-muted);
}
.methKicker, .projCardKicker, .projCardAKicker, .projSectionLabel,
.projSectionALabel, .mapCtrlLabel, .swingPartyLabel, .rtgCountLbl,
.miniLbl, .miniLblFinal, .seatPillLabel, .yearBarLabel, .sliderLabel{
  font-family:var(--t-data);
  font-size:var(--t-fs-2);font-weight:700;
  letter-spacing:0;
  color:var(--t-muted);
}
.methBody, .methP, .projTitle + p, .methLede{
  font-family:var(--t-prose);
  font-variant-numeric:proportional-nums oldstyle-nums;
  color:var(--t-ink2);
}
.methTitle, .projTitle, .methH1{
  font-family:var(--t-data);
  font-weight:900;
  letter-spacing:-.038em;
  color:var(--t-ink);
}

/* Chips, pills and segmented controls: keylines or underlines, never fills. */
.chip, .chipBtn, .zoomBtn, .mapCtrlBtn, .yearBtn, .yearBtnA, .fsBtn,
.chartTab, .seg, .segmentedControl button, .chamberBtn, .gbToggle button{
  font-family:var(--t-data);
  font-size:var(--t-fs-2);font-weight:700;
  color:var(--t-muted);
  background:transparent;
  border-color:var(--t-hair);
}
.chip.active, .chipBtn.active, .zoomBtn.active, .mapCtrlBtn.active,
.yearBtn.active, .yearBtnA.active, .fsBtn.active, .chartTab.active,
.seg.active, .chamberBtn.active{
  color:var(--t-paper);
  background:var(--t-ink);
  border-color:var(--t-ink);
}
.chamberToggle, .segmentedControl, .lookToggle, .themeToggle{
  background:transparent;
  border:1px solid var(--t-ink);
  padding:2px;
}
.metricPill, .seatPill, .panelSparkPill{
  background:transparent;
  border:1px solid var(--t-hair);
  font-family:var(--t-data);
  font-weight:700;
}
.metricPill .dot, .seatsDot, .methDot, .swatch, .methSwatch, .miniDot{
  border-radius:0 !important;
}
.zoomSelect, select, input[type="text"], input[type="number"]{
  font-family:var(--t-data);
  font-size:var(--t-fs-3);
  background:var(--t-paper);
  border:1px solid var(--t-ink);
  color:var(--t-ink);
}

/* ======================================================================
   7 · FIGURES
   Axes are hairlines. The only ink rule inside a figure is a threshold.
   ====================================================================== */
svg text, .axis text, .oddsAxis text, .probAxis text, .seatAxis text,
.tickLabel, .seatTickLabel{
  font-family:var(--t-data) !important;
  font-weight:600;
  fill:var(--t-muted);
}
/* [6.2] The domain path is a hairline everywhere, so no frame competes with
   a threshold; the ticks are the same hairline at the same weight. */
.oddsAxis path.domain, .probAxis path.domain, .axis path.domain,
.oddsAxis line, .probAxis line, .axis line,
.probGrid line, .zeroLine, .seatTickLine{
  stroke:var(--t-hair);
  stroke-width:var(--t-w-hair);
  stroke-dasharray:none;
  opacity:1;
}
/* [6.4] A gridline is quieter than the domain it hangs from: it is the
   record's row rule, solid. It used to be a dashed hairline at 50% alpha,
   and the system has neither a dash nor an alpha — an alpha on a rule
   manufactures a colour the palette does not contain. */
.gridline{
  stroke:var(--t-row-rule);
  stroke-width:var(--t-w-hair);
  stroke-dasharray:none;
  opacity:1;
}
/* [6.11] A cursor's rule marks where the reader is, not where a threshold
   is, so it is not an ink rule. Solid, one pixel, at the caption's step. */
.cursorRule, .annotRule{
  stroke:var(--t-ink3);
  stroke-width:var(--t-w-hair);
  stroke-dasharray:none;
}
/* [6.3][6.5] One tick length and one gap, read from the tokens by
   window.__axis in forecast.js, which every axis on the site is now built
   through. The label's colour and size are set here, once: --t-tick and
   --t-tick-gap are geometry and belong to the axis builder; the type
   belongs to the sheet. */
.oddsAxis .tick line, .probAxis .tick line, .axis .tick line{
  stroke:var(--t-hair);
  stroke-width:var(--t-w-hair);
}
.oddsAxis .tick text, .probAxis .tick text, .axis .tick text,
.tickLabel, .seatTickLabel{
  fill:var(--t-muted);
  font-family:var(--t-data);
  font-size:var(--t-fs-1);
  font-weight:600;
}
.seatMajLine, .seatMajorityLine, .miniZero, .tickZero{
  stroke:var(--t-ink);
  stroke-width:var(--t-w-rule);
  stroke-dasharray:none;
  opacity:1;
}
.seatMajLabel, .thrLabel{
  font-family:var(--t-data);
  font-size:10px;font-weight:700;
  fill:var(--t-ink);
}

/* [6.12][6.13][6.14][6.15] The mark vocabulary, in the sheet.

   window.__mark draws the polls charts; the hindcast and odds charts get
   their marks from here, and both read the same tokens. Before this
   an average was 2.5px on one sheet and 2px on another, and a cursor dot
   was 3.5px, 4px, or whatever the module felt like — which is how "solid
   dot" stopped meaning one thing. */
.markAvgLine, .markAvgProvisional,
.seatsLine, .seatsLineR, .oddsLine, .lineDem, .lineRep{
  fill:none;
  stroke-width:var(--t-mark-avg);
  stroke-linejoin:round;
  stroke-linecap:round;
  stroke-dasharray:none;
}
/* [6.18] An average with one observation behind it is drawn on the dash
   until a second one arrives. It is the only dashed thing in a figure. */
.markAvgProvisional{stroke-dasharray:var(--t-mark-dash);}
/* [6.12] One observation. Hollow, so a pile-up of them stays countable. */
.markRing{fill:none;stroke-width:var(--t-mark-w);}
/* [6.12] The datum under the cursor, at one radius on every chart. */
.markCursor, .dotDem, .dotRep, .seatsDot, .oddsDot{r:var(--t-mark-cursor);}
/* The legacy dots carried a paper ring to lift them off a card that no
   longer exists; on the sheet a hollow ring means one observation, so a
   ringed cursor dot is two marks arguing with each other. */
.seatsDot, .oddsDot{stroke:none;}
.map path, .mapSvg path.state, svg path.state{
  stroke:var(--t-hair);
  stroke-width:.75;
  vector-effect:non-scaling-stroke;
}
.mapSvg path.state.hovered, svg path.state.hovered, .map path:hover{
  stroke:var(--t-ink);
  stroke-width:1.75;
}
/* The selected unit is marked by rule, not by colour. */
svg path.state.selected{
  stroke:var(--t-ink);
  stroke-width:2.75;
}
.mapLabel{
  font-family:var(--t-data);
  font-size:9.5px;
  font-weight:700;
  fill:var(--t-ink);
  paint-order:stroke;
  pointer-events:none;
}
.mapLabel.selected{font-size:11px;}

/* Tooltips are keyline vessels, never floating cards. */
#tip, #simTip, #pastTip, #sldlCursorTip, .tooltipBox{
  font-family:var(--t-data) !important;
  background:var(--t-paper) !important;
  color:var(--t-ink) !important;
  border:var(--t-w-contour) solid var(--t-ink) !important;
  font-size:var(--t-fs-1);
  padding:7px 9px;
}
/* [7.23][9.22] Where there is no hover there is no cursor to follow, so the
   card docks: one place at the foot of the window, out of the way of the
   figure and out from under the finger that opened it. Full measure, because
   at 390 a card sized to its words wastes the room it has. */
#tip.tipDocked, #simTip.tipDocked, #pastTip.tipDocked,
#pastSimTip.tipDocked, #sldlCursorTip.tipDocked, .tooltipBox.tipDocked{
  position:fixed !important;
  left:var(--t-gutter) !important;
  right:var(--t-gutter) !important;
  top:auto !important;
  bottom:var(--t-gutter) !important;
  width:auto !important;
  max-width:none !important;
  min-width:0 !important;
  z-index:9999;
}
#simTip .stDate, .tipHeader, .tipTitle{font-weight:700;color:var(--t-ink);}
#simTip .stLbl, .tipMeta, .tipSub{font-weight:700;color:var(--t-muted);}
#simTip .stVal{font-weight:800;font-variant-numeric:tabular-nums;}

/* Absent is absent, not zero. */
.t-absent, .raceEmpty, .miniRowNoData{
  border:var(--t-w-contour) solid var(--t-ink);
  background:var(--t-paper-2);
  color:var(--t-muted);
  font-family:var(--t-data);
  font-size:var(--t-fs-2);font-weight:600;
  display:flex;align-items:center;justify-content:center;
  min-height:56px;
  padding:8px 10px;
}
.num.isAbsent, .isAbsent{
  font-size:var(--t-fs-3) !important;
  font-weight:700 !important;
  letter-spacing:0 !important;
  color:var(--t-faint) !important;
}

/* Tables: the record. Exact figures, ruled top and bottom, nothing tinted. */
table{font-family:var(--t-data);font-variant-numeric:tabular-nums;}
.bucketTable, .pollTable, .methTable{
  border-top:1px solid var(--t-ink);
  border-bottom:1px solid var(--t-ink);
}
.bucketTable th, .pollTable th, .methTable th{
  font-family:var(--t-data);
  font-size:var(--t-fs-1);font-weight:700;
  color:var(--t-muted);
  background:var(--t-paper);
  border-bottom:1px solid var(--t-ink);
}
.bucketTable td, .pollTable td, .methTable td{
  border-bottom:1px solid var(--t-row-rule);
  color:var(--t-ink);
}

/* ======================================================================
   8 · THE THREE-COLUMN SHEET
   ====================================================================== */
.colRule{background:var(--t-hair);}
.secHead{
  border-bottom:var(--t-w-hair) solid var(--t-hair);
  align-items:flex-start;
}
.secTitle{
  font-size:var(--t-fs-6);
  line-height:1.22;
  max-width:32ch;
}
.secSub{
  font-size:var(--t-fs-3);
  line-height:1.55;
  margin-top:5px;
  max-width:56ch;
}
.seatsCol .lbl, .seatsSide .lbl, .seatPillLabel, .miniLbl, .miniLblFinal,
.swingPartyLabel, .swingMarginLabel, .sliderLabel, .rtgCountLbl,
.methKicker, .projCardKicker, .projCardAKicker, .mapCtrlLabel,
.projSectionLabel, .projSectionALabel, .yearBarLabel, .brand .tag,
.pollsListHead, .secSub, .foot, .foot b, .tickLabel{
  letter-spacing:0 !important;
}
.seatsCol .lbl, .seatsSide .lbl{
  font-family:var(--t-data);
  font-size:var(--t-fs-2);font-weight:700;
  color:var(--t-muted);
}
.seatsCol .num, .seatsSide .num{
  font-family:var(--t-data);
  font-weight:900;
  letter-spacing:-.05em;
  font-variant-numeric:tabular-nums;
}
.seatsCol.d .num, .seatsSide.d .num{color:var(--t-d5);}
.seatsCol.r .num, .seatsSide.r .num{color:var(--t-d1);}
.seatsDash, .seatsDivider{display:none;}
.mapHint{
  font-family:var(--t-data);
  font-size:var(--t-fs-1);font-weight:500;
  color:var(--t-ink3);
  text-align:left;
}

/* The decode gate and the source line. Muted, never faint: a source is not
   decoration and has to survive a real screen. */
.t-how, .t-src, .secSub, .seatsNote{text-wrap:pretty;}
.t-how{
  font-family:var(--t-data);
  font-size:var(--t-fs-1);line-height:1.5;font-weight:500;
  color:var(--t-ink3);
  margin-top:var(--t-sp-3);
  max-width:64ch;
}
.t-how b{color:var(--t-ink);font-weight:700;}
.t-src{
  font-family:var(--t-data);
  font-size:var(--t-fs-1);font-weight:500;
  color:var(--t-ink3);
  margin-top:calc(var(--t-baseline) * 2);
  padding-top:var(--t-baseline);
  border-top:var(--t-w-hair) solid var(--t-hair);
}
.t-src a{color:var(--t-ink);text-decoration:underline;text-underline-offset:2px;}
.seatsNote{
  font-family:var(--t-data);
  font-size:var(--t-fs-1);font-weight:500;
  color:var(--t-ink3);
  margin-top:var(--t-sp-3);
}


/* ======================================================================
   9 · THE POLLS SHEET
   ====================================================================== */
/* ---- columns: hairline between, ink rule above each ---------------------- */
/* The sheet rests on the same scale everything else is spaced by. */
.pollsView{padding-bottom:calc(var(--t-sp-6) * 2);}
/* [3.5][3.7] The sheet's measure, owned here rather than in the app's own
   stylesheet, so the landing sheet and the analyses can read the same one.
   The outer columns sit flush with the masthead's outer edge: the gutter is
   inner padding only, never an outer margin. */
.sheet{max-width:var(--t-sheet);}

/* [3.1] The three columns align because the grid says so.

   They used to align because two min-height reservations forced them to:
   .secTitle reserved two lines and .secSub reserved three, so the display
   figures landed on one line whatever the copy did. That works until the
   copy is one line, and then it is whitespace nobody asked for above the
   head rule — and it never aligned the bottoms at all, so the three source
   lines finished up to 18px apart.

   The columns now share the parent's rows through subgrid. The five rows
   are the head, the note, the display figure, the body, and the source; the
   body is the only one that stretches, so the figures line up at the top of
   the sheet and the sources line up at the foot of it, with no reservation
   anywhere. */
.pollsView .cols{
  display:grid;
  grid-template-columns:1fr 1px 1fr 1px 1fr;
  grid-template-rows:auto auto auto 1fr auto;
  gap:0;
}
.pollsView .colRule{
  background:var(--t-hair);
  width:1px;
  grid-row:1 / -1;
}
.pollsView .col{
  border-top:1px solid var(--t-ink);
  padding:calc(var(--t-baseline) * 2) var(--t-gutter) 0;
  min-width:0;
  display:grid;
  grid-row:1 / -1;
  grid-template-rows:subgrid;
}
/* Outer columns align to the page edge; only the gutters get padding. */
.pollsView .col:first-child{padding-left:0;}
.pollsView .col:last-child{padding-right:0;}
@media (max-width:980px){
  /* Stacked, each column is its own block again: there is no row to share. */
  .pollsView .cols{grid-template-columns:1fr;grid-template-rows:none;}
  .pollsView .colRule{display:none;}
  .pollsView .col{
    display:block;
    grid-row:auto;
    grid-template-rows:none;
    padding:calc(var(--t-baseline) * 2) 0 0;
    /* [3.18] One token for the rhythm between stacked columns. */
    margin-top:calc(var(--t-baseline) * 4);
  }
}

/* ---- the section head: a finding, then its dek --------------------------- */
.pollsView .secHead{
  display:flex;
  justify-content:space-between;
  align-items:flex-start;
  gap:var(--t-sp-4);
  /* [3.8] Snapped to the baseline unit: 8 under the dek, 16 to the note. */
  padding:0 0 var(--t-baseline);
  margin-bottom:calc(var(--t-baseline) * 2);
  border-bottom:var(--t-w-hair) solid var(--t-hair);
}
.pollsView .secTitle{
  font-family:var(--t-data);
  font-weight:800;
  font-size:var(--t-fs-6);
  line-height:1.22;
  letter-spacing:-.018em;
  /* [10] A title measure that fits the column it lives in, not a fixed one. */
  max-width:none;
  color:var(--t-ink);
  font-style:normal;
  /* [3.15] "What gubernatorial polling says, state by state" broke as
     "…says, state / by state" at 981 and 1024 — a two-word widow under a
     three-word line. Fixed by measure, not by a hard break: the browser is
     told to balance the lines and finds a break the copy does not have to
     be rewritten around. */
  text-wrap:balance;
}
.pollsView .secSub{
  font-family:var(--t-data);
  font-weight:500;
  font-size:var(--t-fs-3);
  line-height:1.55;
  letter-spacing:0;
  color:var(--t-muted);
  margin-top:var(--t-sp-1);
  /* [3.14] The dek's measure. 56 characters is where a 13px line stops
     being one thought and starts being a paragraph. */
  max-width:56ch;
}

/* ---- the two-way readout ------------------------------------------------- */
.pollsView .pills{
  display:flex;
  flex-direction:column;
  gap:var(--t-sp-1);
  align-items:flex-end;
  font-family:var(--t-data);
}
.pollsView .pill{
  display:flex;
  align-items:baseline;
  gap:7px;
  font-variant-numeric:tabular-nums;
}
.pollsView .pill .sw{
  width:9px;height:9px;display:inline-block;
  border:1.5px solid var(--t-ink);
}
.pollsView .pill.d .sw{background:var(--t-d5);}
.pollsView .pill.r .sw{background:var(--t-d1);}
/* Approval is not a party contest, so it is marked by weight, not by colour. */
.pollsView .col.isApproval .pill.d .sw{background:var(--t-ink);}
.pollsView .col.isApproval .pill.r .sw{background:var(--t-muted);}
.pollsView .pill .l{
  font-size:var(--t-fs-2);font-weight:700;letter-spacing:0;
  color:var(--t-muted);width:auto;min-width:14px;
}
.pollsView .pill .n{
  font-size:var(--t-fs-5);font-weight:800;color:var(--t-ink);letter-spacing:-.015em;
}
.pollsView .pill .pct{
  font-size:var(--t-fs-1);font-weight:600;color:var(--t-muted);
  font-style:normal;margin-left:1px;
}

/* ---- the display figures ------------------------------------------------- */
.pollsView .seatsNote{
  font-family:var(--t-data);
  font-size:var(--t-fs-1);font-weight:500;
  color:var(--t-ink3);
  margin-top:0;
}
.pollsView .seatsNote + .seats{padding-top:var(--t-sp-1);}
.pollsView .seats{
  display:flex;
  align-items:flex-end;
  justify-content:space-between;
  gap:18px;
  padding:12px 0 8px;
  border-bottom:var(--t-w-hair) solid var(--t-hair);
}
.pollsView .seatsCol{
  text-align:left;
  min-width:0;
}
.pollsView .seatsCol .lbl{
  font-family:var(--t-data);
  font-size:var(--t-fs-2);font-weight:700;letter-spacing:0;
  font-variant:normal;
  color:var(--t-muted);
  margin-bottom:2px;
}
.pollsView .seatsCol .num{
  font-family:var(--t-data);
  font-weight:900;
  font-size:var(--t-fs-8);
  line-height:.86;
  letter-spacing:-.05em;
  font-variant-numeric:tabular-nums;
  color:var(--t-ink);
}
/* The colour marks the datum, so it sits on the swatch, not on the number. */
.pollsView .seatsCol.r{text-align:right;}
.pollsView .seatsCol.r .lbl{text-align:right;}
.pollsView .seatsDash{
  display:none;
}

/* ---- the margin timeline ------------------------------------------------- */
.pollsView .pollsHistHost{
  /* [6.20] Six baseline units. It was five, which held the bars but left
     the scale's outer labels nothing to sit in. */
  height:calc(var(--t-baseline) * 6);
  margin:12px 0 0;
  border:var(--t-w-contour) solid var(--t-ink);
  background:var(--t-paper);
  padding:0;
}

/* ---- figure blocks ------------------------------------------------------- */
/* [3.9] Every figure block opens the same way: two units of air, the
   hairline, then two units back to the figure. */
.pollsView .mapBlock,
.pollsView .probBlock,
.pollsView .pollsListWrap{
  margin-top:calc(var(--t-baseline) * 2);
  padding-top:calc(var(--t-baseline) * 2);
  border-top:var(--t-w-hair) solid var(--t-hair);
}
.pollsView .probHead{
  display:flex;
  justify-content:space-between;
  align-items:baseline;
  gap:var(--t-sp-3);
  margin-bottom:var(--t-baseline);
}
.pollsView .probHead .h{
  font-family:var(--t-data);
  font-style:normal;
  font-weight:800;
  font-size:var(--t-fs-4);
  line-height:1.25;
  letter-spacing:-.015em;
  color:var(--t-ink);
}
.pollsView .probHead .toggle{
  display:flex;align-items:baseline;gap:2px;flex:none;
}
/* Ghost buttons: an underline, no box. */
.pollsView .probHead .toggle button{
  appearance:none;border:none;background:transparent;
  font-family:var(--t-data);font-style:normal;
  font-size:var(--t-fs-2);font-weight:700;letter-spacing:0;
  color:var(--t-muted);cursor:pointer;padding:2px 6px;
}
.pollsView .probHead .toggle button.active{
  color:var(--t-ink);
  text-decoration:underline;
  text-underline-offset:3px;
}
.pollsView .probHead .toggle button:focus-visible{
  outline:1px solid var(--t-ink);outline-offset:2px;
}
.pollsView .probHead .toggle .s{color:var(--t-hair);}
.pollsView .pollsStateTitle{
  font-size:var(--t-fs-4) !important;
  letter-spacing:-.015em !important;
}

/* ---- the map: geography is a figure -------------------------------------- */
.pollsView .mapHost{
  width:100%;
  aspect-ratio:5/3;
}
.pollsView .mapHost svg path.state{
  stroke:var(--t-hair);
  stroke-width:.75;
  vector-effect:non-scaling-stroke;
}
.pollsView .mapHost svg path.state.active{cursor:pointer;}
.pollsView .mapHost svg path.state.hovered,
.pollsView .mapHost svg path.state:hover{
  stroke:var(--t-ink);
  stroke-width:1.75;
  paint-order:stroke;
}
/* The contour reverses on a dark unit, so hovering the overprint or a
   saturated fill marks it as plainly as hovering paper. The flip is set in
   polls.js, which knows each unit's fill. */
.pollsView .mapHost svg path.state.hovered.onDark,
svg path.state.hovered.onDark{stroke:var(--t-paper);}
.pollsView .mapHost svg path.state.selected.onDark,
svg path.state.selected.onDark{stroke:var(--t-paper);}
.pollsView .mapHost svg path.state.selected,
svg path.state.selected{
  stroke:var(--t-ink);
  stroke-width:3;
  paint-order:stroke;
}
.pollsView .mapHost svg path.state:focus-visible,
svg path.state:focus-visible{
  outline:none;
  stroke:var(--t-ink);
  stroke-width:3.5;
  stroke-dasharray:none;
}
/* [14][23] The hint starts on the column's left edge like everything else;
   the base stylesheet's 20px indent had it alone out of the measure. */
.pollsView .mapHint{
  font-family:var(--t-data);
  font-style:normal;
  font-size:var(--t-fs-1);font-weight:500;
  color:var(--t-ink3);
  text-align:left;
  margin-top:var(--t-sp-2);
  padding-left:0;
  text-indent:0;
}

/* ---- charts -------------------------------------------------------------- */
.pollsView .chartHost{height:210px;}
.pollsView .chartHost svg text,
.pollsView .chartHost svg .oddsAxis text{
  font-family:var(--t-data);
  font-size:10px;
  font-weight:600;
  fill:var(--t-muted);
}
.pollsView .chartHost svg .oddsAxis path.domain{
  stroke:var(--t-hair);
}
.pollsView .chartHost svg .oddsAxis line{
  stroke:var(--t-hair);
}
/* A threshold is always an ink rule. */
.pollsView .chartHost svg .seatMajLine{
  stroke:var(--t-ink);
  stroke-width:var(--t-w-rule);
  stroke-dasharray:none;
  opacity:1;
}

/* ---- the record ---------------------------------------------------------- */

.pollsView .pollsListHead{
  font-family:var(--t-data);
  font-size:var(--t-fs-2);font-weight:700;letter-spacing:0;
  color:var(--t-ink);
  margin-bottom:var(--t-baseline);
}
/* [4.20] The record's row is three baseline units, so the well is a whole
   number of rows rather than a round number of pixels — a half-row at the
   bottom edge reads as a rendering fault. On a phone there is no well at
   all: a scroll inside a scroll is a trap on a touch screen, and the record
   flows into the page instead. */
.pollsView .pollsListHost{
  max-height:calc(var(--t-baseline) * 3 * 11);
  overflow-y:auto;
}
@media (max-width:980px){
  .pollsView .pollsListHost{max-height:none;overflow-y:visible;}
}
.pollsView .pollsListHost table{
  width:100%;
  border-collapse:collapse;
  font-family:var(--t-data) !important;
  font-size:var(--t-fs-1);
  border-top:1px solid var(--t-ink);
  border-bottom:1px solid var(--t-ink);
}
.pollsView .pollsListHost th{
  font-family:var(--t-data) !important;
  font-size:var(--t-fs-1) !important;
  font-weight:700 !important;
  letter-spacing:0 !important;
  color:var(--t-muted) !important;
  background:var(--t-paper) !important;
  border-bottom:1px solid var(--t-ink) !important;
  padding:5px 10px 6px 0 !important;
  text-align:right;
}
.pollsView .pollsListHost th:first-child,
.pollsView .pollsListHost th:nth-child(2){text-align:left;}
.pollsView .pollsListHost td{
  font-family:var(--t-data) !important;
  font-size:var(--t-fs-1) !important;
  font-weight:500 !important;
  color:var(--t-ink) !important;
  padding:4px 10px 4px 0 !important;
  text-align:right;
  font-variant-numeric:tabular-nums;
  border-bottom:1px solid var(--t-row-rule) !important;
}
.pollsView .pollsListHost td:first-child,
.pollsView .pollsListHost td:nth-child(2){text-align:left;}
.pollsView .pollsListHost td:first-child{font-weight:600 !important;}
.pollsView .pollsListHost tr{background:transparent !important;}

/* ======================================================================
   10 · THE OTHER SHEETS
   ====================================================================== */

/* Ratings: the count chips are keylines, the number is the datum, the label
   is a label. No tint groups the row. */
.rtgCountItem{background:transparent;border:1px solid var(--t-hair);}
.rtgCountLbl{color:var(--t-muted) !important;font-weight:700;}
.rtgSeg{color:var(--t-paper);font-weight:700;}
.rtgLabels .rlbl{font-weight:700;}
.rtgTossupNote{color:var(--t-muted);}

/* Methodology: prose in the prose face, and no tinted column to group a table. */
.methBody, .methP, .methLede, .methDesc,
.heroLede, [class*="Lede"], .projCardDesc, .projCardADesc{
  font-family:var(--t-prose) !important;
  font-variant-numeric:proportional-nums oldstyle-nums;
  line-height:1.62;
}
.methTable, .methTableA{border-top:1px solid var(--t-ink);border-bottom:1px solid var(--t-ink);}
.methTable td, .methTableA td, .methTable th, .methTableA th{background:transparent !important;}
.methTable tr, .methTableA tr{background:transparent !important;}
.methMono{font-family:var(--t-data);color:var(--t-muted);}
.methHero{border-bottom:0;}
.methSwatch{border:1px solid var(--t-ink);}

/* Projects: the index is a record, not a deck of cards. */
.projCard, .projCardA{background:transparent;}
.projCardOpen, .projCardAOpen{
  font-family:var(--t-data);font-weight:700;font-size:var(--t-fs-2);color:var(--t-ink);
}
.projSectionLabel, .projSectionALabel{color:var(--t-muted);}

/* State legislatures: the map sits on paper like every other figure. */
.stateLegsHost, .stateLegsHost > div, .stateLegsHost svg{background:var(--t-paper) !important;}
.stateLegsHost .card, .stateLegsHost .cardInner{
  background:transparent;border:0;border-top:1px solid var(--t-ink);
}

/* Swingometer: a hairline track, an ink-keyline handle. */
.swingRange, .swingRangeA, input[type="range"]{background:transparent;}
.swingRange::-webkit-slider-runnable-track, input[type="range"]::-webkit-slider-runnable-track{
  height:1.5px;background:var(--t-hair);
}
.swingRange::-webkit-slider-thumb, input[type="range"]::-webkit-slider-thumb{
  -webkit-appearance:none;appearance:none;
  width:12px;height:12px;margin-top:-5px;
  background:var(--t-paper);border:var(--t-w-contour) solid var(--t-ink);
}
.swingMarginLabel, .swingPartyLabel{color:var(--t-muted);font-weight:700;}

/* Past elections: an empty year is absent, and says so plainly. */
/* [10.5] The placeholder column for a chamber with no race this cycle. It
   was set at opacity .35, which put every word in it — its heading
   included — under the contrast floor. Absent is absent: it says so in
   muted ink, at full opacity, like every other absent state on the site. */
.raceEmpty, .col-past-hidden{color:var(--t-muted);}
.col-past-hidden{opacity:1;}
.col-past-hidden .secTitle, .col-past-hidden .secSub{color:var(--t-muted);}

/* Florida: cards to grade 1 like everything else. */
.flrCard{background:transparent;}

/* A box that only groups is no box: the panels drop to grade 1. */
.stateLegsHost .cardInner, .stateLegsHost [class*="panel"],
.floridaHost .cardInner{
  border:0;
  border-top:var(--t-w-hair) solid var(--t-hair);
  background:transparent;
}
.methHero{border-bottom:0;padding-bottom:0;}

/* The rating chips: a square swatch carries the colour, the number and the
   label stay ink and muted so a pale tier is still readable. */
.rtgCountItem{
  position:relative;
  border:1px solid var(--t-hair);
  color:inherit;
}
.rtgCountItem::before{
  content:"";
  position:absolute;
  top:5px;left:5px;
  width:8px;height:8px;
  background:currentColor;
  border:1px solid var(--t-ink);
}
.rtg-safeD{color:var(--t-d5);}
.rtg-likelyD{color:color-mix(in srgb, var(--t-d5) 66%, var(--t-paper));}
.rtg-leanD{color:color-mix(in srgb, var(--t-d5) 34%, var(--t-paper));}
.rtg-tossup{color:var(--t-overprint);}
.rtg-leanR{color:color-mix(in srgb, var(--t-d1) 34%, var(--t-paper));}
.rtg-likelyR{color:color-mix(in srgb, var(--t-d1) 66%, var(--t-paper));}
.rtg-safeR{color:var(--t-d1);}
.rtgCountItem .rtgCountNum{
  color:var(--t-ink) !important;
  font-family:var(--t-data);font-weight:800;font-size:var(--t-fs-5);
}
.rtgCountItem .rtgCountLbl{color:var(--t-muted) !important;font-weight:700;font-size:var(--t-fs-1);}
/* [5.6] The bar's legend, on the same rule as the counts strip: the word is
   a label and the swatch is the tier. Set in the tier's own ink, "Lean D"
   read at 1.21:1 and "Lean R" at 1.31:1 — a legend nobody can read is not a
   legend. */
.rtgLabels, .rtgLabels .rlbl, .rtgLabels .rtgLbl{
  color:var(--t-muted) !important;font-weight:700;
}
.rtgLabels .rtgLbl{
  display:inline-flex;align-items:center;justify-content:center;gap:4px;
}
.rtgLabels .rtgLbl::before{
  content:"";
  width:8px;height:8px;flex:0 0 8px;
  border:1px solid var(--t-ink);
  background:currentColor;
}
.rtgLabels .rtgLbl.rtg-safeD::before{background:var(--t-d5);}
.rtgLabels .rtgLbl.rtg-likelyD::before{background:color-mix(in srgb, var(--t-d5) 66%, var(--t-paper));}
.rtgLabels .rtgLbl.rtg-leanD::before{background:color-mix(in srgb, var(--t-d5) 34%, var(--t-paper));}
.rtgLabels .rtgLbl.rtg-tossup::before{background:var(--t-overprint);}
.rtgLabels .rtgLbl.rtg-leanR::before{background:color-mix(in srgb, var(--t-d1) 34%, var(--t-paper));}
.rtgLabels .rtgLbl.rtg-likelyR::before{background:color-mix(in srgb, var(--t-d1) 66%, var(--t-paper));}
.rtgLabels .rtgLbl.rtg-safeR::before{background:var(--t-d1);}
/* The methodology's rating table names the same seven tiers; its dot is the
   swatch, so its name is a label. */
.methRatingName{color:var(--t-ink) !important;}
.rtgSeg{font-family:var(--t-data);font-weight:700;}

/* The majority is an ink rule through the simulation histogram. */
.histo{align-items:flex-end;position:relative;}
.histo .histoMaj{
  flex:0 0 1.5px;
  align-self:stretch;
  background:var(--t-ink);
  margin:0 2px;
}
.histo .b{background:var(--t-d5);}
.histo .b.r{background:var(--t-d1);}
.histo .b.faint{opacity:.45;}

/* Florida: cards drop to grade 1, the map sits on paper, labels take the
   label voice. */
#flRedistrictingPage .flrCard{
  background:transparent !important;
  border:0 !important;
  border-top:1px solid var(--t-ink) !important;
  padding:15px 0 0 !important;
}
#flRedistrictingPage .flrMapWrap, #flRedistrictingPage .mapStage,
.floridaHost svg, #flRedistrictingPage svg{background:var(--t-paper) !important;}
#flRedistrictingPage .sliderLabel, #flRedistrictingPage .swingCardLabel{
  font-family:var(--t-data);font-weight:700;font-size:var(--t-fs-2);color:var(--t-muted);
}
.panelClassify{
  font-family:var(--t-data);font-weight:700;font-size:var(--t-fs-1);
}

/* Nothing on this site sits on a well of a different colour. */
.mapStage, .mapHostWrap, .tableScroll, .simMini{background:var(--t-paper);}

/* Range handles are squares like everything else. */
input[type="range"]::-webkit-slider-thumb,
.swingRange::-webkit-slider-thumb,
.swingRangeA::-webkit-slider-thumb{
  border-radius:0 !important;
  background:var(--t-paper) !important;
  border:var(--t-w-contour) solid var(--t-ink) !important;
  width:12px;height:12px;
}
input[type="range"]::-moz-range-thumb{
  border-radius:0 !important;
  background:var(--t-paper);border:var(--t-w-contour) solid var(--t-ink);width:10px;height:10px;
}

/* ======================================================================
   11 · WHAT THE AUDIT FOUND
   ====================================================================== */

/* [18] Waiting is not the same claim as absent. A form still fetching is
   hatched; an empty one is a plain vessel with its word inside. */
.pollsHistHost:empty,
.pollsView .pollsHistHost:empty{
  background:repeating-linear-gradient(45deg,
    transparent 0 5px, var(--t-hair) 5px 6.5px);
}

/* [9][20] Touch: the map keeps a usable height and every control clears the
   44px target on a phone. */
@media (max-width:760px){
  .pollsView .mapHost, .mapHost{min-height:280px;}
  .pollsView .probHead .toggle button,
  .probHead .toggle button,
  .chartTab, .chip, .chipBtn, .yearBtn, .yearBtnA, .zoomBtn, .mapCtrlBtn{
    min-height:44px;
    padding:0 12px;
    display:inline-flex;
    align-items:center;
  }
  /* [2.12] The masthead's controls stay one height; on a phone that height
     is the touch target. Rebinding the token is how it moves — no control
     sets a height of its own. */
  :root{--t-control-h:44px;}
  /* [22] The nav scrolls on one line instead of stacking seven ragged rows
     ahead of the data. */
  .nav{
    flex-wrap:nowrap;
    overflow-x:auto;
    justify-content:flex-start;
    scrollbar-width:none;
    -webkit-overflow-scrolling:touch;
    min-width:0;
  }
  .nav::-webkit-scrollbar{display:none;}
  .nav a{white-space:nowrap;padding:10px 8px;}
  /* [2.16] On the phone the nav takes its own row, as it does at tablet.
     Squeezed between the wordmark and two controls it had 150px of the 330
     on offer, which cut the third label in half against the toggle's
     keyline and read as damage rather than as a scroll. On its own row it
     shows four labels and the fifth clipped at the sheet's edge, which is
     what a scroll looks like. The sheet itself never scrolls sideways. */
  .top{
    display:grid;
    grid-template-columns:1fr auto;
    grid-template-areas:"brand actions" "nav nav";
    align-items:center;
    column-gap:10px;
    row-gap:var(--t-sp-1);
    max-width:100%;
  }
  .brand{grid-area:brand;min-width:0;}
  .actions{grid-area:actions;justify-self:end;}
  .nav{grid-area:nav;}
  .sheet, .wrap, body{overflow-x:hidden;}
}

/* [21] The record's pollster column takes the width it has rather than a
   width hard-coded in a template string. */
.pollsListHost td:nth-child(2){max-width:14ch;}
@media (min-width:981px){.pollsListHost td:nth-child(2){max-width:22ch;}}

/* [15] The masthead's controls sit on one height. */
/* The height token is the box, keyline included: baseline.html resets
   box-sizing and the landing sheet does not, which made the same control two
   pixels taller on one page than the other. */
.iconbtn, .donate, .donateBtn{height:var(--t-control-h);box-sizing:border-box;}

/* ======================================================================
   13 · THE MASTHEAD  (chapter 2)
   ====================================================================== */

/* [2.1] The masthead's canonical content is four things in reading order:
   wordmark, tagline, nav, actions. Everything past this is removal.

   [2.2] And it is one rule, not one per page. The app, the landing sheet and
   the standalone analyses all write the same markup — .top > .brand + .nav +
   .actions — and read this block. The landing sheet used to carry a second
   copy under its own class name, which is how two mastheads drift. The
   layout lives here rather than in the app's own sheet so a page that is not
   the app still gets it. */
/* [2.16] The nav's nine labels want 750px of line. There are exactly two
   honest ways to set that: on the middle of a three-column line when the
   sheet is wide enough to hold brand + nav + actions, and on a line of its
   own when it is not. What it must never do is wrap to three ragged rows
   inside a flex row, which is what it did between 768 and 1100 — the widths
   where vertical space is scarcest. */
@media (min-width:1200px){
  .top{
    display:grid;
    grid-template-columns:1fr auto 1fr;
    align-items:baseline;      /* [2.4] one baseline for brand and nav */
    column-gap:var(--t-sp-4);
  }
  /* Placed by column rather than by source order, so a masthead without a
     nav — the landing sheet, an analysis — still puts its actions at the
     right-hand edge instead of in the middle. */
  .brand{grid-column:1;justify-self:start;min-width:0;}
  .nav{
    grid-column:2;
    justify-self:center;
    flex-wrap:nowrap;
    justify-content:center;
    min-width:0;
  }
  /* The controls are boxes, not text, so they centre rather than sit on the
     baseline the type uses. */
  .actions{grid-column:3;justify-self:end;align-self:center;}
}

/* [2.4] Between 761 and 1199 the nav is wider than the gap left between the
   brand and the actions, so it takes the second line whole and centres on
   the sheet. Two deliberate rows beat three accidental ones. */
@media (min-width:761px) and (max-width:1199px){
  .top{
    display:grid;
    grid-template-columns:1fr auto;
    grid-template-areas:"brand actions" "nav nav";
    align-items:center;
    column-gap:var(--t-sp-4);
    row-gap:var(--t-sp-2);
  }
  .brand{grid-area:brand;justify-self:start;min-width:0;}
  .actions{grid-area:actions;justify-self:end;}
  .nav{
    grid-area:nav;
    justify-self:center;
    max-width:100%;
    flex-wrap:wrap;
    justify-content:center;
    row-gap:var(--t-sp-1);
  }
}

/* [2.16] Between 761 and 899 the nav's own row is 47px short of holding all
   nine labels, which wraps two of them onto a lonely third line. The 47px
   is bought back from the label padding: the middots already separate the
   labels, so the air between them was doing nothing the type was not. */
@media (min-width:761px) and (max-width:899px){
  .nav a{padding:6px 5px;}
  .nav .dot{padding:0;}
}

/* [2.5] The tagline survives wherever the brand has a line it does not have
   to share — down to 761, where the two-row masthead gives it one. Below
   that it is competing with the actions for 330px, and it is the wordmark
   said twice, so it goes. It never wraps: a three-line tagline is what a
   wrapping tagline looks like, and it doubled the masthead's height at
   1024. */
.brand .tag{white-space:nowrap;}
@media (max-width:760px){
  .brand .sep, .brand .tag{display:none;}
}

/* [2.4] Brand and nav share a baseline: the wordmark's box is set to the
   nav's own line box so the two read as one line rather than two. */
.brand{align-items:baseline;}
.brand .name{line-height:1.35;}

/* [2.3] The slab is the masthead's only bottom rule. Nothing else may draw
   one under it. */
.top{border-top:0;}
.top + *{border-top:0;}

/* [2.15] The masthead does not stick. It is 53px and a 9px slab at desktop
   and 93px at tablet, and sticking it would park that slab over the figure
   a reader is reading for the whole of every scroll. The nav is nine links
   at the top of a page that is rarely more than two screens; the cost of
   scrolling back is smaller than the cost of never getting the space. */
.top{position:static;}

/* ======================================================================
   14 · PAPER  (chapter 3)
   The system is already paper and ink, so printing it is mostly a matter of
   taking the chrome off and letting the figures have the page. Nothing here
   restyles a figure: a printed figure that does not match the screen one is
   a second design to maintain.
   ====================================================================== */
@media print{
  /* [3.22] Paper is the ground on paper: the sheet's own ground is dropped
     so the stock shows through, and a reader who chose the ink ground still
     gets ink on white rather than a solid black page. The tokens are not
     otherwise restyled — the printed figure has to be the same figure — but
     the three quietest neutrals step down, because a hairline that survives
     a screen does not always survive a printer. */
  :root:root, :root:root body{
    background:none !important;
  }
  :root:root{
    --t-paper:#F6F5EF;
    --t-faint:#8E8C82;
    --t-hair:#A8A59A;
    --t-row-rule:#C4C1B7;
  }
  /* The chrome does not print: the nav is nine links nobody can follow, the
     controls do nothing, and the skip link is for a keyboard. */
  .top, .skiplink, .actions, .nav, .tweaks, .tweaksPanel,
  .mapCtrlBtn, .zoomBtn, .chartTab, .chipBtn, .toggle{display:none !important;}

  /* [3.22] The figures take the page. The three columns become one, because
     three 150mm columns on A4 is three columns of nothing. */
  .sheet{max-width:none;padding:0;}
  .pollsView .cols, .cols{
    display:block !important;
    grid-template-columns:none !important;
    grid-template-rows:none !important;
  }
  .pollsView .col, .col{
    display:block !important;
    grid-row:auto !important;
    grid-template-rows:none !important;
    padding:0 !important;
    margin:0 0 calc(var(--t-baseline) * 4);
  }
  .colRule{display:none !important;}

  /* A figure is not split across a page break, and its title goes with it. */
  .mapBlock, .probBlock, .pollsListWrap, figure, table{
    break-inside:avoid;
  }
  .secTitle, .secHead, .pollsListHead, .probHead{
    break-after:avoid;
  }
  /* The record is a record: all of it prints, not the 260px of it that fits
     a screen. */
  .pollsView .pollsListHost{max-height:none;overflow:visible;}

  /* A link's destination is worth nothing on paper unless it is written out,
     and the source line is the one place it matters. */
  .t-src a::after{content:" (" attr(href) ")";font-weight:500;}

  @page{margin:16mm;}
}

/* ======================================================================
   12 · THE DISTRICT SHAPES
   svg/house.svg and its siblings ship with a white stroke and the original
   author's reds and blues baked into inline style attributes. The fills are
   replaced at runtime; the stroke was not, which left a white grid on the
   paper ground and a bright one on the ink ground.
   ====================================================================== */
#district-shapes,
#district-shapes *,
svg .district,
svg path.district{
  stroke:var(--t-hair) !important;
  stroke-opacity:1 !important;
  stroke-width:.5px !important;
  vector-effect:non-scaling-stroke;
}
svg path.district.hovered,
svg path.district:hover{
  stroke:var(--t-ink) !important;
  stroke-width:1.5px !important;
}
svg path.district.selected{
  stroke:var(--t-ink) !important;
  stroke-width:2.5px !important;
}

/* The two alphas the system does use, named so they are not literals. */
.pollsListHost td,
.bucketTable td, .pollTable td, .methTable td{
  border-bottom-color:var(--t-row-rule);
}

/* ======================================================================
   15 · THE THREE VOICES  (chapter 4)

   [4.1] Every piece of type on this site is one of three things, and the
   audit found seventy-six distinct combinations of face, size, weight,
   tracking and leading pretending otherwise. Eleven of the sizes in use
   were not on the scale at all — 9, 9.5, 10, 12.5, 13.5, 14, 14.5, 16, 20,
   22 and 24 — each one a number somebody typed once.

   [4.24] The three voices, and what each is for:

     the label    Switzer 700, --t-fs-2, no tracking, muted or ink.
                  It names a thing. It is never Title Case, never tracked,
                  never a sentence.
     the finding  Switzer 800, --t-fs-6, -.018em, balanced, the column's
                  measure. It states what the figure found, in a sentence,
                  and it is the only voice allowed to be a claim.
     the prose    Author 400, --t-fs-4, 1.62, oldstyle figures, 64ch.
                  It is the only voice that runs to a paragraph, and the
                  only one set in the serif.

   The display figure is not a voice: it is a datum, set in Switzer 900 on
   the --t-fs-8 clamp, and it is the only type on the site allowed past
   --t-fs-6.
   ====================================================================== */

/* ---- the label ---------------------------------------------------------- */
/* [4.3][4.5] One rule. The tracking that ten of these carried is the
   letterspaced-small-caps habit the system does without: at 10px, 1.5px of
   tracking is 15% of an em, which is not emphasis, it is damage. */
.lbl, .seatsNote, .miniLbl, .miniLblFinal,
.rtgLbl, .rtgCountLbl, .rtgN,
.chartYLabel, .mapCtrlLabel, .pollsListHead,
.swingCardLabel, .swingPartyLabel, .swingMarginLabel, .sliderLabel,
.panelSub, .flrSub, .flrSliderSection, .flrSliderLbl, .flrSeatLabel,
.methKicker, .projCardKicker, .projCardAKicker,
.projSectionLabel, .projSectionALabel, .yearBarLabel,
.sldlPanelSub, .pill .l, .dSide, .rSide,
.pollsListHost th, .bucketTable th, .methTable th{
  font-family:var(--t-data);
  font-size:var(--t-fs-2);
  font-weight:700;
  letter-spacing:0;
  font-style:normal;
  text-transform:none;
}
/* The smallest labels — a swatch's letter, a chart's axis, a table head —
   take the record's size rather than inventing one below it. */
.chartYLabel, .rtgN, .rtgLbl, .flrSeatLabel, .flrSliderLbl,
.pollsListHost th, .bucketTable th, .methTable th,
.pill .l, .dSide, .rSide{font-size:var(--t-fs-1);}

/* ---- the finding -------------------------------------------------------- */
/* [4.9] One rule, one measure, and lines that balance rather than leave a
   two-word last one. */
.secTitle, .panelTitle, .oddsTitle, .probHead .h,
.flrTitle, .projCardTitle, .projCardATitle, .sldlPanelTitle,
.methH2, .rtgTitle, .histoTitle, .figTitle{
  font-family:var(--t-data);
  font-weight:800;
  font-size:var(--t-fs-6);
  line-height:1.22;
  letter-spacing:-.018em;
  font-style:normal;
  color:var(--t-ink);
  text-wrap:balance;
}
/* A finding inside a figure, rather than over one, takes a step down: it is
   still a claim, but it is subordinate to the section's. */
.probHead .h, .oddsTitle, .flrTitle, .sldlPanelTitle, .projCardATitle,
.histoTitle, .figTitle{
  font-size:var(--t-fs-4);
}
/* [4.11][4.14] The two figures that had no title at all now carry one, on
   the same rhythm every other figure block opens with. */
.histoTitle, .figTitle{
  margin:calc(var(--t-baseline) * 2) 0 var(--t-baseline);
}

/* ---- the prose ---------------------------------------------------------- */
/* [4.16][4.17] The only serif on the site, and the only voice that runs to a
   paragraph. Oldstyle figures, because a number inside a sentence is part of
   the sentence; every other number on this site is tabular. */
.methBody, .methLede, .methDesc, .methNote, .methP,
.projCardDesc, .projCardADesc, .heroLede{
  font-family:var(--t-prose);
  font-variant-numeric:proportional-nums oldstyle-nums;
  font-size:var(--t-fs-4);
  font-weight:400;
  line-height:1.62;
  max-width:64ch;
  text-wrap:pretty;
}
.methBody strong, .methLede strong, .methDesc strong,
.projCardDesc strong, .projCardADesc strong{font-weight:600;}
/* A rating's name is a label, not prose, even though it sits in a prose
   block. */
.methRatingName{
  font-family:var(--t-data);
  font-size:var(--t-fs-3);
  font-weight:700;
  letter-spacing:0;
}

/* ---- the display figure -------------------------------------------------- */
/* [4.18][4.19] One clamp, whose floor survives 320px: two numbers of four
   glyphs each at 2.2rem sit inside a 288px column with room to spare. And
   two trackings, because one does not serve both ends of a clamp — -.05em
   is right at 62px and closes the counters at 35px. */
.seats .num, .seatsSide .num, .flrSeatNum, .rtgNum, .rtgCountNum{
  font-family:var(--t-data);
  font-weight:900;
  font-variant-numeric:tabular-nums;
  line-height:.9;
}
.seats .num, .seatsSide .num{
  font-size:var(--t-fs-8);
  letter-spacing:-.05em;
}
@media (max-width:980px){
  .seats .num, .seatsSide .num{letter-spacing:-.035em;}
}
/* A figure's number inside a panel is a datum at reading size, not a
   display figure. */
.flrSeatNum{font-size:var(--t-fs-6);letter-spacing:-.03em;}
.rtgNum, .rtgCountNum{font-size:var(--t-fs-5);letter-spacing:-.03em;}

/* The Florida panel and the odds card style their type from an id selector,
   which outranks a class. Matched here rather than loosened there: the page
   is teleported into the app whole, and its own sheet is not ours to thin
   out until chapter 11. */
#flRedistrictingPage .flrTitle{font-size:var(--t-fs-4);letter-spacing:-.018em;}
#flRedistrictingPage .flrSub,
#flRedistrictingPage .flrSliderSection,
#flRedistrictingPage .flrSliderLbl,
#flRedistrictingPage .flrSeatLabel{
  font-size:var(--t-fs-1);font-weight:700;letter-spacing:0;
}
#flRedistrictingPage .flrSeatNum{font-size:var(--t-fs-6);letter-spacing:-.03em;}
.oddsCard .oddsTitle{font-size:var(--t-fs-4);letter-spacing:-.018em;}

/* The last of the off-scale labels: a ratings bar's segment names, a
   histogram's end labels, the tooltip's rows, the state-legs cursor, and the
   two separators the masthead and the footer set by hand. Nine and ten
   pixels are not sizes, they are what is left when a label is treated as
   packaging. */
.ratingLabels, .ratingLabels .rlbl,
.pollsHistLabels, .pollsHistLabels span,
#tip .miniLbl, #tip .miniLblFinal, #tip .miniVal,
#stateLegsPage .sldlCursorTip .cBase,
.rtgCountLbl, .projCardAKicker, .projCardKicker, .methKicker{
  font-size:var(--t-fs-1);
  font-weight:700;
  letter-spacing:0;
  text-transform:none;
}
.brand .sep, .foot .sep, .zoomRow .sep, .gbMeta .sep{
  font-size:var(--t-fs-3);
  opacity:1;
}
.swingMarginVal{
  font-family:var(--t-data);
  font-size:var(--t-fs-4);
  font-weight:800;
  font-variant-numeric:tabular-nums;
}

/* The last six. A project card's mini variant, the state-legs panel title,
   the hover panel's margin figure and its labels, and the tooltip's rows —
   each of them a size chosen inside the block that needed it rather than
   from the scale. */
.projCardA-mini .projCardATitle{font-size:var(--t-fs-3);}
.sldlPanelTitle{font-size:var(--t-fs-5);}
.panelMarginNum{
  font-family:var(--t-data);
  font-size:var(--t-fs-6);
  font-weight:900;
  letter-spacing:-.03em;
  font-variant-numeric:tabular-nums;
}
.panelUsps, .simTip .stDate, .miniLbl, .miniVal{
  font-size:var(--t-fs-1) !important;
  font-weight:700;
  letter-spacing:0;
}

/* Named exactly, because each of these is inside a block that styles by
   descendant and would otherwise keep its own number: the state-legs panel's
   title, its mode toggle, and its two seat figures; the ratings bar's segment
   counts; a project card's kind; and the methodology table's head. */
/* state-legs.js injects its own sheet at runtime, scoped by the page's id
   and appended after this one, so it wins on order at equal specificity.
   Its panel now reads the tokens directly rather than being answered here. */
.ratingBar .seg span{font-size:var(--t-fs-1);font-weight:700;}
.projCardAMeta span{font-size:var(--t-fs-1);font-weight:700;letter-spacing:0;}
.methTableA th{font-size:var(--t-fs-1);font-weight:700;letter-spacing:0;}

/* ======================================================================
   16 · THE COLOUR LAW  (chapter 5)
   scripts/check_colour.py crawls the running site and asserts these. What
   follows are the cases it found, and what was decided about each.
   ====================================================================== */

/* [5.6] A pill's number is a datum, but its party is already carried by the
   swatch beside it. Set in the data ink it read at 3.94:1, which is under
   the floor for 18px text; set in ink it reads at 14.58:1 and the swatch
   still says which side it is. One mark, one job. */
.pill .n, .pill .pct,
.pill.d .n, .pill.r .n{color:var(--t-ink);}

/* [5.6] The ratings counts row, on the same rule: the number is ink, the
   tier is the swatch. The totals under it are labelled in words — D, tossup,
   R — so they do not need to be labelled in colour as well. The tossup amber
   they used was not in the palette to begin with. */
.rtgNum{color:var(--t-ink) !important;display:inline-flex;align-items:center;gap:4px;}
.rtgNum .rtgSw{
  width:8px;height:8px;display:inline-block;
  border:1px solid var(--t-ink);
}
.ratingTotals .rtD,
.ratingTotals .rtT,
.ratingTotals .rtR{color:var(--t-ink) !important;}

/* [5.6][5.20] The Florida sliders' D and R labels were set in the party
   inks. A slider is a control, and a control's label is a label: colour
   there is colour outside a figure. The swatchless pair is distinguished by
   the letter, which is what the letter is for. */
#flRedistrictingPage .flrSliderLbl,
#flRedistrictingPage .flrSliderLbl.d,
#flRedistrictingPage .flrSliderLbl.r{color:var(--t-muted);}

/* [5.9] The approval series is not a party contest, so it is not drawn in
   the party inks: approval is ink, disapproval is ink at the muted step.
   Confirmed rather than replaced — a reader who sees red and blue on an
   approval chart reads a partisan split that is not in the data. */
.col.isApproval .pill.d .sw{background:var(--t-ink);}
.col.isApproval .pill.r .sw{background:var(--t-muted);}

/* [5.10] Third party and undecided. The site draws two-way contests only,
   so a third position has no ink of its own in use; --t-d2 is reserved for
   it and nothing else. An undecided share is not drawn at all, because the
   forecast no longer allocates it — absent is absent, not zero. If a
   three-way race is ever added, it opens --t-d2 deliberately rather than by
   reaching for an arbitrary colour. */

/* [5.17][5.23] The ramp's direction does not survive greyscale: a D lead
   and an R lead of the same size are the same value, within ΔE 0.89 at the
   extremes. Value alone cannot carry direction on a divergent ramp, so the
   maps carry a second cue that is not colour — the state's own label, drawn
   in the map, and the ink rule at the majority. In print the fills are kept
   rather than flattened, because a greyscale ramp that reads by value alone
   would say "big margin" without saying whose. */

/* [5.6][5.7] A party ink is 3.94:1 (D) and 4.07:1 (R) on the paper ground
   and 3.70:1 / 3.58:1 on the ink ground. That clears the 3:1 floor for
   large text, which is why the sheet's display figures may keep it, and it
   does not clear 4.5:1 for anything smaller. Every one of these already
   carries the letter D or R beside the number, so the colour was saying the
   same thing a second time at the cost of the number's legibility. The
   letter keeps the party; the number goes to ink. */
#flRedistrictingPage .flrSeatLabel.d,
#flRedistrictingPage .flrSeatLabel.r,
#flRedistrictingPage .flrSeatNum.d,
#flRedistrictingPage .flrSeatNum.r,
.swingPartyLabel.d, .swingPartyLabel.r,
.swingMarginVal.swing-d, .swingMarginVal.swing-r, .swingMarginVal.swing-tied,
.swingMarginVal.swing-tied{color:var(--t-ink);}
/* state-legs.js's own sheet is answered at the source; see that file. */

/* [5.17][5.23] The second cue, for the two places colour is not available.

   The ramp survives all three dichromacies — a maximal D and a maximal R
   separate by ΔE 148 under deuteranopia, 135 under protanopia and 114 under
   tritanopia — so the maps need nothing extra for a colour-blind reader.
   What they do not survive is the loss of hue altogether: in greyscale a D
   lead and an R lead of the same size are the same value, within ΔE 0.89 at
   the extremes. Measured by scripts/check_ramp.py, drawn in
   docs/shots/ramp.svg.

   So where hue is gone, direction is carried by the contour instead:
   a Republican lean is outlined, a Democratic lean is not, and a contested
   race keeps the overprint's near-black. Two states of a contour read at a
   glance in a way two greys never will. */
@media print{
  path.state[data-lean="r"], path.district[data-lean="r"]{
    stroke:var(--t-ink);
    stroke-width:1.4;
    stroke-dasharray:3 2;
    vector-effect:non-scaling-stroke;
  }
  path.state[data-lean="d"], path.district[data-lean="d"]{
    stroke:var(--t-ink);
    stroke-width:.5;
    vector-effect:non-scaling-stroke;
  }
}
@media (forced-colors:active){
  path.state[data-lean="r"], path.district[data-lean="r"]{
    stroke-width:1.4;
    stroke-dasharray:3 2;
    vector-effect:non-scaling-stroke;
  }
}

/* ======================================================================
   17 · THE SMALL-STATE INSET  (chapter 7)
   [7.7][7.22] Measured at 390: sixteen of the thirty-five states with a
   race are under 24px on their longest side, and Rhode Island is four
   pixels by six. No projection fixes that — the states are that shape — so
   the ones the map cannot hold sit under it as chips, each with the same
   fill, its own name, and a target a thumb can hit.
   ====================================================================== */
.mapInset{
  display:flex;
  flex-wrap:wrap;
  gap:var(--t-sp-1);
  margin-top:var(--t-baseline);
}
.mapChip{
  appearance:none;
  display:inline-flex;
  align-items:center;
  justify-content:center;
  gap:var(--t-sp-1);
  border:var(--t-w-hair) solid var(--t-ink);
  background:var(--t-paper);
  color:var(--t-ink);
  font-family:var(--t-data);
  font-size:var(--t-fs-1);
  font-weight:700;
  letter-spacing:0;
  min-width:44px;
  height:26px;
  padding:0 var(--t-sp-2);
  cursor:pointer;
  box-sizing:border-box;
}
/* [5.6] The tier goes on the swatch; the name stays ink. Two letters on a
   saturated data ink read at 3.9:1 at 11px, and at 1.0:1 when the fill is
   the paper an unpolled state keeps. */
.mapChipSw{
  width:9px;height:9px;flex:0 0 9px;
  display:inline-block;
  border:var(--t-w-hair) solid var(--t-ink);
  background:var(--t-paper);
}
.mapChip:hover{outline:var(--t-w-contour) solid var(--t-ink);outline-offset:-1px;}
@media (max-width:760px){
  /* The touch target the map itself cannot give these states. */
  .mapChip{min-width:52px;height:44px;font-size:var(--t-fs-2);}
}

/* ======================================================================
   18 · THE RECORD  (chapter 8)

   Density lives in text, so the record should be the easiest thing on the
   site to read. It was written inline, in a template string, with its type,
   its padding, its alignment and its colours all set per cell — five
   different places to change one decision.

   [8.1] One type: the record's own step, on the baseline's rhythm.
   [8.4] One column rule: a label is left, a figure is right, always.
   [8.9] Tabular figures on every numeric column, so the decimals line up.
         They were `normal`, which is why they did not.
   ====================================================================== */
.pollsListHost table{
  width:100%;
  border-collapse:collapse;
  font-family:var(--t-data);
  font-size:var(--t-fs-1);
  line-height:calc(var(--t-baseline) * 2);
  border-top:var(--t-w-hair) solid var(--t-ink);
  border-bottom:var(--t-w-hair) solid var(--t-ink);
}
/* [8.20] The caption states what the table is a record of. It is read, not
   seen: the finding above the figure already carries the claim. */
.pollsListHost caption,
.methTableA caption, .methTable caption{
  position:absolute;
  width:1px;height:1px;
  margin:-1px;padding:0;border:0;
  clip-path:inset(50%);
  overflow:hidden;
  white-space:nowrap;
}
.pollsListHost th{
  font-family:var(--t-data);
  font-size:var(--t-fs-1);
  font-weight:700;
  letter-spacing:0;
  color:var(--t-muted);
  padding:var(--t-sp-1) var(--t-sp-2) var(--t-sp-1) 0;
  border-bottom:var(--t-w-hair) solid var(--t-ink);
  text-align:right;
}
.pollsListHost th:first-child,
.pollsListHost th:nth-child(2){text-align:left;}
.pollsListHost td{
  font-family:var(--t-data);
  font-size:var(--t-fs-1);
  font-weight:500;
  color:var(--t-ink);
  padding:var(--t-sp-1) var(--t-sp-2) var(--t-sp-1) 0;
  border-bottom:var(--t-w-hair) solid var(--t-row-rule);
  text-align:right;
  font-variant-numeric:tabular-nums;
}
.pollsListHost td.recDate{text-align:left;font-weight:600;white-space:nowrap;}
.pollsListHost td.recName{
  text-align:left;
  color:var(--t-ink2);
  /* [8.10] Truncation by measure. The width is a measure, not a count of
     characters chosen inside a template string. */
  max-width:14ch;
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
}
@media (min-width:981px){.pollsListHost td.recName{max-width:22ch;}}
.pollsListHost td.recMargin{font-weight:700;}

/* [8.13] The well has a visible edge, so a reader knows the record goes on
   past it. An ink rule at the foot of the scroll, not a fade: a fade is a
   gradient, and the system has none. */
.pollsView .pollsListWrap{position:relative;}
.pollsView .pollsListHost{
  border-bottom:var(--t-w-contour) solid var(--t-ink);
}
@media (max-width:980px){
  .pollsView .pollsListHost{border-bottom:0;}
}

/* [8.4][8.7] The methodology's table, on the same column rule. */
.methTableA th, .methTable th{
  font-family:var(--t-data);
  font-size:var(--t-fs-1);
  font-weight:700;
  letter-spacing:0;
  color:var(--t-muted);
  text-align:left;
  padding:var(--t-sp-1) var(--t-sp-3) var(--t-sp-1) 0;
  border-bottom:var(--t-w-hair) solid var(--t-ink);
}
.methTableA td, .methTable td{
  padding:var(--t-sp-1) var(--t-sp-3) var(--t-sp-1) 0;
  border-bottom:var(--t-w-hair) solid var(--t-row-rule);
  font-variant-numeric:tabular-nums;
}
/* The margin column is a figure. */
.methTableA td:nth-child(2), .methTableA th:nth-child(2){text-align:right;}

/* [8.23][8.24] At 320 five columns do not fit, and a table that scrolls
   sideways inside a page that does not is a trap. The record drops the two
   columns a reader can reconstruct — each side's share, which the margin
   already states — and keeps the date, the pollster and the margin. */
@media (max-width:400px){
  .pollsListHost th:nth-child(3), .pollsListHost td:nth-child(3),
  .pollsListHost th:nth-child(4), .pollsListHost td:nth-child(4){display:none;}
  .pollsListHost td.recName{max-width:18ch;}
}

/* ---- the agate form  [8.16][8.17][8.18] ---------------------------------

   A dense list of names and numbers, set small and tight, in as many
   columns as the measure will hold.

   The record above is a table because a poll has five things to say about
   itself and they line up in columns. A list of races has two, and a
   two-column table thirty-five rows deep is a table nobody reads to the
   bottom of: every row is the same shape, so the eye has nothing to catch
   on, and the column it sits in grows past the figure it belongs to.

   Agate is the newspaper's answer to exactly this — the standings, the box
   scores, the returns on election night — and it works here for the same
   reason. Three things per line, one line per unit, two columns of them,
   and the whole list is a block you scan rather than a page you read.

   The swatch carries the colour, never the name: a state's name on a
   saturated data ink is the lesson from chapter 5 and from the map chips. */
.agate{
  list-style:none;
  margin:0;
  padding:0;
  columns:2;
  column-gap:var(--t-sp-4);
  column-rule:var(--t-w-hair) solid var(--t-hair);
  font-family:var(--t-data);
  font-size:var(--t-fs-1);
}
/* One column below the point where two of them stop being scannable and
   start being two lists. */
@media (max-width:600px){
  .agate{columns:1;column-rule:none;}
}
.agateRow{
  display:flex;
  align-items:baseline;
  gap:var(--t-sp-1);
  line-height:calc(var(--t-baseline) * 2);
  break-inside:avoid;
}
.agateSw{
  flex:0 0 auto;
  width:var(--t-sp-2);
  height:var(--t-sp-2);
  align-self:center;
  border:var(--t-w-hair) solid var(--t-ink);
  background:var(--t-paper);
}
/* [8.10][8.11] Truncated by measure, never by a character count, and it
   keeps its full name for anyone who wants it. */
.agateName{
  flex:1 1 auto;
  min-width:0;
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
  color:var(--t-ink);
  font-weight:600;
}
.agateVal{
  flex:0 0 auto;
  color:var(--t-ink);
  font-weight:700;
  font-variant-numeric:tabular-nums;
}
/* The block opens like every other figure block on the sheet. */
.agateBlock{
  margin-top:calc(var(--t-baseline) * 2);
  padding-top:calc(var(--t-baseline) * 2);
  border-top:var(--t-w-hair) solid var(--t-hair);
}
.agateHead{
  font-family:var(--t-data);
  font-size:var(--t-fs-2);
  font-weight:700;
  color:var(--t-ink);
  margin-bottom:var(--t-sp-2);
}

/* ======================================================================
   19 · THE STATES OF A THING  (chapter 9)

   [9.1] Eight states, and what each one asserts:

     absent     there is no datum, and there never will be for this unit
     loading    there is a datum and it has not arrived yet
     empty      the query ran and returned nothing
     hovered    the pointer is over this
     focused    the keyboard is on this
     selected   the reader chose this, and it is still chosen
     disabled   this control exists but has nothing to act on
     error      the fetch failed, which is not the same as absent

   The three that are easy to confuse are absent, loading and empty, and
   they are the three the site used to draw the same way — as a 50/50 tie,
   which is a plausible value and therefore a lie.
   ====================================================================== */

/* [9.13][9.16] The focus mark. An SVG path is not covered by the
   :focus-visible rule the controls take — it is not a button, an anchor or
   an input — so a keyboard reader tabbing the map got the hover mark and
   nothing more, on every map. Focus is a heavier rule than hover, which is
   how this system marks one thing above another, and a stroke does not
   shift a single pixel of layout. */
.pollsView .mapHost svg path.state.focused,
.mapSvg path.state.focused,
svg path.state.focused,
svg path.district.focused,
svg path.state:focus-visible,
svg path.district:focus-visible{
  outline:none;
  stroke:var(--t-ink);
  stroke-width:3.5;
  paint-order:stroke;
}
.pollsView .mapHost svg path.state.focused.onDark,
svg path.state.focused.onDark,
svg path.district.focused.onDark,
svg path.state.onDark:focus-visible,
svg path.district.onDark:focus-visible{stroke:var(--t-paper);}
/* The UA's own ring would sit on the SVG's bounding box, not the shape. */
svg path:focus{outline:none;}

/* [9.19] Disabled is faint at full opacity. Nothing in this system fades:
   an opacity is a colour the palette does not contain, and a half-drawn
   control reads as a rendering fault rather than as a decision. */
:where(button, a, input, select, textarea)[disabled],
:where(button, a, input, select, textarea)[aria-disabled="true"],
.is-disabled{
  opacity:1;
  color:var(--t-faint);
  border-color:var(--t-faint);
  background:transparent;
  cursor:default;
  pointer-events:none;
}
.donate[disabled], .donateBtn[disabled]{
  background:var(--t-faint);
  color:var(--t-paper);
  border-color:var(--t-faint);
}

/* [9.2][9.3] The absent vessel: a contour with its word inside, and nothing
   else. It is not a chart with no bars — it is a statement that there is no
   chart to draw. */
.t-absent, .absentVessel{
  display:flex;
  align-items:center;
  justify-content:center;
  min-height:calc(var(--t-baseline) * 9);
  border:var(--t-w-contour) solid var(--t-ink);
  background:transparent;
  font-family:var(--t-data);
  font-size:var(--t-fs-2);
  font-weight:700;
  letter-spacing:0;
  color:var(--t-ink);
  text-align:center;
}

/* [9.4][9.5] Loading is hatched, which is a texture no datum ever has, so
   it cannot be read as one. Absent is a plain vessel; loading is a hatched
   one; the two are never the same picture. */
.t-loading, .isLoading{
  min-height:calc(var(--t-baseline) * 9);
  border:var(--t-w-contour) solid var(--t-ink);
  background:repeating-linear-gradient(45deg,
    transparent 0 5px, var(--t-hair) 5px 6.5px);
}

/* [9.8] An error is not an absence. Absent says "there is nothing here";
   error says "there is something here and we could not get it", which is a
   claim about us rather than about the race. It is the absent vessel with
   the ink reversed, so it is unmistakable at a glance and still no louder
   than a figure. */
.t-error{
  display:flex;
  align-items:center;
  justify-content:center;
  min-height:calc(var(--t-baseline) * 9);
  border:var(--t-w-contour) solid var(--t-ink);
  background:var(--t-ink);
  color:var(--t-paper);
  font-family:var(--t-data);
  font-size:var(--t-fs-2);
  font-weight:700;
  letter-spacing:0;
  text-align:center;
  padding:0 var(--t-sp-4);
}

/* [9.11] An empty result is not an absent one: the record ran and came back
   with nothing, which is a fact about today rather than about the race. */
.t-empty{
  padding:calc(var(--t-baseline) * 2) 0;
  font-family:var(--t-data);
  font-size:var(--t-fs-1);
  font-weight:700;
  color:var(--t-ink3);
  border-top:var(--t-w-hair) solid var(--t-hair);
}

/* [9.17] One selected treatment, everywhere something can be chosen: the
   reversed slab. It is the only reversal inside a figure, and it survives a
   ground change because it is built from the two ground tokens. */
.yearBtnA.active, .yearBtn.active,
.chartTab.active, .chipBtn.active, .chip.active,
.chamberBtn.active, .pageTab.active{
  background:var(--t-ink);
  color:var(--t-paper);
  border-color:var(--t-ink);
}
/* A toggle set inline in a figure's head is words, not chips: reversing it
   puts a slab in the middle of a line. Its selected mark is the rule under
   it, at the weight the nav's active tab uses — the same decision, applied
   to the same kind of thing. */
.probHead .toggle button{
  background:transparent;
  color:var(--t-muted);
  border:0;
  border-bottom:var(--t-w-contour) solid transparent;
}
.probHead .toggle button.active{
  background:transparent;
  color:var(--t-ink);
  font-weight:700;
  border-bottom-color:var(--t-ink);
}

/* ======================================================================
   20 · ACCESS  (chapter 10)
   ====================================================================== */

/* [10.10] Each sheet's own heading. It is read, not seen: on screen the
   section titles carry the findings, and a second title above them would be
   the page saying its own name back to itself. */
.sheetTitle{
  position:absolute;
  width:1px;height:1px;
  margin:-1px;padding:0;border:0;
  clip-path:inset(50%);
  overflow:hidden;
  white-space:nowrap;
}

/* [10.15] The nav's links were 40px tall on a phone, four short of the
   target. The masthead's controls are already at 44 there; its links were
   not. */
@media (max-width:760px){
  .nav a{
    min-height:44px;
    display:inline-flex;
    align-items:center;
  }
  .skiplink{min-height:44px;display:inline-flex;align-items:center;}
  /* A target is 44 in both directions. The ground toggle was 44 tall and 30
     wide, because the word inside it is short — a height alone is not a
     target. */
  .iconbtn, .chip, .chipBtn, .zoomBtn, .mapCtrlBtn, .yearBtn, .yearBtnA{
    min-width:44px;
  }
  /* The chamber toggle was 28 tall, which is a target for a mouse and not
     for a thumb. */
  .chamberBtn, .seg, .segmentedControl button, .gbToggle button{
    min-height:44px;
    display:inline-flex;
    align-items:center;
  }
  /* [10.16] And the rest of them, found by measurement rather than by
     reading the markup: the segmented controls' buttons rather than their
     containers, the state-legislature mode toggle at 21 tall, the two
     analyses' year and party segments at 26 and 30, and the landing sheet's
     two calls to action at 42 — two pixels short, which is the kind of miss
     that only a number catches. */
  .seg button, .yearseg button, .sldlModeToggle button, .toggles .tbtn,
  .colCta a, .footLinks a{
    min-height:44px;
    display:inline-flex;
    align-items:center;
    justify-content:center;
  }
  .toggles .tbtn{min-width:44px;}
}

/* [7.17] A map's text alternative. Read, not seen: on screen the map is the
   better figure, and the same thirty-five rows printed under it would be the
   sheet saying everything twice. */
.figTable{
  position:absolute;
  width:1px;height:1px;
  margin:-1px;padding:0;border:0;
  clip-path:inset(50%);
  overflow:hidden;
  white-space:nowrap;
}

/* [10.12] A figure that changes under the reader's hands announces the
   change. The polls sheet redraws its state chart on a click and the
   swingometer redraws three maps on a drag; neither said anything. */
.t-announce{
  position:absolute;
  width:1px;height:1px;
  margin:-1px;padding:0;border:0;
  clip-path:inset(50%);
  overflow:hidden;
  white-space:nowrap;
}

/* [10.19] Text at 200%. The page does not scroll sideways at either 200%
   text or 400% zoom, but three things clipped their own content rather than
   growing with it: the ground toggle, the small-state chips and the display
   figures. A control sized for one word is a control that loses the word
   when the word gets bigger. */
.iconbtn, .mapChip{
  width:auto;
  height:auto;
  min-height:var(--t-control-h);
  padding-top:var(--t-sp-1);
  padding-bottom:var(--t-sp-1);
}
/* The display figures share a line until they cannot, and then they take
   two. They were pinned to one row, so at 200% they ran into each other. */
.seats, .seatsSide{flex-wrap:wrap;}
.seats > *, .seatsSide > *{min-width:0;}
.seats .num, .seatsSide .num{overflow:visible;}
