body {
  font-family: "Rubik", system-ui, sans-serif;
  font-feature-settings: "liga";
  font-variant-ligatures: common-ligatures;
  line-height: 1.2;
  margin: 0 auto;
  max-width: 1200px;
  padding: 0;
}

form {
  font-size: inherit;
  margin: 0;
}

button,
input,
select,
textarea {
  font-size: inherit;
  line-height: inherit;
}

input,
select,
textarea {
  border: 1px solid #ccc;
  border-radius: 0;
  box-shadow: inset 0 1px 3px #eee;
  display: inline-block;
  padding: 0.5em 0.6em;
}

input:focus,
select:focus,
textarea:focus {
  border-color: #73acf3;
  box-shadow: 0 0 4px 0 rgba(18, 159, 234, 0.2);
  outline: 0;
  outline: thin dotted;
  outline: 1px auto #73acf3;
}

/* The race picker. Sits above .graphic-head so switching races does not shift
   the title, and capped to the head's width so the two line up.

   Eleven chips rather than a drop-down: switching is one tap instead of a round
   trip through a menu, and on election night a reader flips between races
   rather than choosing once. It also puts the nine questions on screen, which a
   closed select never did. */
.race-picker {
  align-items: center;
  border: 0;
  column-gap: 12px;
  display: flex;
  flex-wrap: wrap;

  /* The picker is the first thing in the document, so a focused chip's ring had
     nothing above it to draw into and came out sliced along the top. The ring
     travels 3px by declaration — 1px outline, 2px offset — but `outline: auto`
     paints wider than it declares, so this covers it with a pixel to spare. */
  margin: 6px auto 14px;
  max-width: 750px;

  /* A fieldset will not shrink below its contents without this, so the chips
     would push the embed wider instead of wrapping. */
  min-width: 0;
  padding: 0;
  row-gap: 8px;
}

