:root {
  --color-dem: #1a80c4;
  --color-dem-light: #6e9cc1;
  --color-gop: #cc3d3d;
  --color-gop-light: #d88d8d;
  --color-lgrey: #e8e8ed;
  --color-grey: #616467;
  --color-ind: #7ead53;
  --color-yes: #27b122;
  --color-no: #1094cf;
}

body {
  font-family: Rubik, system-ui, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  line-height: 1.5;
  margin: 0;
  min-height: 225px;
}

.edash-head {
  align-items: flex-start;
  border-bottom: 2px solid #ececed;
  display: flex;
  flex-flow: column nowrap;
  justify-content: space-between;
  line-height: 1.5rem;
  margin: 0 0 .625rem;
  padding: 0;
  text-align: center;
  text-transform: uppercase;
}

/* An h1 so the document does not start at h2. Margin reset because the heading
   tag brings UA margins the div it replaced did not have. */
.edash-head-title {
  color: var(--color-dem);
  font-size: 1.75rem;
  line-height: 1;
  margin: 0;
}

@media (max-width: 480px) {
  .edash-head-title,
  .towns {
    margin-inline: auto;
  }
}

#refresh {
  cursor: pointer;
}

.races {
  --gap: 2rem;

  display: grid;
  gap: 3rem var(--gap);
  grid-template-columns: repeat(3, 1fr);
}

.race-group > .races {
  grid-template-columns: repeat(3, 1fr);
  row-gap: 2rem;
}

@media (max-width: 810px) {
  .races {
    row-gap: 2rem;
  }

  .race-group > .races {
    row-gap: 1.5rem;
  }

  /* Same 2rem as above, on the margin the questions use instead. */
  .questions > .question {
    margin-block-end: 1.5rem;
  }
}

@media (max-width: 800px) {
  .races,
  .race-group > .races {
    grid-template-columns: 1fr;
  }

  .races {
    padding-inline: 0.25rem;
  }
}

.race-group h2 {
  font-size: 1.5rem;
  margin: 0;
}

/* A race with more candidates than fit one column takes the full grid width and
   splits into two .race lists, so each row keeps the same name/votes/pct shape */
.race-group--wide {
  grid-column: 1 / -1;
}

.bar {
  transition: width 1s ease 0s;
  width: 0;
}

.bar.dem {
  background: var(--color-dem);
}

.bar.gop {
  background: var(--color-gop);
}

.victory {
  border: 0;
  border-right: 2px solid var(--color-grey);
  bottom: -5px;
  left: 50%;
  position: absolute;
  top: -5px;
}

.victory .label {
  color: var(--color-grey);
  font-weight: 600;
  position: absolute;
  top: calc(1.1rem);
  transform: translateX(-50%);
  white-space: nowrap;
}

.electoral-leaderboard {
  align-items: stretch;
  display: flex;
  font-weight: bold;
  justify-content: space-around;
  line-height: 1;
  list-style-type: none;
  margin: 2.5rem 0 0;
  padding: 0;
  text-transform: uppercase;
}

.electoral-leaderboard .party {
  color: #666;
  display: flex;
  flex-direction: column;
  font-size: 3.5rem;
  justify-content: flex-end;
  text-align: center;
}

.electoral-leaderboard .party .label {
  align-items: center;
  display: flex;
  font-size: 16px;
  font-weight: normal;
  gap: 0.4rem;
  margin: 0 auto 6px;
  text-transform: uppercase;
}

.electoral-leaderboard .party.dem {
  color: var(--color-dem);
}

.electoral-leaderboard .party.gop {
  color: var(--color-gop);
}

.electoral-leaderboard .popvotes {
  color: var(--color-grey);
  font-size: 1rem;
  font-weight: 200;
  min-width: 8.2rem;
  padding: 0.5rem 0 0;
  text-transform: uppercase;
  width: 100%;
}

svg#winner {
  fill: currentcolor;
}

.race {
  display: flex;
  flex-flow: column nowrap;
  gap: 0.75rem;
}

/* A race with too many candidates for one column. Must come after .race so it
   wins the display declaration — same specificity, so source order decides. */
