:root {
  color-scheme: light dark;
  --border: #d0d0d8;
  --tied: #b45309;
  --muted: #6b7280;
  --withdrawn: #9ca3af;
  --favorite: #f59e0b;
  --link: #1d4ed8;
}

/* The link blue is too dark to read against a dark background; lighten it there. */
@media (prefers-color-scheme: dark) {
  :root {
    --link: #93c5fd;
  }
}

/* The page's colours are set outright rather than left to the browser's default canvas. Some in-app
   browsers and forced-dark modes recolour a page that leaves them unset, and could lighten the text
   without darkening the background. The `color-scheme` meta tag in each page's head says the same thing
   before this stylesheet loads. */
html {
  background: Canvas;
  color: CanvasText;
}

body {
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
  max-width: 760px;
  margin: 2rem auto;
  padding: 0 1rem;
  line-height: 1.4;
}

header {
  margin-bottom: 1.5rem;
}

h1 {
  margin-bottom: 0.25rem;
}

/* "Tiebreaker game: Kong", under the tournament name. */
.tiebreak-game {
  margin: 0 0 0.25rem;
}

/* Sits directly above the "Published …" line, as its first line. */
.qualifying-ended {
  margin-bottom: 0;
}

.qualifying-ended:not([hidden]) + #generated-at {
  margin-top: 0;
}

.muted {
  color: var(--muted);
  font-size: 0.9rem;
}

/* The panel/scroll buttons (Favorites, Milestones, Restrictions, Links, About), left-aligned on one
   wrapping row. The division switch is not among them — see .division-tabs below. */
.header-controls {
  margin-top: 0.75rem;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.tournament-value-toggle {
  font: inherit;
  padding: 0.3rem 0.8rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: transparent;
  color: inherit;
  cursor: pointer;
}

.tournament-value-toggle[aria-pressed="true"] {
  background: #1f2937;
  color: #f9fafb;
  border-color: transparent;
}

/* "↑ Top" at the foot of the page, matching the header pills and aligned with them, and the "All boards"
   link to the menu pushed to the far end. */
.back-to-top-row {
  margin: 1.5rem 0 0.5rem;
  display: flex;
  justify-content: flex-start;
  align-items: center;
}

/* Deliberately quiet: small muted text, no pill, underlined only on hover. The board is what the page is
   for; this is for the few who want another one. */
.all-boards-link {
  margin-left: auto;
  color: var(--muted);
  font-size: 0.8rem;
  text-decoration: none;
}

.all-boards-link:hover,
.all-boards-link:focus-visible {
  text-decoration: underline;
}

/* ----------------------------------------------------------------------------------------------
   Division switch, drawn as tabs around the standings.
   The strip sits directly above the panel and is pulled down one pixel so the tabs' bottom borders land
   on the panel's top border. The selected tab then paints its own bottom border in the page background
   colour, punching a gap in that line so the tab and the panel below read as one shape.
   ---------------------------------------------------------------------------------------------- */
.division-tabs {
  display: flex;
  gap: 0.25rem;
  margin-top: 1.5rem;
  margin-bottom: -1px;
  padding-left: 0.5rem;
  position: relative;
  z-index: 1; /* so the selected tab's background covers the panel border it overlaps */
  /* Last resort for more divisions than even the compact labels fit: scroll the strip, not the page. */
  overflow-x: auto;
  scrollbar-width: none;
}

.division-tabs[hidden] {
  display: none;
}

.division-tabs button {
  font: inherit;
  padding: 0.45rem 1.1rem;
  border: 1px solid var(--border);
  border-radius: 10px 10px 0 0;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  white-space: nowrap;
}

.division-tabs button:hover {
  color: inherit;
}

/* The muted gray that dims the unselected tabs only reaches ~3.9:1 on a dark canvas — too low for a
   control this central. Lift it the same way the tie-panel caption and About note are lifted. */
@media (prefers-color-scheme: dark) {
  .division-tabs button {
    color: #aab2c0;
  }
}

.division-tabs button[aria-selected="true"] {
  background: Canvas;
  border-bottom-color: Canvas;
  color: inherit;
  font-weight: 600;
}

/* Tighten the tabs on a phone so three "Division X" labels fit across a 360px screen. */
@media (max-width: 480px) {
  .division-tabs {
    padding-left: 0.25rem;
  }

  .division-tabs button {
    padding: 0.45rem 0.6rem;
  }
}

/* More tabs than fit with full labels: fitDivisionTabs() (app.js) drops the "Division " prefix so each
   tab is just its letter, keeping the same padding so they're still easy to tap. */
.division-tabs.compact .division-tab-prefix {
  display: none;
}

.division-tabs.compact button {
  min-width: 2.75rem;
}

/* The panel the tabs wrap: the finalists filter above the standings. Its top-left corner stays square
   because the first tab sits on it; when the strip is hidden (one division, or a `?view=` locked kiosk)
   the panel closes back up into an ordinary rounded box. */
.division-panel {
  border: 1px solid var(--border);
  border-radius: 0 10px 10px 10px;
  padding: 1rem 1rem 0.25rem;
}

.division-tabs[hidden] + .division-panel {
  border-radius: 10px;
}

#divisions {
  display: block;
}