.race-picker-group {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

/* Two offices and nine questions are different kinds of thing, so the rule says
   so rather than leaving eleven chips in one undifferentiated run. */
.race-picker-group--questions {
  border-left: 1px solid #dbe0e7;
  padding-left: 12px;
}

.race-chip {
  align-items: center;
  background: #fff;
  border: 1px solid #dbe0e7;
  border-radius: 3px;
  color: #000;
  cursor: pointer;
  display: inline-flex;
  font-size: 14px;
  font-weight: 600;
  line-height: 1;
  min-height: 36px;
  padding: 0 13px;
  position: relative;
  white-space: nowrap;
}

.race-chip:hover {
  border-color: #9aa3ad;
}

/* Hidden, but still focusable and still a radio to assistive tech. The label
   around it carries the visible state. */
.race-chip input {
  height: 1px;
  margin: 0;
  opacity: 0;
  position: absolute;
  width: 1px;
}

.race-chip:focus-within {
  outline: 1px auto #73acf3;
  outline-offset: 2px;
}

/* Solid ink rather than the race's own color. On this page color is the
   diverging scale, and spending it here would blunt the one place it carries
   data — white on the question green (#27b122) is 2.8:1 in any case. */
.race-chip:has(input:checked) {
  background: #000;
  border-color: #000;
  color: #fff;
}

/* All eleven ride one line down to 714px, measured the same in WebKit and Gecko
   with Rubik loaded. Below that the questions take a line of their own, and the
   rule between the groups goes with them — a vertical hairline at the start of
   a line divides nothing.

   Forced with a full-width basis rather than left to wrap on its own, because
   dropping the rule and its padding gives back 13px, which is enough for the
   group to fit beside the offices again: the break would keep undoing its own
   trigger for a dozen pixels. Being a rule rather than an accident also means
   the run never splits with Q1 to Q4 stranded up on the office row. */
@media (max-width: 714px) {
  .race-picker-group--questions {
    border-left: 0;
    flex-basis: 100%;
    padding-left: 0;
  }
}

/* Where there is no pointer to aim with, the chips grow to a 44px target. Keyed
   to the input rather than the width: a narrow desktop window is still a mouse,
   and a wide tablet is still a thumb. */
@media (pointer: coarse) {
  .race-chip {
    min-height: 44px;
  }
}

/* Takes the space the key leaves rather than its own content width — a long
   question is wider than the head, so an auto basis would push the key onto a
   line of its own and strand it left-aligned under the text. The 320px basis is
   what still makes the key wrap below on a phone, where there is no room to sit
   beside anything. */
.race-id {
  flex: 1 1 320px;
  min-width: 0;
}

/* The question itself, directly under the title. The chip only has room for
   "Q7". Governor and Senate have no question to carry, so the line is left
   empty rather than removed — taking it out of flow dropped the map up under
   the title and back down on every switch between a race and a question. */
.race-blurb {
  color: #5f636a;
  font-size: 15px;
  line-height: 1.45;
  margin: 4px 0 0;
  min-height: 1lh;
}

/* Visually hidden but still announced — the fieldset's legend, which names what
   the eleven chips are a choice between. */
.sr-only {
  clip-path: inset(50%);
  height: 1px;
  overflow: hidden;
  position: absolute;
  white-space: nowrap;
  width: 1px;
}

/* disclosure
----------------------------------------------------------------- */

/* The panel is hidden with the `hidden` attribute rather than a class, so it
   leaves the accessibility tree and the tab order together with the layout.
   !important guards against any later display rule silently revealing it. */
[hidden] {
  display: none !important;
}

.disclosure {
  align-items: center;
  background: none;
  border: 0;
  color: #000000;
  display: flex;
  font-family: "Rubik", system-ui, sans-serif;
  font-size: 18px;
  font-weight: 600;
  gap: 10px;
  margin: 0 auto 20px auto;
  padding: 10px 0;
  text-align: left;
  width: 100%;
}

/* Caret is decorative and driven entirely by aria-expanded, so the visual state
   and the announced state cannot drift apart. */
.disclosure::before {
  border-color: transparent transparent transparent currentColor;
  border-style: solid;
  border-width: 6px 0 6px 9px;
  content: "";
  flex: 0 0 auto;
  height: 0;
  transition: transform 0.15s ease;
  width: 0;
}

.disclosure[aria-expanded="true"]::before {
  transform: rotate(90deg);
}

.disclosure:focus-visible {
  outline: 2px solid #73acf3;
  outline-offset: 2px;
}

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

/* buttons
----------------------------------------------------------------- */
button {
  cursor: pointer;
  display: inline-block;
  line-height: 1;
  text-align: center;
  user-select: none;
  vertical-align: baseline;
  white-space: nowrap;
}

button[disabled] {
  box-shadow: none;
  cursor: not-allowed;
  filter: alpha(opacity=40);
  opacity: 0.4;
}

button.map-refresh {
  background: #ffffff;
  border: 1px solid #8f959d;
  border-radius: 4px;
  color: #16171a;
  font-family: "Rubik", system-ui, sans-serif;
  font-size: 13px;
  font-weight: 600;
  padding: 6px 12px;
  transition: background-color 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}

/* Inverts rather than tinting. The previous #f4f5f7 fill was a 4% shift off
   white — the rule fired but read as nothing, which is indistinguishable from
   a broken hover. */
.map-refresh:not([disabled]):hover {
  background: #16171a;
  border-color: #16171a;
  color: #ffffff;
}

/* Pressed state carries the affordance without motion */
.map-refresh:not([disabled]):active {
  background: #000000;
  border-color: #000000;
}

/* Overrides the blanket button[disabled] opacity: 0.4, which on near-black text
   leaves an unreadable grey. Recessed fill reads as not-yet rather than broken. */
.map-refresh[disabled] {
  background: #f7f8f9;
  border-color: #e2e4e7;
  color: #6f757d;
  opacity: 1;
}

.map-refresh:focus-visible {
  outline: 2px solid #16171a;
  outline-offset: 2px;
}

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


/* colors
-----------------------------------------------------------------
   Three palettes, picked by the class the race puts on <body>: party colors for
   governor and senate, yes/no colors for the ballot questions, and a neutral
   pair as the fallback. Every shade is the same color at a lower opacity, so a
   race needs two custom properties rather than forty rules — which is what the
   ten near-identical .ballot-qN blocks that used to sit here were. */
body {
  --col0: #008300;
  --col1: #4a3aa7;
  --col2: #756bb1;
}

body.governor,
body.senate {
  --col0: #3989cb;
  --col1: #bd1c00;
}

body[class*="ballot-q"] {
  --col0: #27b122;
  --col1: #1094cf;
}

.col0 {
  color: var(--col0);
}

.col1 {
  color: var(--col1);
}

.col2 {
  color: var(--col2);
}

.bar.col0,
.town.col0 {
  background-color: var(--col0);
  fill: var(--col0);
}

.bar.col1,
.town.col1 {
  background-color: var(--col1);
  fill: var(--col1);
}

/* A third color for the rare race with a viable independent. Never shaded: the
   margin ramp is between the top two, so a col2 town takes the flat fill. */
.bar.col2,
.town.col2 {
  background-color: var(--col2);
  fill: var(--col2);
}

/* shade5 is the full-strength fill declared above, so it needs no rule of its
   own; the rest step down. fill-opacity rather than a translucent fill so the
   0.5 stroke between towns stays solid at every shade. */
.town.shade4 {
  fill-opacity: 0.8;
}

.town.shade3 {
  fill-opacity: 0.6;
}

.town.shade2 {
  fill-opacity: 0.4;
}

.town.shade1 {
  fill-opacity: 0.3;
}

.keys {
  font-size: 14px;
  line-height: 1.2;
}

.keys .square {
  display: inline-block;
  height: 14px;
  width: 14px;
}

/* The two ramps meet in the middle: sqt5 is col0 at full strength, sqb5 is col1.
   Opacity over the white page reads the same as the rgba fills these replaced. */
.keys .square.sqt5 {
  background-color: var(--col0);
}

.keys .square.sqt4 {
  background-color: var(--col0);
  opacity: 0.8;
}

.keys .square.sqt3 {
  background-color: var(--col0);
  opacity: 0.6;
}

.keys .square.sqt2 {
  background-color: var(--col0);
  opacity: 0.4;
}

.keys .square.sqt1 {
  background-color: var(--col0);
  opacity: 0.3;
}

.keys .square.sqb1 {
  background-color: var(--col1);
  opacity: 0.3;
}

.keys .square.sqb2 {
  background-color: var(--col1);
  opacity: 0.4;
}

.keys .square.sqb3 {
  background-color: var(--col1);
  opacity: 0.6;
}

.keys .square.sqb4 {
  background-color: var(--col1);
  opacity: 0.8;
}

.keys .square.sqb5 {
  background-color: var(--col1);
}

.graphic {
  margin: 0 auto;
  max-width: 960px;
  width: 100%;
}
/* Mirrors .graphic-head. Both rows are about the map rather than part of it, so
   they bracket it: identity above, provenance and controls below. */
.graphic-foot {
  align-items: center;
  column-gap: 20px;
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  margin: 0 auto 18px auto;
  row-gap: 6px;
  width: 100%;
}

/* Was #999fa5 at 2.67:1 — below AA on attribution the AP requires be legible */
.credit {
  color: #5f636a;
  font-size: 12px;
  font-weight: 400;
  line-height: 1.3;
}
.graphic-content {
  height: 0;
  margin-bottom: 10px;
  overflow: hidden;
  padding-bottom: 75%;
  padding-top: 0;
  position: relative;
  width: 100%;
}
.graphic-content svg {
  background-color: transparent;
  display: inline-block;
  height: 100%;
  left: 0;
  max-width: 100%;
  position: absolute;
  top: 0;
  width: 100%;
  z-index: 1000;
}
.graphic-content .infobox {
  bottom: 0;
  display: block;
  font-size: 16px;
  height: auto;
  left: 0;
  line-height: 1;
  pointer-events: none;
  position: absolute;
  width: 100%;
}
.graphic-content .infobox h2 {
  font-size: 22px;
  font-weight: 200;
  margin: 0 0 4px;
}
.graphic-content .infobox table {
  border: none;
  font-size: 16px;
  margin: 12px 0 0;
  padding: 0;
}
.graphic-content .infobox .infotable th {
  border: none;
  font-weight: normal;
  padding: 2px 14px 2px 0;
  text-align: left;
  white-space: nowrap;
}
.graphic-content .infobox table td {
  border: none;
  padding: 2px 5px;
  text-align: right;
}
.graphic-content .infobox .infotable td:has([class^='info-vote']) {
  padding-right: 10px;
}
.graphic-content .infobox .infotable td:has([class^='info-pct']) {
  border-left: 2px solid #dbe0e7;
  padding-left: 10px;
}
.graphic-content .infobox .winner-img {
  margin-left: 0.5rem;
  width: 14px;
}
.graphic-content .infobox .info-precincts {
  white-space: nowrap;
}
.graphic-content .infobox table td.info-bar0,
.graphic-content .infobox table td.info-bar1,
.graphic-content .infobox table td.info-bar2 {
  text-align: left;
}
.graphic-content .infobox table td.info-bar0 .bar,
.graphic-content .infobox table td.info-bar1 .bar,
.graphic-content .infobox table td.info-bar2 .bar {
  display: inline-block;
  height: 10px;
}
.graphic-content .keys {
  display: none;
}
.graphic-content .square {
  display: inline-block;
  height: 10px;
  width: 10px;
}
.table-wrapper {
  margin: 0 auto 20px auto;
}

/* Stands in for the results when a race has no file to read. Sits where the
   map's top edge would be, so the page does not jump when it appears. */
.race-status {
  background: #f7f8f9;
  border-radius: 3px;
  color: #5f636a;
  font-size: 15px;
  margin: 0 auto 14px;
  max-width: 750px;
  padding: 12px 15px;
}

.graphic-head {
  align-items: baseline;
  column-gap: 24px;
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  margin: 0 auto 14px auto;
  max-width: 750px;
  row-gap: 8px;
  width: 100%;
}

.race-title {
  font-size: 26px;
  letter-spacing: -0.015em;
  line-height: 1.15;
  margin: 0;
}

/* margin key
-----------------------------------------------------------------
   Compact by design: the statewide result is already reported in full by the
   infobox, so the key only has to name the two colors and say which direction
   is a wider margin. Swatches reuse the per-race sqt/sqb palette and the names
   reuse col0 and col1, so this stays correct for the senate and ballot-question
   templates without new tokens. */
.scale {
  text-align: right;
}

.scale-row {
  align-items: center;
  display: flex;
  gap: 8px;
  justify-content: flex-end;
  margin: 0;

  .keys.scale-bar {
    border-radius: 2px;
    display: flex;
    flex: 0 0 auto;
    overflow: hidden;
    width: 140px;
  }
}

.scale-cand {
  font-size: 16px;
  font-weight: 700;
  white-space: nowrap;
}

.scale-note {
  color: #5f636a;
  font-size: 11px;
  letter-spacing: 0.01em;
  margin: 4px 0 0;
}


/* Five classes on purpose: the per-race rules pin a fixed 14px square at four,
   which would tie and be settled by source order. These must win outright. */
.scale .keys.scale-bar .square {
  flex: 1 1 0;
  height: 9px;
  width: auto;
}

/* Marks where the lead flips, doing the job the old "Even" tick label did */
.scale .keys.scale-bar .square:nth-child(5) {
  margin-right: 3px;
}

/* Digits stop shifting as counts tick up on election night */
.infobox table td,
.table-wrapper table {
  font-variant-numeric: tabular-nums;
}
/* auto, not hidden: a table cannot be squeezed below the sum of its columns'
   min-content widths, so max-width alone cannot prevent overflow. hidden made
   the columns past the edge unreachable; auto scrolls them inside this box and
   keeps the page itself from scrolling sideways. */
.table-wrapper {
  overflow-x: auto;
  width: 100%;
}

.table-wrapper table {
  border-bottom: none;
  font-size: 15px;
  line-height: 1;
  margin: 0 0 20px 0;
  max-width: 100%;
  width: 100%;
}

/* Auto table layout sized this column for its single longest value: 338 of the
   351 town names are under 12 characters, but Manchester-by-the-Sea is 21 and
   was setting the width for every row. Capped just above the longest
   unbreakable word (Middleborough, 13 characters) so the cap is honoured rather
   than overridden by min-content, and the 13 long names wrap at the spaces and
   hyphens they already contain. */
.table-wrapper table th:first-child,
.table-wrapper table td:first-child {
  width: 13ch;
}
/* DataTables 2 renames the search wrapper: 1.10's #datatable_filter is gone */
.dt-search {
  margin: 10px 0;
}

/* DataTables reserves 30px of padding on every sortable header for its sort
   arrows — across six columns that is 180px, wider than a whole column. Pull the
   arrow in and cut the reservation to match. Scoped by id so it outranks
   DataTables' own two-class selector outright, rather than depending on which
   stylesheet the browser happens to read second. */
#datatable thead > tr > th {
  padding-right: 18px;
}

