/* ------------------------------------------------------------------------------------------------
   Merged-queues board.

   Self-contained on purpose. The browser tool this page was ported from pulled in the whole W3.CSS
   framework (~25KB) to use perhaps a dozen of its classes, and drove layout through its 12-column grid
   helpers (w3-col s3 m3 l3) written into cloned template elements. Everything here is the handful of
   rules the card view actually needs, using CSS grid directly.

   Themed with tokens rather than fixed colors so the page follows the viewer's light/dark preference —
   a phone in a dark room at a late-night tournament shouldn't get a white flash.
   ------------------------------------------------------------------------------------------------ */

:root {
  color-scheme: light dark;
  --border: #d0d0d8;
  --muted: #6b7280;
  --warning: #b45309;
  --card-border: #93c5fd;
  /* The juniors card is not a machine, so it does not wear the machine cards' blue. Gold shares a hue
     with the first queue's row tint, which is a real risk on a board where that queue is SEFF — but
     the two never do the same job: a queue tint is a low-alpha wash lying under a row's text, and this
     is a solid 2px line drawn around a card. Deep enough that it reads as structure rather than as a
     row that has somehow escaped its card. */
  --juniors-border: #b8860b;
  --on-game: #059669;
  /* The room-wide banner is deliberately outside the row palette: solid and dark where every queue tint
     is a pale wash, so "this applies to everyone" can't be misread as "this belongs to queue 0". It
     carries its own contrast, which is why the same two values serve both colour schemes. */
  --announce-bg: #1e293b;
  --announce-text: #f8fafc;
  /* A banner that belongs to particular queues sits on this neutral ground unless it belongs to exactly
     one, in which case that queue's own wash replaces it. Grey-on-grey alpha, so it lands correctly on
     a white page and a black one alike. */
  --announce-queue-bg: rgba(127, 127, 127, 0.12);

  /* Row tints keyed to a tournament's colorIndex, so two merged queues are told apart at a glance.
     Low-alpha so they read as a wash rather than a block of color behind the text. */
  --queue-0: rgba(245, 158, 11, 0.18);
  --queue-1: rgba(139, 92, 246, 0.16);
  --queue-2: rgba(16, 185, 129, 0.16);
  --queue-3: rgba(244, 114, 182, 0.16);
}

@media (prefers-color-scheme: dark) {
  :root {
    --border: #3f3f46;
    --warning: #fbbf24;
    --card-border: #1e40af;
    /* Same register as the blue it sits beside: dark and saturated, not the bright amber the row tints
       take here. A gold as light as those washes would stop reading as an edge against a near-black
       page and start reading as another queue colour. */
    --juniors-border: #a16207;
    --on-game: #34d399;
    /* Blue, and saturated. The slate this used to be sat at the same lightness and the same low
       saturation as the violet queue tint, which is the one thing the band must not resemble — every
       row tint here is a pale wash, so a muted ground is a ground that can be mistaken for one. This is
       the blue the cards already draw their borders in: the page's structural colour, which no
       tournament can ever be assigned. */
    --announce-bg: #1e40af;

    /* The same tints, restated brighter. A wash that reads clearly over a white page all but vanishes
       over a near-black one, so dark mode takes the lighter shade of each hue at close to twice the
       alpha — from across the room the tint has to be as easy to tell apart as the label it stands for. */
    --queue-0: rgba(251, 191, 36, 0.30);
    --queue-1: rgba(167, 139, 250, 0.30);
    --queue-2: rgba(52, 211, 153, 0.28);
    --queue-3: rgba(244, 114, 182, 0.30);
  }
}

/* This board's usual home is a TV on the wall, read from across the room rather than from a phone in
   someone's hand, so everything scales with the viewport off one knob: every size below is in rem, and
   this is the rem. Clamped at both ends — a phone at the machine still gets ordinary 16px text, and a
   very wide panel stops growing before a card holds three words a line. */
