: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: #3ec339;
  --color-no: #6bc6f3;
}

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: 4rem;

  display: grid;
  gap: 3rem var(--gap);
  grid-template-columns: repeat(2, 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;
  }
}

@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 */
[class*='candidate--']:has(.bar) {
  padding-top: 0.5rem;
  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;
  }
}