#datatable thead > tr > th span.dt-column-order {
  right: 4px;
}
.town {
  cursor: pointer;
  display: block;
  fill: #ddd;
  stroke: #efefef;
  stroke-width: 0.5;
  transition: fill 0.25s ease, fill-opacity 0.25s ease;
}
.town:hover,
.town:focus,
.town.searched {
  outline: none;
  stroke: #000;
  stroke-width: 2;
}
#tooltip {
  background: #fff;
  border-radius: 3px;
  box-shadow: 1px 1px 2px 1px #ccc;
  font-size: 16px;
  left: 300px;
  padding: 1px 5px;
  pointer-events: none;
  position: absolute;
  top: 400px;
  visibility: hidden;
  z-index: 100000;
}
#tooltip:after {
  border: 10px solid transparent;
  border-bottom: 0;
  border-right: 0;
  border-top-color: #fff;
  bottom: 0;
  content: "";
  height: 0;
  left: 18px;
  margin-bottom: -9px;
  margin-left: -6.5px;
  position: absolute;
  width: 0;
}
/* Six columns of results do not fit this embed: the text plus DataTables' own
   cell padding is already ~710px before a single sort arrow, and the widest the
   embed ever gets is 750px. So the two raw vote counts drop first — the
   percentages are what a reader compares, and the counts stay available in the
   infobox and by widening. */