html {
  font-size: clamp(16px, 1.15vw, 30px);
  /* The board sizes itself to the window (see fitBoard), so whether the page scrolls is something the
     cards decide — and a scrollbar that comes and goes would change the width they were measured
     against, which is a loop waiting to happen. The gutter is held open either way. */
  scrollbar-gutter: stable;
}

/* Near-full-bleed on purpose: on the TV the wasted margin is wasted legibility, and the padding is
   there so the outermost cards don't sit flush against the bezel. */
body {
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
  margin: 1.25rem 0;
  padding: 0 1.25rem;
  line-height: 1.4;
}

h1 {
  margin-bottom: 0.25rem;
}

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

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

/* The "All boards" link beside the title: small and muted, and invisible until app.js sees the mouse
   move. Hidden with visibility rather than display so it keeps its space either way: the header's height
   is one of the numbers fitBoard spends the screen with, and a link that changed the layout as it came
   and went could wrap the header line and leave the board fitted to a screen it no longer has. */
/* Wraps rather than squeezing: on a phone too narrow for both, the link drops to a line of its own under
   the title instead of folding the title onto two lines. */
.title-group {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 0.75rem;
}

.all-boards-link {
  color: var(--muted);
  font-size: 0.8rem;
  text-decoration: none;
  white-space: nowrap;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s, visibility 0.3s;
}

.all-boards-link.shown {
  opacity: 1;
  visibility: visible;
}

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

/* --------------------------------------------------------------------------------------------
   The directors' announcement banners, above everything else on the page.
   -------------------------------------------------------------------------------------------- */

.announcements {
  display: grid;
  gap: 0.4rem;
  margin-bottom: 1rem;
}

/* The room-wide banner: every tournament on the board is carrying this message, so it's the one thing
   on the page that speaks for the whole room, and it's set as a solid band to say so. Larger than the
   board's own text. `pre-line` because a director types these as they'd write them — Match Play keeps
   the line breaks, and so should the wall. */
.announcement {
  margin: 0;
  padding: 0.5rem 0.9rem;
  border: 0;
  border-radius: 0.3rem;
  background: var(--announce-bg);
  color: var(--announce-text);
  font-size: 1.15rem;
  font-weight: 600;
  white-space: pre-line;
}

/* A banner only some of the queues are carrying, which is a narrower claim: a bordered wash rather than
   a band. The two never share the page — a tournament carries one message, so a message every
   tournament is carrying is the only message there is — which means the contrast has to survive being
   seen one at a time, and a band against a wash does. */
.announcement-queue {
  border: 1px solid var(--border);
  background: var(--announce-queue-bg);
  color: inherit;
}

/* Where it belongs to exactly one queue it wears that queue's own wash, so the banner points at its
   rows the same way the legend chip does. */
.announcement-queue[data-queue="0"] { background: var(--queue-0); }
.announcement-queue[data-queue="1"] { background: var(--queue-1); }
.announcement-queue[data-queue="2"] { background: var(--queue-2); }
.announcement-queue[data-queue="3"] { background: var(--queue-3); }

/* "Announcement:", or the names of the queues this one belongs to. Not dimmed on the solid band, where
   the contrast is already spent on the text. */
.announcement-source {
  margin-right: 0.35rem;
  font-weight: 700;
}

.announcement-queue .announcement-source {
  opacity: 0.75;
}

/* --------------------------------------------------------------------------------------------
   Header controls
   -------------------------------------------------------------------------------------------- */

.pill {
  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;
}

/* The "updated at" line's status dot, which also pauses/resumes polling. Its pulse on each successful
   load is the only moving thing on the page, so a viewer can tell a live board from a frozen one. */
.snapshot-status {
  font: inherit;
  font-size: 0.9rem;
  color: var(--muted);
  background: transparent;
  border: 0;
  padding: 0 0 0 0.5rem;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
}

.snapshot-dot {
  width: 0.55rem;
  height: 0.55rem;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.35;
}