/* ----------------------------------------------------------------------------------------------
   Jump bar for the one-list layout (a tournament with no division restrictions): a button per division
   that scrolls to its banner row. Sticky inside the panel, so it rides along the whole list and lets go
   with the panel; a solid background hides the rows scrolling under it. The division on screen is
   marked with aria-current and drawn filled, like a pressed header pill.
   ---------------------------------------------------------------------------------------------- */
.division-jump {
  position: sticky;
  top: 0;
  z-index: 6;
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin: -0.5rem 0 0.5rem;
  padding: 0.5rem 0;
  background: Canvas;
  border-bottom: 1px solid var(--border);
  overflow-x: auto;
  scrollbar-width: none;
}

.division-jump[hidden] {
  display: none;
}

.division-jump button {
  font: inherit;
  font-size: 0.9rem;
  padding: 0.3rem 0.9rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: transparent;
  color: inherit;
  cursor: pointer;
  white-space: nowrap;
}

.division-jump button[aria-current="true"] {
  background: #1f2937;
  color: #f9fafb;
  border-color: transparent;
  font-weight: 600;
}

/* Bare letters when "Division X" doesn't fit across the screen (fitDivisionJump), as with the tabs. */
.division-jump.compact .division-tab-prefix {
  display: none;
}

.division-jump.compact button {
  min-width: 2.75rem;
}

/* "Show only finalists", a pill like the header buttons that fills in while it's on. */
.finalists-filter-row {
  margin-bottom: 0.25rem;
}

.finalists-filter-row[hidden] {
  display: none;
}

.finalists-filter {
  font: inherit;
  font-size: 0.9rem;
  padding: 0.3rem 0.8rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: transparent;
  color: inherit;
  cursor: pointer;
}

/* A check mark that appears when the filter is on, so its state reads without relying on the fill. */
.finalists-filter[aria-pressed="true"]::before {
  content: "✓ ";
}

.finalists-filter[aria-pressed="true"] {
  background: #1f2937;
  color: #f9fafb;
  border-color: transparent;
}

/* On a phone the frame's side padding comes straight out of the table's width; keep the frame but give
   most of that space back to the rows. */
@media (max-width: 480px) {
  .division-panel {
    padding: 0.75rem 0.6rem 0.25rem;
  }
}

/* Tournament navigation tabs (one per manifest entry). Sits above the header; hidden when there's only
   one tournament and in signage mode. */
.tournament-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: 1.25rem;
  border-bottom: 1px solid var(--border);
  padding-bottom: 0.75rem;
}

.tournament-tabs[hidden] {
  display: none;
}

.tournament-tab {
  font: inherit;
  padding: 0.3rem 0.9rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: transparent;
  color: inherit;
  cursor: pointer;
}

.tournament-tab.is-active {
  background: #1f2937;
  color: #f9fafb;
  border-color: transparent;
}

/* Landing page: a card per tournament linking into its standings. */
.tournament-cards {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.tournament-card {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  padding: 0.9rem 1.1rem;
  border: 1px solid var(--border);
  border-radius: 12px;
  text-decoration: none;
  color: inherit;
}

.tournament-card:hover,
.tournament-card:focus-visible {
  border-color: #1f2937;
}

.tournament-card-name {
  font-size: 1.15rem;
  font-weight: 600;
}

.tournament-card-meta {
  color: var(--muted);
  font-size: 0.9rem;
}

/* Favorites dropdown menu. */
.favorites-menu {
  position: relative;
}

.favorites-menu-button {
  font: inherit;
  padding: 0.3rem 0.8rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: transparent;
  color: inherit;
  cursor: pointer;
}

.favorites-menu-list {
  position: absolute;
  top: calc(100% + 0.3rem);
  left: 0;
  z-index: 10;
  min-width: 12rem;
  display: flex;
  flex-direction: column;
  padding: 0.3rem;
  border: 1px solid var(--border);
  border-radius: 0.5rem;
  background: Canvas;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);
}

.favorites-menu-list[hidden] {
  display: none;
}