@media (max-width: 760px) {
  .hide-for-medium {
    display: none;
  }
}

@media (max-width: 525px) {
  .hide-for-small {
    display: none;
  }
}
@media (max-width: 360px) {
  .hide-for-extrasmall {
    display: none;
  }
}
@media (min-width: 445px) {
  .graphic-content {
    padding-bottom: 62.5%;
  }
  .graphic-content .infobox {
    font-size: 16px;
    width: 50%;
  }
  #datatable > tbody > tr > td:nth-child(1) {
    white-space: nowrap;
  }
}
@media (min-width: 600px) {
  .graphic-content .keys {
    display: block;
    font-size: 14px;
    line-height: 1;
    position: absolute;
    right: 0;
    top: 3%;
  }
  .table-wrapper table {
    font-size: 17px;
  }
  .graphic-content .infobox table {
    font-size: 24px;
  }
}
@media (min-width: 640px) {
  .dt-container .dt-search {
    float: none;
    text-align: right;
  }
}
@media (min-width: 699px) {
  .graphic-content .infobox {
    bottom: 3%;
  }

  .graphic-content .keys {
    right: 8%;
  }
}
.graphic-content .infobox h2 {
  font-size: 30px;
}
h3 + small {
  font-size: 14px;
  font-weight: 200;
}
.graphic-content .infobox table td.info-bar0 .bar,
.graphic-content .infobox table td.info-bar1 .bar,
.graphic-content .infobox table td.info-bar2 .bar {
  height: 16px;
}

/* The town fills cross-fade on every refresh; the disclosure caret and the
   refresh button are guarded where they are declared. */
@media (prefers-reduced-motion: reduce) {
  .town {
    transition: none;
  }
}