.snapshot-status.pulse .snapshot-dot {
  animation: snapshot-pulse 0.9s ease-out;
}

@keyframes snapshot-pulse {
  0% { opacity: 1; transform: scale(1.6); }
  100% { opacity: 0.35; transform: scale(1); }
}

/* A paused board is a stale board, so say so rather than only dimming the dot. */
.snapshot-status[aria-pressed="true"] .snapshot-dot {
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .snapshot-status.pulse .snapshot-dot { animation: none; }
}

/* The title, the tint key and the "Updated …" line, all on one row. On a wall-mounted TV every row of
   chrome above the cards costs a row of queue, and these three are each short enough that stacking them
   was spending three lines on one line's worth of words. Wrapping, so a narrow screen drops the key and
   the timestamp below the title rather than squeezing all three.

   Baseline-aligned rather than centred: the title is several times the size of the text beside it, and
   what makes them read as one line is their words sitting on the same rule. */
.header-line {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.4rem 1.25rem;
  margin-top: 0.5rem;
}

/* The margins that separated these when they were stacked would now push the row around. */
.header-line h1,
.header-line .time-key,
.header-line #generated-at {
  margin: 0;
}

/* A touch larger than the other muted text: it explains what the numbers mean. */
.header-line .time-key {
  font-size: 1rem;
}

/* The tint key: a chip per tournament carrying the same wash its rows do, so the color is legible as
   a label rather than as decoration. Filled with the row tint itself rather than a swatch beside the
   name — the chip is then literally a sample of the row it stands for, down to the squared-off corners
   the rows have. */
.legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.legend-item {
  padding: 0.15rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: 0.2rem;
  font-weight: 600;
}

.legend-item[data-queue="0"] { background: var(--queue-0); }
.legend-item[data-queue="1"] { background: var(--queue-1); }
.legend-item[data-queue="2"] { background: var(--queue-2); }
.legend-item[data-queue="3"] { background: var(--queue-3); }

/* --------------------------------------------------------------------------------------------
   A card per machine holding its whole queue.
   -------------------------------------------------------------------------------------------- */

.queue-cards {
  margin-top: 1.5rem;
  display: grid;
  /* min(…, 100%) so the wide card the TV wants can't push a phone into sideways scrolling: on a narrow
     screen the track collapses to the container instead of overflowing it. */
  grid-template-columns: repeat(auto-fill, minmax(min(26rem, 100%), 1fr));
  gap: 0.75rem;
}

/* The board packed four across instead of three: a card gives up a third of its width, and the names
   in it, to buy the row of height that a board with more queue than screen is short of. The fitter
   sets and clears this (see fitBoard) — it is a trade made only when it pays, not a preference. */
.queue-cards.compact {
  grid-template-columns: repeat(auto-fill, minmax(min(20rem, 100%), 1fr));
}

.queue-card {
  border: 2px solid var(--card-border);
  border-radius: 8px;
  padding: 0.6rem 0.75rem 0.5rem;
}