.race--split {
  --col-gap: var(--gap, 4rem);

  display: grid;
  gap: 0.75rem var(--col-gap);
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

/* One header per column, so Votes and Percent sit over the numbers they label.
   Collapsed to a single column there is only one column to label, and the second
   header would land in the middle of the list, so it goes away. */
@media (max-width: 800px) {
  .race--split {
    grid-template-columns: 1fr;
  }

  /* Three classes deliberately: `.race .candidate` sets display: flex at equal
     specificity further down the file, so two would lose on source order */
  .race--split > .candidate.vote-hdr--repeat {
    display: none;
  }
}

.race .candidate {
  display: flex;
  flex-flow: row nowrap;
  font-size: 1.2rem;
  gap: 1rem;
  line-height: 1.8;
}

@media (max-width: 480px) {
  .race > .candidate {
    font-size: 1rem;
    gap: 1rem;
  }
}

.race .candidate .name {
  align-items: center;
  display: flex;
  flex-basis: 65%;
  gap: 0.5rem;
}

.race .candidate .name img {
  background: var(--color-lgrey);
  border-radius: 50%;
  flex: 0 0 auto;
  height: 2.5rem;
  object-fit: cover;
  width: 2.5rem;
}

@media (max-width: 480px) {
  .race .candidate .name {
    gap: 0.375rem;
  }

  .race .candidate .name img {
    height: 2rem;
    width: 2rem;
  }
}

.race .candidate .votes {
  flex: 0 0 19%;
}

.race .candidate .pct {
  flex: 0 0 14%;
}

.race > div {
  align-items: center;
  justify-content: space-between;
}

.candidate .pct,
.candidate .votes {
  text-align: right;
}

/* The bar is absolutely positioned against this row's padding box, so the
   padding reserves a band for it and keeps it off the portrait below.
   Tightened here rather than on .race .candidate: the 1.8 line-height there is
   what held the name clear of its bar, putting nearly 8px of leading over the
   cap at 1.2rem, and 1.8 is still right for the rows that have no bar. Since the
   ballot questions moved to a shared track, :has(.bar) is now exactly the
   candidate races. */
[class*='candidate--']:has(.bar) {
  line-height: 1.4;
  padding-top: 0.4rem;
  position: relative;
}

.candidate > .bar {
  background: var(--color-bar);
  height: 0.3rem;
  left: 0;
  position: absolute;
  top: 0;
}

/* Visually hidden but still announced. Carries the winner state as real text,
   which the ✓ pseudo-element below cannot do — generated content is not exposed
   to assistive tech and disappears entirely if the stylesheet fails to load. */
.sr-only {
  clip-path: inset(50%);
  height: 1px;
  overflow: hidden;
  position: absolute;
  white-space: nowrap;
  width: 1px;
}

.winner .name {
  font-weight: 700;
}

/* The name stays ink; only the checkmark takes the bar color. --color-bar is set
   on the row, so it inherits down to this pseudo-element. */
.winner .name::after {
  color: var(--color-bar);
  content: '✓';
  padding: 0 6px 0 4px;
}

.race .candidate.vote-hdr .lbl {
  font-size: 0.875rem;
  line-height: 1;
}

/* Rule above every candidate row. Was :not(:first-of-type), which stood in for
   "not the header" — that broke once the split race grew a second header. */
.race > div:not(.vote-hdr) {
  border-top: 1px solid var(--color-lgrey);
}

.edash-race-meta {
  color: var(--color-grey);
  display: block;
  font-size: 0.75rem;
  font-weight: 200;
  text-transform: uppercase;
}

footer .edash-race-meta {
  text-transform: unset;
}

/* Set on the row, not on > .bar — .name is a sibling of .bar, so a property
   scoped to .bar can't reach the name or its ::after checkmark */
.candidate--dem {
  --color-bar: var(--color-dem);
}

.candidate--gop {
  --color-bar: var(--color-gop);
}

.race .candidate.vote-hdr {
  color: var(--color-grey);
  font-size: 0.875rem;
}

.edash-footer {
  border-top: 2px solid #e8e8ed;
  color: #616467;
  display: flex;
  font-size: 0.75rem;
  font-weight: 200;
  justify-content: space-between;
  margin: 1.5rem 0 0;
  padding: 0.3125rem 0 0;
}

@media (max-width: 800px) {
  .edash-footer {
    flex-direction: column;
    padding-inline: 0.25rem;
  }
}

.refreshing {
  animation: fadeinout 2s linear infinite;
  cursor: not-allowed !important;
}

@keyframes fadeinout {
  0%,
  100% {
    opacity: 0.2;
  }

  50% {
    opacity: 1;
  }
}

.towns a {
  color: var(--color-grey);
  font-size: 0.875rem;
  line-height: 1;
  text-decoration: none;
  text-decoration-thickness: 1px;
  text-transform: initial;
  transition: text-decoration-thickness .2s ease;
}

.towns a:hover {
  text-decoration: underline;
  text-decoration-thickness: 2px;
}

/* Motion off for anyone who asks. The bar width and the refreshing pulse are the
   two that matter — a 1s width animation on every update and an infinite fade.
   dashboard.js checks the same query and writes vote figures out directly rather
   than counting up over two seconds. */
@media (prefers-reduced-motion: reduce) {
  .bar {
    transition: none;
  }

  .refreshing {
    animation: none;
  }

  .towns a {
    transition: none;
  }
}

/* ballot questions
-----------------------------------------------------------------
   A yes/no row is a candidate row with a different colour token, so everything
   above applies unchanged and only --color-bar is set here. */
.candidate--yes {
  --color-bar: var(--color-yes);
}

.candidate--no {
  --color-bar: var(--color-no);
}

/* The nine questions share one grid. Their headings run to one, two or three
   lines, so each block spans three subgrid rows: heading, bar, votes. Without it
   the vote rows start at a different height in every column and the eye has
   nothing to read across.

   A subgrid spans the parent's tracks, so the parent's row-gap is what lands
   inside each block — hence setting it here, and spacing the blocks on a margin
   instead. */
.races.questions {
  row-gap: 0.5rem;
}

.questions > .question {
  display: grid;
  gap: 0.5rem;
  grid-row: auto / span 3;
  grid-template-rows: subgrid;
  margin-block-end: 2.5rem;
}

.questions .question h2 {
  font-size: 1rem;
  line-height: 1.3;
  margin: 0;
}

/* One track per question rather than a bar per answer. Yes and No are the whole
   vote — they sum to 100% exactly, which a candidate race does not — so the two
   fills share a track and meet at the margin. Where that join sits against the
   tick is the answer to the only question a reader has: is it passing?
   Deliberately not used for candidate races, even a two-candidate one: there the
   100% is a coincidence of who filed, not a property of the data. */
.qbar {
  background: var(--color-lgrey);
  display: flex;
  height: 0.75rem;

  /* A touch more room under the bar than above: the row-gap is one value for
     both, and the bar's hard edge against small grey type needs more. */
  margin-block-end: 0.25rem;
  position: relative;
}

.qbar-fill {
  transition: width 1s ease;
  width: 0;
}

.qbar-yes {
  background: var(--color-yes);
}

/* Anchored to the right edge by the auto margin, the way .bar.ind pushes the gop
   bar over on the balance of power track, so No grows inward from its own side
   rather than trailing along behind Yes. The two then travel toward each other
   and meet at the margin.

   The auto margin also decides where any rounding shortfall lands: the two
   percentages sum to 100, but resolving them against the track's pixel width can
   leave a sliver, and this puts it at the join rather than off the right end.

   The inset rule is the seam between the two fills, which land green against
   blue. Kept even with the threshold line above: that one is fixed at 50% and
   this one sits wherever the vote actually split, so the gap between them is the
   margin. */
.qbar-no {
  background: var(--color-no);
  box-shadow: inset 1px 0 0 #ffffff;
  margin-left: auto;
}

/* The 50% a question passes at, drawn like the majority line on the balance of
   power bars: overshooting the track top and bottom so it reads as a threshold
   laid over the result rather than a division inside it. Which is why the track
   above has no radius and no clipping — same as .electoral-bars over there. */
.qbar-mark {
  border: 0;
  border-right: 2px solid currentcolor;
  bottom: -5px;
  color: #000;
  left: calc(50% - 1px);
  position: absolute;
  top: -5px;
}

/* The answer rows no longer carry a bar of their own, so each name takes a
   swatch to tie it to its half of the track above. */
.question .candidate--yes .name::before,
.question .candidate--no .name::before {
  background: var(--color-bar);
  border-radius: 2px;
  content: '';
  display: inline-block;
  flex: 0 0 auto;
  height: 0.75rem;
  width: 0.75rem;
}

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