.favorites-menu-list button {
  font: inherit;
  text-align: left;
  padding: 0.4rem 0.6rem;
  border: none;
  border-radius: 0.35rem;
  background: transparent;
  color: inherit;
  cursor: pointer;
  white-space: nowrap;
}

.favorites-menu-list button:hover,
.favorites-menu-list button:focus-visible {
  background: rgba(245, 158, 11, 0.14);
}

/* "Clear favorites" is the one destructive item, so a rule sets it apart from the rest of the menu. */
.favorites-menu-list button.destructive {
  margin-top: 0.25rem;
  padding-top: 0.5rem;
  border-top: 1px solid var(--border);
  border-radius: 0 0 0.35rem 0.35rem;
}

/* Disabled items (Unpin / Share / Clear with nothing favorited): dimmed and inert, no hover highlight. */
.favorites-menu-list button:disabled {
  opacity: 0.45;
  cursor: default;
}

.favorites-menu-list button:disabled:hover {
  background: transparent;
}

.tournament-value {
  margin-bottom: 1.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.tournament-value[hidden] {
  display: none;
}

.tv-bar {
  position: relative;
}

.tv-track {
  height: 1.4rem;
  background: #e5e7eb;
  border-radius: 999px;
  overflow: hidden;
}

.tv-fill {
  height: 100%;
  width: 0;
  background: #9ca3af;
  transition: width 0.3s ease;
}

.tv-fill.met {
  background: #16a34a;
}

.tv-label {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 0.5rem;
  font-size: 0.72rem;
  font-weight: 600;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  pointer-events: none;
}

/* Two-layer label so text stays legible regardless of fill width: a dark base label reads
   over the light-gray track, and a white copy is clip-path'd to exactly the filled fraction
   (driven by --tv-fill-pct, the same percentage the fill width uses) so it only paints over
   the colored fill. */
.tv-label--under {
  color: #1f2937;
  text-shadow: none;
}

.tv-label--over {
  color: #fff;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.55);
  clip-path: inset(0 calc(100% - var(--tv-fill-pct, 0%)) 0 0);
}

.tv-text {
  margin-top: 0.5rem;
  font-size: 0.85rem;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

/* Rank/division restrictions panel (toggled by the "Restrictions" header button). */
.restrictions {
  margin-bottom: 1.5rem;
  font-size: 0.9rem;
}

.restrictions[hidden] {
  display: none;
}

.restrictions-heading {
  font-weight: 600;
  margin-bottom: 0.35rem;
}

.restrictions-band-list {
  margin: 0;
  padding-left: 1.2rem;
}

.restrictions-band-list li {
  margin: 0.1rem 0;
}

/* Compact per-player restriction pill (e.g. "A only") shown beside a restricted player's name in the
   standings tables. Hidden by default; the "Restrictions" toggle adds `show-restrictions` to <body>
   to reveal them, mirroring how the favorites stars are gated. */
.restriction-label {
  display: none;
  font-size: 0.68rem;
  font-weight: 600;
  color: var(--muted);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0 0.35rem;
  white-space: nowrap;
  vertical-align: middle;
}

body.show-restrictions .restriction-label {
  display: inline-block;
}

/* Rank-notice badge (e.g. "Top 250"): same gating as the restriction pill, and never on signage, which
   carries no TD information. */
.rank-notice-label {
  display: none;
  font-size: 0.68rem;
  font-weight: 600;
  color: var(--muted);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0 0.35rem;
  white-space: nowrap;
  vertical-align: middle;
}

body.show-restrictions .rank-notice-label {
  display: inline-block;
}

body.signage .rank-notice-label,
body.signage details.rank-noticed {
  display: none;
}

/* The About panel (toggled by the "About" header button, or opened with ?about=1). Its copy is static
   markup in index.html; these rules only give it a readable measure and rhythm. Hidden in signage mode
   along with the button that opens it. */
.about {
  margin-bottom: 1.5rem;
  padding: 1rem 1.25rem;
  border: 1px solid var(--border);
  border-radius: 12px;
  font-size: 0.95rem;
}

.about[hidden] {
  display: none;
}

.about h2 {
  font-size: 1.15rem;
  margin: 0 0 0.5rem;
}

.about h3 {
  font-size: 1rem;
  margin: 1.1rem 0 0.35rem;
}

.about p {
  margin: 0.5rem 0;
}

.about ul {
  margin: 0.5rem 0 0;
  padding-left: 1.2rem;
}

.about li {
  margin: 0.45rem 0;
}

/* On a phone the panel is most of the screen; reclaim the side padding for the text itself. */
@media (max-width: 480px) {
  .about {
    padding: 0.9rem 0.9rem;
  }
}

/* ----------------------------------------------------------------------------------------------
   Links (snapshot `links`). Ordinary clickable links below the standings; in signage mode the same
   list is drawn as QR codes instead (see the signage block at the end of this file), since nobody can
   click a screen across the room.
   ---------------------------------------------------------------------------------------------- */
.links {
  margin: 2rem 0 1rem;
  padding-top: 1rem;
  border-top: 1px solid var(--border);
}

.links[hidden] {
  display: none;
}

.links-heading {
  font-size: 1rem;
  margin: 0 0 0.5rem;
}

.links-list {
  margin: 0;
  padding-left: 1.2rem;
}

.links-list li {
  margin: 0.3rem 0;
}

.links-list a {
  color: var(--link);
}

/* One tile per link: the code above the label a viewer reads before deciding to scan. The <svg> carries
   its own white background and quiet zone, so it needs no padding of its own — and stays black-on-white
   in dark mode, because not every scanner handles an inverted code. */
.links-qr-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 1.5rem 2rem;
}

.qr-tile {
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
}

.qr-code {
  display: block;
  width: clamp(180px, 16vw, 300px);
  height: auto;
  border-radius: 4px;
}

.qr-caption {
  font-size: 1.05rem;
  font-weight: 600;
  text-align: center;
  max-width: clamp(180px, 16vw, 300px);
}

/* Grouped tournament-value readout: one section per milestone family (player count / rated player
   count / Hybrid), each opening with a "Current status" group and then one group per goal. Status
   and goal lines are indented under their group heading. */
.tv-section + .tv-section {
  margin-top: 0.7rem;
}

.tv-section-heading {
  font-weight: 700;
}

.tv-group {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

/* Inherits the normal text color: the indent is what separates a heading from its lines, so
   dimming it as well made the structure read as less important than it is. */
.tv-group-heading {
  margin-top: 0.3rem;
}

.tv-group .tv-text-line {
  padding-left: 1.1rem;
}

.tv-text-line {
  display: flex;
  gap: 0.5rem;
  align-items: baseline;
}

.tv-text-icon {
  width: 1.1em;
  text-align: center;
  flex-shrink: 0;
}

.tv-text-icon.met {
  color: #16a34a;
  font-weight: 700;
}

/* The empty-progress ring (U+25CB) measures the same as the filled-quadrant stages (◔◑◕) but is drawn
   as a hairline, so it reads faint beside them. Thickening just its stroke evens out the ramp without
   changing the glyph's size or its "no progress yet" meaning. */
.tv-text-icon.empty {
  -webkit-text-stroke: 0.4px currentColor;
}

.division {
  margin-bottom: 2rem;
}

.division h2 {
  border-bottom: 2px solid var(--border);
  padding-bottom: 0.25rem;
}

/* How a field-sized division got its size ("69 spots — half of the 138 players outside A, rounded up"). */
.division-sizing {
  margin: -0.25rem 0 0.75rem;
  font-size: 0.85rem;
  color: var(--muted);
}

table {
  width: 100%;
  border-collapse: collapse;
}

th, td {
  text-align: left;
  padding: 0.35rem 0.5rem;
  border-bottom: 1px solid var(--border);
}

th {
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: #6b7280;
}

td.rank, th.rank {
  width: 3rem;
}

/* The one-list layout's Rank and Seed columns: narrow, with the headings turned to read upward. */
th.seed-col {
  padding-left: 0.2rem;
  padding-right: 0.2rem;
  vertical-align: bottom;
}

td.seed-col, th.seed-col {
  width: 2.25rem;
}

.vertical-head {
  display: inline-block;
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  white-space: nowrap;
}

td.points, th.points {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.tied {
  color: var(--tied);
  font-weight: 600;
}

/* Star toggle, shared by the standings rows and the pinned strip. Hidden by default; the "Add/Remove
   favorites" menu item adds `show-stars` to <body> to reveal them. `display:none` also drops them from
   the tab order while hidden. */
.fav-toggle {
  display: none;
  font: inherit;
  line-height: 1;
  padding: 0 0.2rem;
  margin-right: 0.15rem;
  border: none;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  vertical-align: baseline;
}

body.show-stars .fav-toggle {
  display: inline;
}

.fav-toggle:hover,
.fav-toggle.is-fav {
  color: var(--favorite);
}

/* A favorited player's row: soft amber wash plus an amber edge on the leading cell. Applies to the
   main standings tables and to the bubble-watch / ineligible-restricted tables alike (their first cell
   isn't a rank cell, so target :first-child rather than td.rank). */
tr.favorite td {
  background: rgba(245, 158, 11, 0.12);
}

tr.favorite td:first-child {
  box-shadow: inset 0.2rem 0 0 var(--favorite);
}

/* Per-division "Favorites" box shown at the top of a division when favorites are pinned. */
.favorites-box {
  margin: 0.5rem 0 0.75rem;
  border: 1px solid var(--border);
  border-radius: 0.5rem;
  padding: 0.5rem 0.75rem;
}

.favorites-heading {
  font-size: 0.9rem;
  margin: 0 0 0.35rem;
  border: none;
}

.favorites-list {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}

/* Star, name, and the rank·points summary sit next to each other with a small, even gap — no
   auto-margin pushing the summary to the far edge (that left an awkward gap on wide screens). */
.favorites-item {
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
}

.favorites-item .fav-toggle {
  display: inline; /* always actionable in the box, independent of the show-stars mode */
}

.favorites-name {
  font-weight: 600;
}

.favorites-meta {
  color: var(--muted);
  font-size: 0.82rem;
  font-variant-numeric: tabular-nums;
}

.withdrawn td {
  color: var(--withdrawn);
}

/* A seat in the one-list layout held for whoever loses a cutline tiebreaker game: no player yet, so it
   reads as a blank to be filled in, quieter than the real rows around it. */
.placeholder-row td {
  color: var(--muted);
  font-style: italic;
}

@media (prefers-color-scheme: dark) {
  .placeholder-row td {
    color: #aab2c0;
  }
}

/* Cutline (finalists/contenders boundary) and byeline (tiebreaker-decided cut) separators. */
.line-row td {
  padding: 0.15rem 0.5rem;
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  border-bottom: none;
}

.cutline-row td {
  font-size: 0.875rem;
  padding-top: 0.4rem;
  color: #b91c1c;
  /* Paint the separator with a gradient background (not a border) so it shares the byeline's
     rendering path — same behavior under `border-collapse: collapse` if any quirk crops up. */
  border-top: none;
  background-image: linear-gradient(#b91c1c, #b91c1c);
  background-repeat: no-repeat;
  background-position: top;
  background-size: 100% 6px;
}

.byeline-row td {
  font-size: 0.875rem;
  padding-top: 0.4rem;
  color: #b91c1c;
  /* A CSS `dashed` border renders inconsistently under `border-collapse: collapse`
     (dashes get squeezed to fit each cell edge, so the middle can look solid).
     Paint the dashes with a gradient instead for a uniform dash/gap at any width. */
  border-top: none;
  background-image: repeating-linear-gradient(
    to right,
    #b91c1c 0,
    #b91c1c 6px,
    transparent 6px,
    transparent 12px
  );
  background-repeat: no-repeat;
  background-position: top;
  background-size: 100% 6px;
}

/* An insignificant bye line: a tie across it is settled by the automatic tiebreaker rather than a
   tiebreaker game, so it differs from a significant one only in color: the same dashes, in orange
   rather than red. */
.byeline-row.byeline-automatic td {
  font-weight: 600;
  color: #c2620c;
  background-image: repeating-linear-gradient(
    to right,
    #d97706 0,
    #d97706 6px,
    transparent 6px,
    transparent 12px
  );
}

/* A higher division's cutline redrawn through this division's top tied block. Distinct violet (vs.
   the division's own red cutline) to signal it belongs to a different, more exclusive division — the
   players above it are still contending upward via a real tiebreaker game. Solid line like a cutline. */
.higher-cutline-row td {
  font-size: 0.875rem;
  padding-top: 0.4rem;
  color: #7c3aed;
  /* Unlike the short CUTLINE / BYE LINE tags the shared .line-row rule shouts, this row carries a full
     sentence — read it as written, in sentence case and at normal tracking. */
  text-transform: none;
  letter-spacing: normal;
  border-top: none;
  background-image: linear-gradient(#7c3aed, #7c3aed);
  background-repeat: no-repeat;
  background-position: top;
  background-size: 100% 6px;
}

/* A qualifier after a line's short uppercase tag ("Cutline (could increase if more players join the
   tournament)"): read as written, in sentence case at normal tracking and weight, so the tag stays the
   thing that shouts. */
.line-row .line-detail {
  text-transform: none;
  letter-spacing: normal;
  font-weight: 500;
}

/* On the real cutline the qualifier is an aside to the tag, not a second tag: a step smaller. */
.cutline-row .line-detail {
  font-size: 0.75rem;
}

/* The row that opens a division in the one-list layout: the division's name, big enough to find while
   scrolling, on a tinted band with its size beside it ("41 spots — half of the 82 players outside A,
   rounded up"). It sits right under the previous division's cutline, so the red bar says where that
   division's finals end and the band says whose rows start. The band is a gradient over an opaque
   Canvas so the row can be pinned over the rows scrolling beneath it in signage mode. */
.division-banner-row td {
  padding: 0.5rem 0.5rem 0.4rem;
  font-size: 1.05rem;
  font-weight: 700;
  text-transform: none;
  letter-spacing: normal;
  background-color: Canvas;
  background-image: linear-gradient(rgba(107, 114, 128, 0.14), rgba(107, 114, 128, 0.14));
  border-bottom: 2px solid var(--border);
}

.division-banner-row .line-detail {
  margin-left: 0.4rem;
  font-size: 0.8rem;
  font-weight: 400;
  color: var(--muted);
}

/* The muted gray is too dim on a dark canvas under the band's tint; lift it like the tab labels. */
@media (prefers-color-scheme: dark) {
  .division-banner-row .line-detail {
    color: #aab2c0;
  }
}

/* A division's rank notices on their own line of its banner (the one-list layout). */
.division-banner-row details.rank-noticed-names {
  margin-top: 0.3rem;
}

/* On a division's tab the notice sits just above the table: keep it off the table's header. */
section.division > details.rank-noticed-names {
  margin-bottom: 0.5rem;
}

/* Where the cutline would fall at the next rung of the finals ladder — a projection, not a boundary
   anyone is held to today, so it is deliberately quieter than the real cutline above it: muted gray, a
   thin dotted rule (vs. the cutline's solid 6px bar and the bye line's bold dashes), and a lighter tag. */
.potential-cutline-row td {
  font-size: 0.8rem;
  font-weight: 600;
  padding-top: 0.35rem;
  color: #6b7280;
  border-top: none;
  background-image: repeating-linear-gradient(
    to right,
    #9ca3af 0,
    #9ca3af 3px,
    transparent 3px,
    transparent 8px
  );
  background-repeat: no-repeat;
  background-position: top;
  background-size: 100% 2px;
}

.potential-cutline-row .line-detail {
  font-weight: 400;
  font-style: italic;
}

/* Scenario icon + expand caret, sitting to the left of a tied player's points. */
.tie-marker {
  display: inline-flex;
  align-items: center;
  gap: 0.15rem;
  margin-right: 0.4rem;
  vertical-align: middle;
}

.tiebreak {
  font-size: 0.85em;
  line-height: 1;
}

/* Scenario-specific colors: green = resolved into seeds, red = a real game is needed, gray = inert tie. */
.tiebreak-seeded {
  color: #15803d;
}

.tiebreak-playoffGame {
  color: #b91c1c;
}

.tiebreak-belowCutline {
  color: #6b7280;
}

/* Rows of a tied group are tappable to reveal the automatic-tiebreaker breakdown below them. */
.tiebreak-row {
  cursor: pointer;
}

.tiebreak-row:hover,
.tiebreak-row.tie-active {
  background: rgba(180, 83, 9, 0.09);
}

.tie-caret {
  display: inline-block;
  color: #9ca3af;
  font-weight: 700;
  transition: transform 0.15s ease;
}

.tiebreak-row[aria-expanded="true"] .tie-caret {
  transform: rotate(90deg);
}

/* The expandable detail panel: one row per tied player with their best-game scores. */
.tie-detail-row td {
  background: rgba(180, 83, 9, 0.06);
  padding: 0.5rem 0.75rem 0.6rem;
  border-bottom: 1px solid var(--border);
}

.tie-detail-heading {
  font-weight: 600;
  color: #b45309;
  font-size: 0.85rem;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
}

/* Scenario label as its own pill next to the heading (avoids an em-dash join), colored per scenario. */
.tie-detail-tag {
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  padding: 0.05rem 0.4rem;
  border-radius: 999px;
  background: rgba(107, 114, 128, 0.15);
  color: #6b7280;
}

.tie-detail-tag-seeded {
  background: rgba(21, 128, 61, 0.15);
  color: #15803d;
}

.tie-detail-tag-playoffGame {
  background: rgba(185, 28, 28, 0.15);
  color: #b91c1c;
}

.tie-detail-tag-belowCutline {
  background: rgba(107, 114, 128, 0.15);
  color: #6b7280;
}

.tie-detail-caption {
  color: var(--muted);
  font-size: 0.82rem;
  line-height: 1.45;
  margin-top: 0.25rem;
}

/* On dark backgrounds the plain --muted gray is hard to read, especially at caption size on a phone;
   lift it to a lighter gray so the explanation stays legible. */
@media (prefers-color-scheme: dark) {
  .tie-detail-caption {
    color: #aab2c0;
  }
}

/* Bump the caption a touch on narrow (mobile) viewports, where it carries most of the explanation. */
@media (max-width: 480px) {
  .tie-detail-caption {
    font-size: 0.88rem;
  }
}

/* Names and scores share two columns across the whole group (subgrid), so every score starts at the same
   x and, right-aligned, the digits line up. */
.tie-detail-list {
  margin-top: 0.5rem;
  display: grid;
  grid-template-columns: minmax(0, max-content) max-content;
  justify-content: start;
  gap: 0.3rem 1rem;
}

.tie-detail-player {
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
  align-items: baseline;
}

.tie-detail-player.withdrawn .tie-detail-name {
  color: var(--withdrawn);
}

.tie-detail-name {
  font-weight: 600;
}

.tie-detail-games {
  font-variant-numeric: tabular-nums;
  font-size: 0.9rem;
  text-align: right;
}

.tie-game:not(:last-child)::after {
  content: ", ";
  color: var(--muted);
}

.tie-game.decider {
  font-weight: 700;
  color: #b45309;
}

.tie-detail-nodata {
  color: var(--muted);
  font-style: italic;
}

.withdrawn-note {
  font-size: 0.75rem;
  color: var(--withdrawn);
  font-style: italic;
}

details.alternates,
details.bubble-watch,
details.ineligible-restricted,
details.rank-noticed {
  margin-top: 0.75rem;
}

details.alternates summary,
details.bubble-watch summary,
details.ineligible-restricted summary,
details.rank-noticed summary {
  cursor: pointer;
  color: #6b7280;
  font-size: 0.9rem;
  font-weight: 500;
}

/* The muted summary opens onto a comma-separated run of names in the same muted style on the same line.
   Each "85 Nick Campbell" keeps together. */
.rank-noticed-names:not([open]) .rank-noticed-list {
  display: none;
}

.rank-noticed-name {
  white-space: nowrap;
}

.rank-noticed-name.favorite {
  font-weight: 700;
  color: var(--favorite);
}

details.bubble-watch table,
details.ineligible-restricted table {
  margin-top: 0.5rem;
  font-size: 0.9rem;
}

th.higher-div,
td.higher-div,
th.restricted-to,
td.restricted-to {
  width: 6rem;
  text-align: center;
}

.error {
  color: #b91c1c;
}

.manual-refresh {
  font: inherit;
  margin-top: 0.5rem;
  padding: 0.3rem 0.8rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: transparent;
  color: inherit;
  cursor: pointer;
}

.manual-refresh[hidden] {
  display: none;
}

/* The time plus the status indicator: kept on one line so the dot never wraps alone. */
.generated-at-time {
  white-space: nowrap;
}

/* Snapshot status indicator: a dot beside the "Published …" timestamp that pulses once per successful
   load, and toggles the poll loop between live and paused when clicked. Sits inline inside #generated-at
   so it reads as part of that line. */
.snapshot-status {
  font: inherit;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  margin-left: 0.45rem;
  /* The time wrapper is nowrap; let the paused label inside the button still wrap on a narrow phone. */
  white-space: normal;
  /* Generous padding for a comfortable tap target on a phone — the dot itself is deliberately small. */
  padding: 0.3rem 0.5rem;
  border: 1px solid transparent;
  border-radius: 999px;
  background: transparent;
  color: inherit;
  cursor: pointer;
  vertical-align: baseline;
}

.snapshot-status:hover,
.snapshot-status:focus-visible {
  border-color: var(--border);
}

.snapshot-dot {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: #16a34a;
}

/* One pulse per successful load: the dot swells briefly and throws off a ring that fades outward. */
.snapshot-status.is-pulsing .snapshot-dot {
  animation: snapshot-pulse 900ms ease-out 1;
}

@keyframes snapshot-pulse {
  0% {
    transform: scale(1);
    box-shadow: 0 0 0 0 rgba(22, 163, 74, 0.55);
  }
  35% {
    transform: scale(1.6);
  }
  100% {
    transform: scale(1);
    box-shadow: 0 0 0 0.55rem rgba(22, 163, 74, 0);
  }
}

/* Paused: amber, static, and spelled out beside the dot — with polling stopped this is the only cue
   that the timestamp above is deliberately standing still. */
.snapshot-status.is-paused {
  color: #b45309;
  border-color: currentColor;
}

.snapshot-status.is-paused .snapshot-dot {
  background: #b45309;
  animation: none;
}

/* The green reads too dark against a dark background; lift both states a step. */
@media (prefers-color-scheme: dark) {
  .snapshot-dot {
    background: #22c55e;
  }

  .snapshot-status.is-paused {
    color: #f59e0b;
  }

  .snapshot-status.is-paused .snapshot-dot {
    background: #f59e0b;
  }
}

/* Keep the per-load feedback without any movement when the viewer asks for reduced motion. */
@media (prefers-reduced-motion: reduce) {
  .snapshot-status.is-pulsing .snapshot-dot {
    animation: snapshot-pulse-reduced 900ms ease-out 1;
  }

  @keyframes snapshot-pulse-reduced {
    0%,
    100% {
      opacity: 1;
    }
    50% {
      opacity: 0.2;
    }
  }
}

.manual-refresh:disabled {
  cursor: not-allowed;
  opacity: 0.5;
}

/* ----------------------------------------------------------------------------------------------
   Signage / kiosk mode (body.signage). Strips the interactive chrome and maximizes information
   density for an unattended big-screen display; the auto-scroll controller in app.js owns scrolling.
   Only these overrides apply — the normal page is unchanged.
   ---------------------------------------------------------------------------------------------- */
body.signage {
  max-width: none; /* use the whole screen rather than the reading-width column */
  margin: 0;
  padding: 0.5rem 1.5rem;
}

/* Hide everything interactive: the Favorites / Milestones / Restrictions / Links / About controls, the
   division tab strip and finalists filter, the "↑ Top" button, the manual-refresh button, and the pause/resume
   status dot (an unattended board must never be pausable at the screen). Signage is a read-only display,
   so none of them are actionable. */
body.signage .header-controls,
body.signage .division-tabs,
body.signage .division-jump,
body.signage .finalists-filter-row,
body.signage .back-to-top-row,
body.signage .manual-refresh,
body.signage .snapshot-status,
body.signage .tournament-tabs {
  display: none;
}

/* Without the tab strip there's nothing for the panel border to attach to, and a board wants every
   pixel for rows — drop the frame and reclaim the padding. */
body.signage .division-panel {
  border: none;
  border-radius: 0;
  padding: 0;
}

/* The About panel goes with the button that opens it: a wall of explanatory copy is for someone holding
   the page, not for an unattended board. (?about=1 alongside ?signage=1 can't sneak it back in.) */
body.signage #about {
  display: none;
}

/* Links become QR codes here (renderLinks builds the tiles). Give the strip room to breathe at the foot
   of the board and let the captions carry at a distance. Only for a standalone signage board: inside the
   signage host's panels the section is suppressed with ?links=0, and the host draws its own QR strip
   below them where it can't scroll away. */
body.signage .links {
  margin-top: 2.5rem;
  padding-top: 1.5rem;
}

body.signage .links-heading {
  font-size: 1.3rem;
  text-align: center;
  margin-bottom: 1rem;
}

/* Signage never surfaces a viewer's favorites (isFavorite() returns false there, so rows aren't tagged
   and the pinned box isn't built). These rules are defense-in-depth against any first-render timing:
   suppress the favorite highlight and hide the favorites box outright. */
body.signage tr.favorite td,
body.signage tr.favorite td:first-child {
  background: revert;
  box-shadow: revert;
}

body.signage .favorites-box {
  display: none;
}

/* Pin the header (tournament name + updated time) to the top while scrolling, like the division
   headings below it, so the tournament is always labeled on an unattended display. A solid background
   and higher z-index hide the rows scrolling underneath and keep it above the division headings.
   updateSignageHeaderOffset() publishes the header's live height as --signage-header-offset, which the
   division headings stick just below. Also tighten the vertical rhythm so more rows fit on screen. */
body.signage header {
  position: sticky;
  top: 0;
  z-index: 10;
  margin-bottom: 0.75rem;
  background: Canvas;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
}

body.signage .division {
  margin-bottom: 1rem;
}

/* Pin each division's heading to the top of the viewport while its rows scroll past. `position:
   sticky` is constrained to the division's own <section>, so once the bottom of (say) Division A's
   section reaches the top, A's heading is pushed off and Division B's heading takes over — the current
   division is always labeled. A solid background hides the rows scrolling underneath it. */
body.signage .division h2 {
  font-size: 1.1rem;
  padding-bottom: 0.15rem;
  position: sticky;
  top: var(--signage-header-offset, 0);
  z-index: 5;
  margin-top: 0;
  background: Canvas;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
}

/* The one-list layout has a single table, so the division headings above can't take turns at the top.
   Its banner rows do instead: each is pinned just under the header, and the next division's banner slides
   over it (it comes later, so it paints on top), keeping the current division labeled the same way. */
body.signage tr.division-banner-row td {
  position: sticky;
  top: var(--signage-header-offset, 0);
  z-index: 5;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
}

body.signage th,
body.signage td {
  padding: 0.2rem 0.5rem;
}

/* A player's name linked to their page on the scoring system: looks exactly like the unlinked name. */
.player-link {
  color: inherit;
  text-decoration: none;
}