.queue-card h2 {
  font-size: 1.3rem;
  margin: 0 0 0.35rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* minmax(0, …) on the player column so it, rather than the fixed columns, absorbs a long name — a grid
   track's default min-width is its content, which would otherwise push "Waited" off the card. */
.queue-head,
.queue-row {
  display: grid;
  grid-template-columns: 1.5rem minmax(0, 1fr) 3.5rem 6rem;
  gap: 0.35rem;
  padding: 0.3rem 0.15rem;
  align-items: baseline;
}

.queue-head {
  font-weight: 600;
  font-size: 0.9rem;
  border-bottom: 1px solid var(--border);
}

.queue-head > *,
.queue-row > * {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A phone hasn't the room the TV has, so under the width where a card holds all four columns
   comfortably the fixed ones give ground to the name — the only column whose content nobody can guess
   from context. The heading wraps onto two lines there rather than clipping to "Wait + Play…". */
@media (max-width: 34rem) {
  .queue-head,
  .queue-row {
    grid-template-columns: 1.5rem minmax(0, 1fr) 3.5rem 5.5rem;
  }

  .queue-head > * {
    white-space: normal;
  }
}

.queue-row + .queue-row {
  border-top: 1px solid var(--border);
}

.queue-row .position,
.queue-head .position {
  text-align: center;
  color: var(--muted);
}

.queue-row .waited,
.queue-head .waited {
  text-align: right;
}

/* The tint that says which tournament a queued player is entered in — the Queue column says it in
   words, this says it at a glance while scanning down a card. */
.queue-row[data-queue="0"] { background: var(--queue-0); }
.queue-row[data-queue="1"] { background: var(--queue-1); }
.queue-row[data-queue="2"] { background: var(--queue-2); }
.queue-row[data-queue="3"] { background: var(--queue-3); }

/* The player actually on the machine, which on a merged board is only ever the top row: every queue's
   head carries Match Play's on-the-game stamp, so a pooled row holds several, and only the longest-
   waiting of them is at the machine. The single most-asked question at a game is "is that person playing
   or waiting?", so the row is marked rather than left to be inferred from position. The ▶ replaces a
   queue position, which means nothing for a turn already in progress, and the time column counts up
   their time on the game rather than a wait that stopped mattering when they started. */
.queue-row.on-game {
  font-weight: 600;
}

.queue-row.on-game .position {
  color: var(--on-game);
  font-weight: 400;
}

/* A row reporting the machine's state has no player, queue or wait to line up with, so its message
   takes the whole width instead of being clipped to fit the Player column. */
.queue-row.state-row {
  grid-template-columns: 1.75rem minmax(0, 1fr);
}

/* "Queue is empty" / "Blocked for repair" / "Queue is closed" — a state, not a player. */
.state {
  color: var(--muted);
  font-style: italic;
}

.state.blocked {
  color: var(--warning);
  font-style: normal;
}

/* The row standing in for everyone past the card's row budget. Allowed to wrap where the player rows
   clip, because a clipped list of names loses the "and 4 more" that makes the line mean anything. */
.overflow-row .overflow {
  color: var(--muted);
  font-size: 0.9rem;
  white-space: normal;
}

.overflow-row .position {
  font-size: 0.9rem;
}

/* Machines nobody can play sit behind the ones players can act on, rather than being hidden — a
   blocked machine with people still queued on it is exactly what someone needs to see. */
.queue-card.out-of-play {
  opacity: 0.7;
}

/* --------------------------------------------------------------------------------------------
   The juniors tile: a standings card among the machine cards.

   Deliberately built out of the same parts as a machine card — same border, same heading, same row
   grid — because it IS one of the cards, not a panel that happens to be next to them. What it changes
   is only what its columns hold: a points figure where a machine card counts minutes, and no time
   column at all, so the fixed track is narrower and the name gets the difference.
   -------------------------------------------------------------------------------------------- */

/* The one heading on the board that is a phrase rather than a name. A machine card clips its title to
   one line because "Teenage Mutant Ninja Turtles (Pro)" is recognisable from its first half and every
   card is the same height; a title someone wrote for this card is not, and "Junior Pinball Olympiad
   Stand..." would be the board failing to say the one thing it was told to say. So it wraps. */
.juniors-card {
  border-color: var(--juniors-border);
}

.juniors-card h2 {
  white-space: normal;
  overflow: visible;
}

.juniors-row {
  grid-template-columns: 1.5rem minmax(0, 1fr) 5rem 4.5rem;
}

@media (max-width: 34rem) {
  .juniors-row {
    grid-template-columns: 1.5rem minmax(0, 1fr) 4rem 4rem;
  }
}

/* Right-aligned like every other column of figures on the page, so the digits line up down the card
   rather than each row's number starting wherever its name left off. */
.queue-row .points,
.queue-head .points {
  text-align: right;
}

/* The card saying, once at its foot, that some of these points were recomputed against another
   tournament's scoring. It carries that on its own: the rows are deliberately unmarked, because the
   whole purpose of the recomputation is that every number in the column now means the same thing.
   Sized and coloured like the overflow line — both are the card talking about itself. */
.juniors-note {
  margin: 0.45rem 0 0;
  color: var(--muted);
  font-size: 0.85rem;
  line-height: 1.3;
}

/* A juniors row that has games to show. Not styled as a link or a button — every row on this board
   looks the same and that consistency is worth more than an affordance nobody on a TV can use — but it
   answers the pointer and the keyboard, which is what tells a phone reader it does something. */
.juniors-row-openable {
  cursor: pointer;
}

.juniors-row-openable:hover,
.juniors-row-openable:focus-visible {
  outline: 2px solid var(--juniors-border);
  outline-offset: -2px;
}

/* --------------------------------------------------------------------------------------------
   The games behind one junior's total.

   A modal <dialog>, so it floats over the board instead of reflowing it, and so Escape, focus
   trapping and the backdrop all come from the browser rather than from this file. Sized in rem like
   everything else here, which means it scales with the same one knob the board does: readable on a
   phone in someone's hand and still readable projected on a wall.
   -------------------------------------------------------------------------------------------- */

.juniors-detail {
  width: min(34rem, calc(100vw - 2rem));
  max-height: calc(100vh - 4rem);
  overflow-y: auto;
  padding: 1rem 1.15rem 1.15rem;
  /* The card's own edge, carried onto the panel it opens, so the two read as one thing. */
  border: 2px solid var(--juniors-border);
  border-radius: 8px;
  /* Canvas/CanvasText rather than fixed colors: the dialog is the one element here the page doesn't
     paint a background behind, so it has to carry the viewer's own light/dark ground itself. */
  background: Canvas;
  color: CanvasText;
  font-size: 1rem;
}

.juniors-detail::backdrop {
  background: rgba(0, 0, 0, 0.55);
}

.juniors-detail-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 0.6rem;
}

.juniors-detail-head h2 {
  margin: 0;
  font-size: 1.3rem;
}

.juniors-detail-head p {
  margin: 0.15rem 0 0;
}

.juniors-detail-head .pill {
  margin-top: 0;
  flex: none;
}

/* Machine name takes the slack; the three figure columns are fixed so they line up down the panel.
   minmax(0, …) on the name for the same reason the queue rows use it — a grid track's default minimum
   is its content, which would otherwise push the points off the edge. */
.juniors-detail-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 7rem 6rem;
  gap: 0.4rem;
  padding: 0.35rem 0.15rem;
  align-items: baseline;
}

.juniors-detail-row.with-source {
  grid-template-columns: minmax(0, 1fr) 7rem 6rem 6rem;
}

.juniors-detail-row + .juniors-detail-row {
  border-top: 1px solid var(--border);
}

.juniors-detail-head-row {
  font-weight: 600;
  font-size: 0.9rem;
  border-bottom: 1px solid var(--border);
}

.juniors-detail-row .score,
.juniors-detail-row .points {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.juniors-detail-row .machine {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A game that couldn't be converted, so it added nothing to the total. Dimmed rather than hidden —
   it is the row that explains why the total looks short. */
.juniors-detail-row.uncounted {
  color: var(--muted);
}

.juniors-detail .juniors-note {
  margin-top: 0.7rem;
}

/* A phone has no room for four columns of figures beside a machine name, so the fixed tracks give
   ground first — the same trade the queue cards make at the same width. */
@media (max-width: 34rem) {
  .juniors-detail-row {
    grid-template-columns: minmax(0, 1fr) 5.5rem 4.5rem;
  }

  .juniors-detail-row.with-source {
    grid-template-columns: minmax(0, 1fr) 5rem 4rem 4rem;
  }

  .juniors-detail-head-row {
    font-size: 0.8rem;
  }

  .juniors-detail-head-row > * {
    white-space: normal;
  }
}

[hidden] {
  display: none !important;
}
