/* =====================================================================
   The live board — mobile first.
   A court card is drawn as a court seen from above: a net down the
   middle, kitchen bands either side, each team's score on its own half.
   ===================================================================== */

.board { max-width: 620px; margin: 0 auto; min-height: 100dvh; display: flex; flex-direction: column; }
.board-scroll { flex: 1; padding: 16px 16px 108px; }

.board-split { display: block; }
.board-aside { display: none; }

.board-head {
  position: sticky; top: 0; z-index: 30;
  background: var(--brand); color: var(--on-brand);
  padding: 12px 16px 13px;
}
.board-head .top { display: flex; align-items: flex-start; gap: 10px; }
.board-head h1 { font-size: 19px; color: var(--on-brand); flex: 1; min-width: 0; overflow-wrap: anywhere; }
.board-sub { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 4px; font-size: 12.5px; opacity: .8; }
.code-chip {
  font-family: var(--mono); font-size: 13.5px; letter-spacing: .18em; font-weight: 500;
  background: rgba(255,255,255,.16); color: var(--on-brand);
  padding: 3px 9px; border-radius: var(--r-sm);
  border: 1px solid rgba(255,255,255,.18);
  cursor: pointer;
}
.code-chip:hover, .code-chip:focus-visible { background: rgba(255,255,255,.28); }

/* A court with players on it but no clock yet. */
.chip-ready {
  font-family: var(--body); font-size: 10px; font-weight: 600; letter-spacing: .1em;
  background: rgba(255,255,255,.18); padding: 2px 7px; border-radius: var(--r-full);
}

/* The Courts tab repeats "next on" for phones; desktop has the side panel. */
/* 768, not 1024: from there the side panel carries this (see "tablets"
   below), and the copy in the page would say it twice. */
@media (min-width: 768px) { .mobile-only { display: none; } }
.iconbtn {
  width: 40px; height: 40px; flex: none; display: grid; place-items: center;
  border-radius: var(--r-full); color: var(--on-brand); background: rgba(255,255,255,.10);
}
.iconbtn:active { background: rgba(255,255,255,.22); }
.iconbtn-plain { background: transparent; color: var(--text-muted); }

/* ---- bottom tabs (thumb reach) -------------------------------------- */
.tabbar {
  position: fixed; bottom: 0; left: 0; right: 0; z-index: 20;
  max-width: 620px; margin: 0 auto;
  display: flex;
  background: var(--surface); border-top: 1px solid var(--border);
  padding-bottom: env(safe-area-inset-bottom, 0);
}
.tabbar button {
  flex: 1 1 0; min-width: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
  min-height: 58px; padding: 8px 4px;
  font-size: 10.5px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
  color: var(--text-faint); border-top: 2.5px solid transparent; margin-top: -1px;
}
.tabbar button[aria-current="page"] { color: var(--accent); border-top-color: var(--accent); }
.tab-ico { position: relative; display: grid; place-items: center; }
.tab-badge {
  position: absolute; transform: translate(15px, -11px);
  background: var(--live); color: var(--on-live);
  font-family: var(--mono); font-size: 9.5px; font-weight: 500;
  min-width: 16px; height: 16px; border-radius: var(--r-full);
  display: grid; place-items: center; padding: 0 4px;
}

/* ---- court card ------------------------------------------------------ */
.court { margin-bottom: 14px; overflow: hidden; }
/* wrap, not nowrap. The head carries an icon, the court's name, a status
   chip, the clock, the save state and the count, and on a 390px phone those
   come to about 425px. Held on one line they could not shrink past their own
   text -- a flex item's automatic minimum is its content -- so the head
   pushed the card 24px wider than the screen and the whole board scrolled
   sideways, sticky header and all. It appeared the day the save state
   ("Score saved") joined the line and took the head past what fits. */
.court-head {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: 4px 10px;
  background: var(--brand); color: var(--on-brand); padding: 9px 14px;
}
.court-no { font-family: var(--display); font-weight: 600; font-size: 15px; letter-spacing: .02em; }
/* min-width:0 so a long clock-plus-status can ellipsise inside the line it
   wraps onto instead of setting a floor for the whole head. */
.court-meta {
  display: flex; align-items: center; gap: 7px; min-width: 0;
  font-family: var(--mono); font-size: 11.5px; opacity: .88;
}
.court-head-closed { background: var(--surface-alt); color: var(--text-faint); }

.court-body { background: var(--court-surface); padding: 14px 12px 12px; position: relative; }

/* A wide gutter down the middle: without it the "+" of one side sits hard
   against the "-" of the other and gets mis-tapped across the net. */
.sides {
  display: grid; grid-template-columns: 1fr 1fr; gap: 18px;
  position: relative; z-index: 1; padding: 6px 0 10px;
}
/* The net, and the kitchen either side of it, drawn on the play area
   rather than on the card. They used to be drawn on .court-body with 58px
   reserved at the bottom for whatever button happened to be there, so
   every change below the court moved the net. */
/* Sidelines and baselines, so the play area reads as a court rather than
   as a green panel with a line down the middle. Drawn at low opacity:
   this is the surface a score sits on, not a diagram to study. */
.playing .sides {
  border: 1px solid var(--court-line);
  border-radius: var(--r-sm);
  padding: var(--s-2) var(--s-2) var(--s-3);
}
.playing .sides::before {
  content: ""; position: absolute; inset: 0 auto 0 50%; z-index: 0;
  border-left: 2px dashed var(--court-line); transform: translateX(-1px);
}
.playing .sides::after {
  content: ""; position: absolute; top: 0; bottom: 0; z-index: 0;
  left: calc(50% - 44px); width: 88px;
  border-left: 1px solid var(--court-line); border-right: 1px solid var(--court-line);
  opacity: .6; pointer-events: none;
}
.side > * { position: relative; z-index: 1; }
.side { display: flex; flex-direction: column; align-items: center; gap: 10px; text-align: center; }
.side-names { min-height: 44px; display: flex; flex-direction: column; justify-content: center; gap: 2px; }
.pname { font-size: 15px; font-weight: 600; line-height: 1.2; overflow-wrap: break-word; color: var(--text); }
/* --text-muted, not --text-faint: the faint ink is measured against white
   and this pill sits on the pale green of a court, where it came out at
   2.5:1. A rating is read at a glance from a metre away. 4.96:1. */
.pname .lvl { font-family: var(--mono); font-size: 10.5px; color: var(--text-muted); margin-left: 4px; }

/* Replace-this-player.
   Quiet, but a button. As a --text-faint glyph on no background it read as
   a smudge beside the name rather than as something you could press, and
   people reported that replacing a player was simply not available. It has
   an edge and a face now: still the smallest thing on the court, still not
   competing with the score, but unmistakably a control. */
.swapbtn {
  display: inline-grid; place-items: center;
  width: 22px; height: 22px; margin-left: 5px; vertical-align: middle;
  border-radius: var(--r-full);
  color: var(--text-muted);
  background: var(--surface);
  border: 1px solid var(--border-strong);
}
.swapbtn:hover, .swapbtn:focus-visible {
  color: var(--accent); background: var(--accent-soft); border-color: var(--accent);
}

/* Two rows, not one: the score spans the top and the two steppers share
   the row beneath. One row meant the number and both buttons competed for
   half a card's width, and the loser was clipped by overflow:hidden. */
/* Two fixed columns rather than two fractions: the steppers are round
   buttons of a known size, and a fraction stretched them into slabs wide
   enough to read as empty input fields. The score spans both and is free
   to be wider than the pair beneath it. */
.scorer {
  display: grid;
  grid-template-columns: 48px 48px;
  grid-template-areas: "score score" "minus plus";
  gap: 12px 26px;
  justify-content: center; justify-items: center; align-items: center;
}
.score {
  grid-area: score;
  font-family: var(--dot); font-weight: 900; line-height: 1;
  /* Fluid, so a narrow phone and a courtside tablet both get the largest
     number that fits rather than one fixed size that suits neither. */
  font-size: clamp(52px, 11vw, 76px);
  /* min-width:0 keeps a wide number from widening the stepper tracks
     underneath it; the digits simply centre over the pair. */
  min-width: 0; width: 100%; text-align: center; font-variant-numeric: tabular-nums;
  letter-spacing: .02em; color: var(--text);
}
/* A court that has not started yet.
   ---------------------------------------------------------------------
   Doto draws zero as an open diamond of dots. At 88px, four of those
   across three untouched courts is the first thing the eye lands on, and
   it reads as an ornament rather than as a score — the board looked
   broken before anyone had played a point. Real scores are perfectly
   legible in this face (11, 7, 10 all read at a glance), so the problem
   was never the typeface: it was that "nothing has happened yet" was
   being shouted in the largest type on the page.

   Muted, it reads as what it is — a court waiting to start — and the
   running courts beside it keep the attention instead. */
.score-idle { opacity: .44; }
input.score-idle:hover,
input.score-idle:focus { opacity: 1; }

.scorer > .stepper:first-child { grid-area: minus; }
.scorer > .stepper:last-child  { grid-area: plus; }
/* The score is an input when you may change it: tap the steppers, or type
   straight over it when a score has drifted several points. */
input.score {
  width: 100%; padding: 2px 0;
  background: transparent; border: 1.5px solid transparent; border-radius: var(--r-sm);
  -moz-appearance: textfield;
}
input.score:hover { border-color: var(--border); }
input.score:focus {
  outline: none;
  background: var(--surface);
  border-color: var(--accent);
}
input.score::-webkit-outer-spin-button,
input.score::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.stepper {
  width: 48px; height: 48px; flex: none; border-radius: var(--r-full);
  display: grid; place-items: center; font-size: 21px; font-weight: 700; line-height: 1;
  background: var(--surface); color: var(--text); border: 1.5px solid var(--border-strong);
}
.stepper:active { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }
.stepper:disabled { opacity: .4; }

.court-actions {
  margin-top: var(--s-3); display: flex; gap: 8px;
  position: relative; z-index: 1;
  max-width: 340px; margin-inline: auto;
}
.court-note { margin-top: 8px; text-align: center; font-size: 12.5px; color: var(--text-faint); position: relative; z-index: 1; }
/* Centred, not stacked at the top: the card is deliberately as tall as
   its neighbour (see .courtgrid), and content pinned to the top of that
   height is what made an open court look like a panel that had failed to
   load rather than a court waiting for players. */
.court-empty {
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: var(--s-2); padding: var(--s-5) var(--s-3); text-align: center;
}

/* ---- queue ----------------------------------------------------------- */
.qrow { display: flex; align-items: center; gap: 11px; padding: 11px 14px; border-bottom: 1px solid var(--border); }
.qrow:last-child { border-bottom: none; }
.qrow.up { background: var(--accent-soft); }
.qpos { font-family: var(--mono); font-size: 13px; color: var(--text-faint); width: 22px; text-align: right; flex: none; }
.qrow.up .qpos { color: var(--accent); font-weight: 500; }
.qname { flex: 1; min-width: 0; font-weight: 500; overflow-wrap: anywhere; }
.qmeta { font-family: var(--mono); font-size: 11.5px; color: var(--text-faint); flex: none; }

/* ---- how far off being ranked -----------------------------------------
   A bar for where they are and words for what is left. The bar is the old
   "2 of 3" in a shape you read without counting; the words are the thing
   the player actually asked, which no number on this row answered before. */
.qgap { flex: none; display: inline-flex; align-items: center; gap: 8px; }
.qgap-bar {
  width: 46px; height: 5px; flex: none;
  border-radius: var(--r-full);
  background: var(--border);
  overflow: hidden;
}
.qgap-fill { display: block; height: 100%; background: var(--text-faint); border-radius: inherit; }
.qgap-txt {
  font-size: 11.5px;
  color: var(--text-faint);
  white-space: nowrap;
}
/* One away. Worth marking: that player goes and finds a fourth. */
.qgap-close .qgap-fill { background: var(--accent); }
.qgap-close .qgap-txt  { color: var(--accent); font-weight: 600; }

/* A phone row holds a name, a bar, a phrase and a W-L line. Something has
   to go, and it is the phrase -- the bar still shows the distance, and the
   title on the group still spells it out on a long press. */
@media (max-width: 420px) {
  .qgap-txt { font-size: 11px; }
  .qgap-bar { width: 32px; }
}

/* ---- the next game ----------------------------------------------------- */
/* The one panel on the board about a game that has not happened yet, which
   makes it the only place an organiser can still change the outcome. Every
   colour was measured against --brand: lime 7.83:1, --on-brand 10.38:1, the
   warm orange 5.13:1. */
.nextup {
  background: var(--brand); color: var(--on-brand);
  border-radius: var(--r-lg); padding: 14px 16px 13px;
  box-shadow: var(--shadow);
}
/* Light on the navy, as the side panel's copy of this card has it. Three
   classes deep on purpose: refresh.css loads last and sets every eyebrow to
   --text-muted at two, which beat the two-class ".nextup .eyebrow" that
   used to be here -- and left "NEXT GAME" dark grey on navy at ~1.4:1,
   unreadable, on every phone and every spectator's screen. */
.nextup .nextup-top > .eyebrow { color: var(--on-brand); opacity: .75; }
.nextup-top { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }

/* The reading sits level with the heading rather than under the names, so
   the eye takes the verdict and the players in one pass. */
.nextup-bal {
  flex: none;
  font-family: var(--mono); font-size: 11px; font-weight: 500;
  letter-spacing: .02em; white-space: nowrap;
}
.nextup-bal-even, .nextup-bal-close { color: var(--live); }
.nextup-bal-tilted   { color: var(--on-brand); opacity: .72; }
.nextup-bal-lopsided { color: var(--action-on-dark); }

.nextup-teams {
  display: grid; grid-template-columns: 1fr auto 1fr;
  align-items: start; gap: 10px; margin-top: 10px;
  font-weight: 600; font-size: 15px; line-height: 1.3;
}
/* .75, not .55: at .55 "vs" measured 3.8:1 on the navy. */
.nextup-teams .vs {
  font-family: var(--mono); font-size: 10.5px; opacity: .75;
  align-self: center; text-transform: uppercase; letter-spacing: .08em;
}
.nextup-teams .namebtn { color: var(--on-brand); border-bottom-color: rgba(234,240,236,.35); }
.nextup-teams .namebtn:hover { color: var(--live); border-bottom-color: var(--live); }
.nextup-teams .namebtn:focus-visible { outline-color: var(--live); }

/* The side's combined rating: the working behind the percentage. */
.nextup-rate {
  display: block; margin-top: 3px;
  font-family: var(--mono); font-size: 11px; font-weight: 400;
  /* .75, not .6: a rating is read, and at .6 it measured 4.3:1. */
  color: var(--on-brand); opacity: .75;
}

/* Whoever is holding the court, in a challenge. Not a button — they are not
   in the plan, so there is nothing here to swap. */
.nextup-hold { font-weight: 600; opacity: .82; }

/* Grows from the middle, so which way it leans reads before any number. */
.nextup-meter {
  margin-top: 11px; height: 4px; border-radius: var(--r-full);
  background: rgba(234,240,236,.18); overflow: hidden;
}
.nextup-meter i {
  display: block; height: 100%; border-radius: inherit;
  background: currentColor; transition: width .3s ease;
}

/* ---- share sheet ----------------------------------------------------- */
/* .scrim, .scrim-center, .confirm-box and .confirm-actions now live in
   app.css, which BOTH layouts load. They were here while the board was the
   only screen that asked before doing something; the dashboard asks too
   now, and a second copy of a dialog is a dialog that drifts. .sheet below
   still sits inside .scrim -- the pair is split across two files, which is
   the price of sharing it. */
.sheet {
  width: 100%; max-width: 620px; max-height: 92dvh; overflow-y: auto;
  background: var(--bg); border-radius: var(--r-xl) var(--r-xl) 0 0;
  padding: 8px 16px calc(24px + env(safe-area-inset-bottom, 0));
  box-shadow: var(--shadow-lift);
  animation: rise .22s cubic-bezier(.2,.8,.3,1);
}
@keyframes rise { from { transform: translateY(20px); } to { transform: translateY(0); } }
.grabber { width: 38px; height: 4px; border-radius: var(--r-full); background: var(--border-strong); margin: 6px auto 14px; }

/* QR must stay dark-on-white whatever the theme, or scanners struggle. */
.qrcard { background: #fff; border-radius: var(--r-lg); padding: 16px; display: grid; place-items: center; }
.qrcard svg { width: 100%; height: auto; max-width: 250px; display: block; shape-rendering: crispEdges; }
.bigcode {
  font-family: var(--mono); font-weight: 500; font-size: 30px; letter-spacing: .26em;
  text-align: center; padding-left: .26em;
}

/* ---- toast ----------------------------------------------------------- */
.toast {
  position: fixed; left: 50%; bottom: 84px; transform: translateX(-50%); z-index: 60;
  max-width: min(440px, calc(100vw - 32px)); width: max-content;
  background: var(--brand-deep); color: var(--on-brand);
  padding: 11px 17px; border-radius: var(--r-full);
  font-size: 13.5px; font-weight: 600; text-align: center;
  box-shadow: var(--shadow-lift); animation: rise .18s ease;
}
.toast-warn { background: var(--win); color: #fff; }

/* ---- breadcrumb back to the dashboard ------------------------------- */
.crumb {
  display: inline-flex; align-items: center; gap: 5px;
  color: var(--on-brand); opacity: .82;
  font-size: 12.5px; font-weight: 700; letter-spacing: .01em;
  text-decoration: none; padding: 3px 8px 3px 4px;
  margin: -2px 0 6px -4px; border-radius: var(--r-sm);
  min-height: 30px;
}
.crumb:hover { opacity: 1; background: rgba(255,255,255,.12); }


/* ---- court header tool (re-draw) -------------------------------------- */
.court-tool {
  display: inline-grid; place-items: center;
  width: 26px; height: 26px; border-radius: var(--r-full);
  color: var(--on-brand); background: rgba(255,255,255,.14);
}
.court-tool:hover, .court-tool:focus-visible { background: rgba(255,255,255,.3); }

/* ---- replacement picker ----------------------------------------------- */
/* Its own styles: the board page loads app.css and board.css only, so the
   dashboard's list classes are not available here. */
.picker-box { max-height: 88dvh; display: flex; flex-direction: column; }
.picker-search { margin-bottom: 12px; flex: none; }

.picker {
  border: 1px solid var(--border); border-radius: var(--r-md);
  background: var(--surface);
  overflow-y: auto; -webkit-overflow-scrolling: touch;
  margin-bottom: 16px; min-height: 0;
}
.picker-row {
  display: flex; align-items: center; gap: 12px; width: 100%;
  padding: 11px 13px; text-align: left;
  border-bottom: 1px solid var(--border);
  color: var(--text);
}
.picker-row:last-child { border-bottom: none; }
.picker-row:hover, .picker-row:focus-visible { background: var(--accent-soft); }
.picker-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.picker-pos {
  flex: none; min-width: 30px;
  font-family: var(--mono); font-size: 12px; font-weight: 500;
  color: var(--text-faint);
}
.picker-pos.is-next { color: var(--accent); font-weight: 700; }

.picker-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.picker-name { font-weight: 600; font-size: 14.5px; overflow-wrap: anywhere; }
.picker-meta { font-size: 12px; color: var(--text-faint); }

.picker-rate { flex: none; text-align: right; display: flex; flex-direction: column; gap: 2px; }
.picker-rate-n { font-family: var(--mono); font-size: 13.5px; font-weight: 500; }
.picker-rate-d { font-size: 11px; color: var(--text-faint); white-space: nowrap; }
.picker-rate-d.is-match { color: var(--accent); font-weight: 600; }

.picker-empty { padding: 22px 14px; text-align: center; color: var(--text-faint); font-size: 13.5px; }
/* ---- how evenly matched the court is --------------------------------- */
/* The tier class carries the hue; everything inside inherits it through
   currentColor, so the number, the words and the bar can never disagree. */
/* The Settings tab is a whole view, not a card appended to the board: the
   court context around it belongs to the other tabs. */
.board-scroll.showing-settings > *:not(#organiser-settings) { display: none; }
#organiser-settings { margin-top: 2px; }

/* ---- resting: present, but out of the rotation ----------------------- */
.qrow-resting .qname { color: var(--text-muted); }
.qrow-resting        { opacity: .92; }

/* ---- a score the board will not accept ------------------------------- */
.score-bad {
  color: var(--live);
  box-shadow: inset 0 0 0 2px var(--live);
}

/* ---- correcting a score in the log ----------------------------------- */
.editrow {
  display: flex; align-items: center; gap: 12px;
  padding: 9px 11px;
  border: 1px solid var(--border); border-radius: var(--r-sm);
  background: var(--bg-alt);
  cursor: text;
}
.editname { flex: 1; min-width: 0; font-size: 14px; font-weight: 600; overflow-wrap: anywhere; }
.score-edit {
  flex: none; width: 62px; text-align: center;
  font-family: var(--mono); font-size: 19px; font-weight: 500;
}

/* ---- sortable standings headers -------------------------------------- */
/* The whole header is the target, so the button fills it rather than
   leaving a dead strip of padding that looks clickable and is not. */
.th-sort { padding: 0 !important; }
.th-sort button {
  width: 100%; padding: 10px 8px;
  display: inline-flex; align-items: center; justify-content: flex-end; gap: 3px;
  font: inherit; color: inherit; letter-spacing: inherit; text-transform: inherit;
  background: none; cursor: pointer;
}
.th-sort button:hover { color: var(--text); }
/* padding:0 on the cell took the last column's gutter with it. */
.th-sort:last-child button { padding-right: 18px; }
.th-sort:first-child button { justify-content: flex-start; padding-left: 18px; }
.th-arrow { opacity: 0; font-size: 9px; line-height: 1; }
.th-sort.is-sorted            { color: var(--accent); }
.th-sort.is-sorted .th-arrow  { opacity: 1; }

/* ---- the spectator prompt -------------------------------------------- */
/* One line above the courts. The form it points at is below them, so the
   scoreboard is what fills the screen on arrival. */
.joinbar {
  display: flex; align-items: center; gap: 12px;
  margin-bottom: 14px; padding: 11px 14px;
  border: 1px solid var(--accent); border-radius: var(--r-md);
  background: var(--surface); color: var(--text);
  text-decoration: none;
}
.joinbar strong { display: block; font-size: 14.5px; }
.joinbar .hint  { display: block; margin-top: 1px; }
.joinbar > span:first-child { flex: 1; min-width: 0; }
.joinbar-go {
  flex: none; padding: 8px 15px; border-radius: var(--r-full);
  background: var(--accent); color: var(--on-accent);
  font-size: 13px; font-weight: 700; white-space: nowrap;
}
.joinbar:hover .joinbar-go { filter: brightness(1.06); }

/* ---- how even the two sides are, above a court ----------------------- */
.balance {
  display: flex; align-items: center; gap: 9px;
  margin-bottom: 12px; padding: 6px 10px;
  border-radius: var(--r-sm);
  background: var(--bg-alt);
  font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
  position: relative; z-index: 1;
}
.balance-num { font-family: var(--mono); font-size: 13px; font-weight: 500; letter-spacing: 0; }
.balance-txt { flex: none; }
.balance-meter {
  flex: 1; min-width: 36px; height: 4px; overflow: hidden;
  border-radius: var(--r-full); background: var(--border);
}
.balance-meter i { display: block; height: 100%; border-radius: inherit; background: currentColor; }

/* Measured against --court-surface, which is what these sit on. They were
   the wrong way round and two of them were not legible: "even" — the
   ordinary case — came out in the win orange at 3.65:1 and filled the
   width, while "lopsided" was lime on pale green at 1.20:1. So the
   unremarkable result shouted and the one worth acting on could not be
   read. Teal for a fair draw; orange only for one worth re-drawing. */
.balance-even     { color: var(--accent); }      /* 5.13:1 */
.balance-close    { color: var(--accent); }      /* 5.13:1 */
.balance-tilted   { color: var(--text-muted); }  /* 5.68:1 */
.balance-lopsided { color: var(--action-ink); }  /* 4.53:1 */

/* The phone's narrower take on the same strip. */
.balance { padding: 6px 2px 8px; }
.balance-num { font-size: 11.5px; }
.balance-txt { font-size: 10.5px; letter-spacing: .08em; }
.balance-meter { height: 3px; opacity: .55; }

/* ---- people waiting to be let in ------------------------------------- */
.pending-row { display: flex; align-items: center; gap: 8px; }
.pending-row .qname { flex: 1; min-width: 0; }

/* ---- check-in dialog -------------------------------------------------- */
/* Roomier than a confirm box: it carries a form, and on a short phone in
   landscape it has to be able to scroll rather than crop the button. */
.checkin-box {
  max-width: 460px; padding: 14px;
  max-height: calc(100dvh - 40px); overflow-y: auto;
}
.checkin-box > form { border: none; box-shadow: none; background: none; padding: 0 8px 8px; margin: 0; }
.checkin-close { display: flex; justify-content: flex-end; }
.checkin-close .iconbtn { color: var(--text-muted); }
.checkin-box .flash { margin: 0 8px 12px; }

/* ---- countdown to the start ------------------------------------------ */
/* Removed by the renderer once the time passes: a countdown that has
   finished counting is clutter sitting over the scoreboard. */
.countdown {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  padding: 11px 14px; border-radius: var(--r-md);
  border: 1px solid var(--border); background: var(--surface);
}
.countdown-clock {
  font-family: var(--mono); font-size: 21px; font-weight: 500;
  color: var(--accent); letter-spacing: .02em;
}
.countdown-at { margin-left: auto; font-size: 12.5px; color: var(--text-faint); }

/* =====================================================================
   Desktop — must stay at the END of this file.

   A media query adds no specificity, so these rules only beat the
   mobile defaults above by coming later in the cascade. Moving this
   block earlier silently reverts the desktop layout to the phone one.

   A phone-width column on a large monitor wastes the room that makes a
   scorer's table useful: from 1024px the board widens, courts sit side
   by side, the tab strip leaves the bottom edge, and the queue becomes
   a panel that is always in view.
   ===================================================================== */
/* ---- tablets: the side panel from 768px ---------------------------------
   Below 1024 the board used to be the phone's: one 620px column, so an
   iPad held upright -- or a laptop at 150% display scaling -- showed the
   courts in the middle with empty bands either side and no queue panel.
   From 768 there is room for both, so the panel comes back beside the
   courts and the board takes the width.

   Everything else stays the phone's: the sticky header, the bottom tabs,
   the card view. That is also why the panel does not stick here -- the
   header already does, and a sticky panel would slide up under it. */
@media (min-width: 768px) and (max-width: 1023px) {
  .board, .tabbar { max-width: none; }
  .board-split {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 280px;
    gap: 20px;
    align-items: start;
  }
  .board-aside { display: block; }
  /* Only the Courts tab has a panel; elsewhere the column is not held. */
  .board-split:has(> .board-aside:empty) { grid-template-columns: minmax(0, 1fr); }
  .board-split:has(> .board-aside:empty) > .board-aside { display: none; }
}

@media (min-width: 1024px) {
  .board { max-width: 1280px; }
  .board-scroll { flex: 1; padding: 20px 24px 48px; }

  /* 320px rather than 300: the rail now carries the leaders as well as
     the queue, and a name plus a W-L needs the extra room. */
  .board-split {
    display: grid;
    /* 288px, not 320: the rail holds a name and a W-L, and the 32px it was
       carrying spare is 32px the four names on a court row are not. */
    grid-template-columns: minmax(0, 1fr) 288px;
    gap: 24px;
    align-items: start;
  }
  /* 16px, not 62: 62 cleared a sticky header that no longer sticks, so
     the rail parked itself a header's height below where it should. */
  .board-aside { display: block; position: sticky; top: 16px; }

  /* Courts first. The rewards on offer and the folded "how this runs"
     are reference, not action, and stacked above the courts they put
     280px of reading between the tab strip and the first score. They
     keep their place on a phone, where the whole page is one scroll
     anyway and reordering would only hide them. */
  .board-scroll { display: flex; flex-direction: column; }
  .board-extra { order: 1; }

  /* The header does not follow you on a large screen: it is sticky only
     on phones, where vertical space is scarce. Leaving it sticky here made
     it ride up over the tab strip on scroll - two stacked sticky bars
     fighting for the same 0px. The tab strip sticks instead, because
     navigation is what you actually reach for mid-scroll. */
  .board-head { position: static; }

  /* auto-fill, not auto-fit: a lone court keeps a sensible width instead
     of stretching across the whole column.

     Declared on the courts' own container. The previous version put it on
     the stack that contains them, via :has(.court), which made every
     sibling a cell of the same size — the countdown and the session
     numbers were laid out as though they were courts, which is exactly
     how the top of the page came to read as three unrelated boxes. */
  .courtgrid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
    gap: 16px;
    /* stretch, not start: an open court beside a running one was a stub
       half its neighbour height, which read as something being broken
       rather than as a court waiting for players. */
    align-items: stretch;
  }
  .courtgrid > .court { display: flex; flex-direction: column; }
  .courtgrid > .court > .court-body { flex: 1; }

  /* The only sticky chrome on desktop. Every positioning property the
     mobile bar sets is restated here rather than left to inherit, so the
     bottom bar cannot leak in as a half-applied top bar. */
  .tabbar {
    position: sticky;
    top: 0; left: auto; right: auto; bottom: auto; z-index: 20;
    width: auto; max-width: none;
    margin: 0;
    /* Matches .board-scroll inset so tabs line up with the content. */
    padding: 0 24px;
    gap: 2px;
    background: var(--bg);
    border-top: none; border-bottom: 1px solid var(--border);
    box-shadow: none;
  }
  .tabbar button {
    flex: 0 0 auto;
    flex-direction: row; gap: 9px;
    min-height: 42px; padding: 9px 18px;
    font-size: 11.5px;
    border-top: none; border-bottom: 2.5px solid transparent; margin-top: 0;
  }
  .tabbar button[aria-current="page"] {
    border-top-color: transparent;
    border-bottom-color: var(--accent);
  }
  .tab-badge { transform: translate(13px, -10px); }

  /* Settings is a destination, not one of the views you flick between. */
  .tabbar .tab-right { margin-left: auto; }

  /* The header can breathe, and the code gets read from further away. */
  .board-head { padding: 14px 24px 15px; }
  .board-head h1 { font-size: 23px; }
  .code-chip { font-size: 15px; padding: 4px 11px; }

  /* A bigger target for scoring at arm's length. */
  /* clamp() already grows the score with the viewport; this is the ceiling
     for a courtside tablet, where the board is read standing up. */
  .score { font-size: clamp(64px, 7vw, 88px); }
  .scorer { gap: 14px 30px; }
  .stepper { width: 52px; height: 52px; font-size: 23px; }
  .side-names { min-height: 50px; }
  .pname { font-size: 16px; }
  /* A court, roughly court-shaped. At 230px tall in a 900px window the
     hero of this page was the smallest thing on it, and the centre
     line and the two ends had nothing to divide. Content-box height,
     so a court that needs more room still grows. */
  .court-body { padding: 20px 16px 18px; min-height: 230px; }
  .court-body.playing { display: flex; flex-direction: column; justify-content: center; }
}

@media (min-width: 1440px) {
  .board { max-width: 1360px; }
}

/* Wide monitors: let the third court onto the first row.
   ---------------------------------------------------------------------
   The arithmetic is the whole reason these exist. The rail is a fixed
   320px with a 24px gap, and .board-scroll adds 24px of padding either
   side, so the court grid gets (board width - 392px). A court track is
   minmax(340px, 1fr) with a 16px gap, so three of them need 1052px —
   which means the board itself has to reach ~1444px before a third
   court will sit beside the other two.

   Capped at 1360px it never did: on an 1880px screen the board used
   1360px, laid two courts out, and wrapped the third onto a row of its
   own with a whole court's width empty beside it — while ~500px of
   monitor went unused on either side. A three-court session is the
   common case, and the organiser was scrolling to see a court that had
   room to be in view. */
@media (min-width: 1600px) {
  .board { max-width: 1540px; }
}

@media (min-width: 1920px) {
  .board { max-width: 1760px; }
}

/* =====================================================================
   TWO WAYS TO SEE THE COURTS (desktop)
   =====================================================================
   The card layout gave each court ~300px of height to show four names and
   two numbers, so a club running six was scrolling to referee. The row
   layout fixed the density and broke something else: four names, two
   scores, four steppers, a balance gauge and the action all competing for
   the ~890px left beside the up-next rail, so every name ellipsised
   (Bem Ocampo became "Bem Ocampo 2....") and the right-hand pair
   disappeared underneath the action button.

   Neither shape is wrong; they answer different questions. Running the
   night, you want every court at once (Rows). Scoring one court at arm's
   length, you want the number to be enormous (Courts). So the board asks
   which, remembers the answer, and stops pretending one layout does both.

   The switch is a class on .courtgrid - .view-rows / .view-cards - and
   nothing about scoring, staging or the debounced flush is touched,
   because that logic is the part of this page that must not break for a
   cosmetic change.

   The phone gets the card either way: one court at a time down a narrow
   screen is the right shape there, and a row would only squeeze the score.
   ===================================================================== */
@media (min-width: 1024px) {
  /* Neither view wants the stacking margin the phone card carries; the
     grid's own gap is the spacing. */
  .courtgrid > .court { margin-bottom: 0; }

  /* Rows as tall as a court needs, never a share of the container. Inside
     the app shell .board-scroll is a flex item that fills the viewport, so
     the inherited stretch handed each court a third of the screen and a
     two-line row became 400px of empty navy. */
  .courtgrid { align-content: start; }

  /* ===================================================================
     VIEW: COURTS - a card per court, the score at full size
     =================================================================== */
  /* 440px, not the 340px the grid inherits. With the session rail beside
     them the courts have ~1080px, and at 340px auto-fit put THREE cards on
     a row — 340px each, which is not enough for two names, two ratings and
     two faces, so every name broke one letter per line. Two abreast is the
     shape the mockup draws and the width the content needs. */
  .courtgrid.view-cards {
    grid-template-columns: repeat(auto-fit, minmax(440px, 1fr));
    grid-auto-rows: min-content;
    gap: 16px;
  }

  /* ===================================================================
     VIEW: ROWS - one court per line
     =================================================================== */
  .courtgrid.view-rows {
    grid-template-columns: 1fr;
    gap: 10px;
    grid-auto-rows: min-content;
  }

  /* Identity left, play area right. 128px -- narrower than the 156 it once
     was, since the head holds a number, a status and three tools, and wider
     than the 90 it was cut to: at 90 the court number was 13.5px and the
     tools were 20px circles, too small to hit or to read from arm's length,
     while the row's middle had width to spare. 128 holds the three tools,
     now 28px, on one line. */
  .courtgrid.view-rows > .court {
    display: grid;
    grid-template-columns: 128px minmax(0, 1fr);
    align-items: stretch;
  }

  .courtgrid.view-rows > .court > .court-head {
    flex-direction: column;
    align-items: flex-start;
    justify-content: center;
    gap: 6px;
    padding: 12px;
  }
  /* Centred, like everything under it. The lines below the number (status,
     clock, tools) are centred by the head's justify-content, but the number
     takes a whole line to itself and so sat against the left edge while the
     chip under it sat in the middle. */
  .courtgrid.view-rows > .court > .court-head .court-no { font-size: 16px; text-align: center; }
  /* A spectator's court has no clock and no tools, so the meta box is empty
     -- but still a flex item, and the gap before it pushed the chip 2-3px
     off the centre line the number is on. */
  .courtgrid.view-rows > .court > .court-head .court-meta:empty { display: none; }
  /* The status and the re-draw tool share one short line under the court
     number. wrap, not nowrap: a court whose hold streak has a chip as well
     drops it to a second line rather than pushing the tool out through the
     side of the navy block, which is what a fixed-width head does to
     anything that will not fit. */
  .courtgrid.view-rows > .court > .court-head .court-meta {
    font-size: 11.5px;
    gap: 5px;
    flex-wrap: wrap;
    max-width: 100%;
  }
  .courtgrid.view-rows > .court > .court-head .chip-ready { font-size: 9.5px; padding: 3px 7px; }
  .courtgrid.view-rows > .court > .court-head .court-tool { width: 28px; height: 28px; }
  /* The glyph is drawn at 15px (board.js); CSS outranks the attribute. */
  .courtgrid.view-rows > .court > .court-head .court-tool svg { width: 16px; height: 16px; }

  /* [ ——— the court ——— ] [actions]
     Two columns, not four. The draw-quality gauge used to hold its own
     column, and the two action buttons used to sit side by side; both were
     spending horizontal width that the four names needed and neither has
     to be on that axis at all. */
  .courtgrid.view-rows > .court > .court-body {
    position: relative;
    min-height: 0;
    padding: 13px 14px;
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    column-gap: 14px;
  }

  /* The draw quality, along the top of the row: the figure, the word for
     it, and the bar.
     It spends its width on the row's own line rather than taking a column
     out of the four names, which is what it used to do. The bar was on its
     own here for a while and a bare 3px line says "something is 83% of
     something" to nobody -- the number is the part people read out. */
  .courtgrid.view-rows .balance {
    grid-column: 1 / -1;
    margin: 0 0 9px;
    padding: 0;
    background: none;
    gap: 8px;
  }
  .courtgrid.view-rows .balance-num { font-size: 11.5px; }
  .courtgrid.view-rows .balance-txt { font-size: 9.5px; opacity: .8; }
  .courtgrid.view-rows .balance-meter {
    height: 3px;
    border-radius: var(--r-full);
    opacity: .85;
  }

  /* One grid for all six things on the court, rather than two sides each
     dividing their half again. Nested, a name was competing with its own
     steppers for half of half the row and lost by 80px; flattened, the two
     name tracks share everything the scores do not need.

     display:contents on .side removes the box without removing the
     element, so the markup board.js builds is untouched. */
  .courtgrid.view-rows .sides {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto auto minmax(0, 1fr);
    align-items: center;
    gap: 10px;
    padding: 0;
  }
  .courtgrid.view-rows .side { display: contents; }

  /* No box inside the box. .playing draws the court as a bordered, rounded
     panel — right on a card, but in a row it was a second rounded
     rectangle inset inside the first, and its empty ends read as a panel
     that had failed to load. The row's own surface is the court. */
  .courtgrid.view-rows .playing .sides {
    border: 0;
    border-radius: 0;
    padding: 0;
  }

  /* The net still divides the two scores. The kitchen band does not: at
     row height it drew two lines through the middle of the numbers. */
  .courtgrid.view-rows .playing .sides::after { display: none; }

  .courtgrid.view-rows .side-names { min-height: 0; gap: 6px; }
  .courtgrid.view-rows .sides > .side:last-child .side-names { order: 2; }
  .courtgrid.view-rows .sides > .side:last-child .scorer      { order: 1; }

  /* Left team, score, right team — filled edge to edge.
     Hugging the score instead left ~200px of empty court at each end of an
     846px play area with everything huddled in the middle: it read as a
     wide green box somebody had failed to fill rather than as a row. The
     slack belongs either side of the score, where it is symmetric and the
     score is plainly the centre of the row. */
  .courtgrid.view-rows .pname {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    gap: 5px;
    min-width: 0;
    overflow: hidden;
    font-size: 13.5px;
    line-height: 1.35;
  }
  .courtgrid.view-rows .sides > .side:last-child .pname { justify-content: flex-end; }

  /* The name is the only thing here allowed to shrink, and the only thing
     allowed to truncate. min-width:0 is what makes that true: without it a
     flex item refuses to go below its content width, so a long name pushed
     the score, the steppers and the buttons sideways instead of
     ellipsising — which is what a 25-character name did to this row. */
  .courtgrid.view-rows .pn {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .courtgrid.view-rows .pname .lvl,
  .courtgrid.view-rows .pname .streak,
  .courtgrid.view-rows .pname .swapbtn { flex: none; margin: 0; }
  .courtgrid.view-rows .pname .lvl { font-size: 10px; }

  /* Replacing a player sits on the outside of the name, mirrored about the
     net, so it is always the thing furthest from the score — and always
     there. As a 60%-opacity glyph tucked against the inner edge it read as
     a smudge, not a control: several people looked at this row and
     reported that swapping a player was missing. */
  .courtgrid.view-rows .pname .swapbtn { order: -1; width: 22px; height: 22px; }
  .courtgrid.view-rows .sides > .side:last-child .pname .swapbtn { order: 1; }

  /* One row: minus, score, plus. Two rows was right when the score owned a
     card; in a strip it is what made the strip tall. */
  .courtgrid.view-rows .scorer {
    grid-template-columns: auto auto auto;
    grid-template-areas: "minus score plus";
    gap: 5px;
  }
  /* Still the largest thing in the row, and still tabular — it just
     stopped needing 300px of card to say so. The Courts view is where a
     score has to read from the far side of the net.

     An explicit width, not auto. When you can score, .score is an <input>,
     and width:auto on an input means its intrinsic size — the browser's
     ~20-character default. In a track sized `auto` that came to ~250px per
     side, the two of them ate the whole row, and the 1fr name tracks
     either side resolved to nothing: four players, no names. */
  .courtgrid.view-rows .score {
    font-size: 32px;
    width: 1.85em;
    min-width: 0;
    flex: none;
  }
  .courtgrid.view-rows .stepper { width: 30px; height: 30px; font-size: 17px; }

  /* Side by side. Stacked, the pair stood 74px tall and every row was as
     tall as them — 104px of row to hold two lines of names that need 44.
     The names have width to spare now that they are not all crowded into
     the middle, so the buttons can spend it and the row can be a row. */
  .courtgrid.view-rows .court-actions {
    margin: 0;
    max-width: none;
    flex-direction: row;
    align-items: center;
    gap: 7px;
  }
  .courtgrid.view-rows .court-actions .btn {
    white-space: nowrap;
    min-height: 34px;
    padding-inline: 12px;
    font-size: 14px;
  }
  /* "Start"/"Finish", not "Start game"/"Finish game": the column is 98px
     wide and the court underneath the button is what the game is. */
  .courtgrid.view-rows .btn-word { display: none; }

  /* Said once under the first court was already enough; said under every
     court it was three copies of the same sentence competing with the
     scores. The button beside it says the same thing. */
  .courtgrid.view-rows .court-note { display: none; }

  /* An open court is a slim waiting strip, not a full-height panel. */
  .courtgrid.view-rows .court-empty { padding: 14px; }
}

/* ---- the view switch -------------------------------------------------
   A segmented pair above the courts. Two options, both named, the current
   one filled - not an icon toggle you have to press to find out what it
   does. The choice is this viewer's, kept in localStorage, so a scorer who
   works in rows does not get cards back every time the page reloads. */
.courtbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin: 0 2px;
}
.courtbar .eyebrow { margin: 0; }
.viewtoggle {
  display: inline-flex;
  gap: 2px;
  padding: 2px;
  background: var(--surface-alt);
  border: 1px solid var(--border);
  border-radius: var(--r-full);
}
.viewopt {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 12px;
  border-radius: var(--r-full);
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--text-muted);
  background: transparent;
  white-space: nowrap;
}
.viewopt:hover { color: var(--text); }
.viewopt.on {
  background: var(--surface);
  color: var(--text);
  box-shadow: 0 1px 2px rgba(27, 42, 51, .12);
}
.viewopt svg { flex: none; }
/* The switch changes a desktop layout; on a phone both options render the
   same card, so offering the choice would be a control that does nothing. */
@media (max-width: 1023px) { .viewtoggle { display: none; } }

/* ---- the night at a glance ------------------------------------------- */
/* These facts sat in the main column as four bordered tiles. On a phone
   they were a row worth scrolling past; on a desktop they landed in the
   court grid and took a court's worth of space to repeat what the tab
   badges already said. In the header they are free: there is a band of
   empty space beside the session name on every screen wider than a phone,
   and nothing else wants it. */
.head-stats {
  display: flex;
  align-items: center;
  gap: 18px;
  flex: none;
}
.stat { text-align: center; min-width: 0; }
.stat-n {
  display: block;
  font-family: var(--dot);
  font-weight: 900;
  font-size: 26px;
  line-height: 1;
  color: var(--on-brand);
  font-variant-numeric: tabular-nums;
}
/* Same reasoning as .score-idle: a zero count is the least useful thing
   in the header, and in this face it is the most eye-catching. */
.stat-zero .stat-n { opacity: .45; }
.stat-zero .stat-l { opacity: .5; }

.stat-l {
  display: block;
  margin-top: 3px;
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--on-brand);
  opacity: .72;
  white-space: nowrap;
}
/* On a phone the header is already carrying a name, a venue and a code, and
   the tab badges count the courts and the queue anyway. */
@media (max-width: 719px) { .head-stats { display: none; } }

/* ---- the organiser note ---------------------------------------------- */
/* One sentence of guidance. It was a full white card the width of the page,
   which gave a footnote the weight of a court. */
.board-note {
  margin: 0 2px 18px;
  padding-left: 11px;
  border-left: 2px solid var(--border-strong);
  font-size: 13px;
  line-height: 1.55;
  color: var(--text-muted);
}
.board-note strong { color: var(--text); }

/* ---- winners stay on -------------------------------------------------- */
/* Sits in the court header between the court name and the clock. It borrows
   nothing from --live or the READY chip: those say "this is happening now",
   and a hold is a fact about the last game, not this one. */
.holdchip {
  flex: none;
  font-family: var(--mono);
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: .05em;
  padding: 3px 8px;
  border-radius: var(--r-full);
  background: var(--win-soft);
  color: var(--action-ink);
  white-space: nowrap;
}

/* ---- a run of wins, or of losses -------------------------------------- */
/* The server only sends runs of two or more, so one of these on a name
   always means something. Both directions are shown: a player four losses
   deep is the one the organiser should be putting on a stronger side. */
.streak {
  display: inline-block;
  margin-left: 6px;
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  line-height: 1.6;
  padding: 0 5px;
  border-radius: var(--r-full);
  vertical-align: 1px;
}
.streak-win  { background: var(--accent-soft); color: var(--accent); }
.streak-loss { background: var(--surface-alt); color: var(--text-muted); }

/* ---- lift the courts off the page ------------------------------------- */
/* The complaint was that the board reads flat, and a court card carrying the
   same hairline border as every list row is most of why. The shadow is the
   one app.css already defines, so the board still looks like the app. */
.court { box-shadow: var(--shadow); }

/* ---- the results feed in the rail ------------------------------------- */
/* Three columns rather than a sentence: the eye goes down the score column
   looking for the game it half-remembers, and ragged text would make that
   a reading exercise. */
.recent {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  column-gap: 10px;
  padding: 9px 12px;
  border-bottom: 1px solid var(--border);
  line-height: 1.35;
}
.recent:last-child { border-bottom: none; }
.recent-win {
  font-size: 12.5px; font-weight: 700; color: var(--text);
  overflow-wrap: anywhere;
}
.recent-score {
  font-family: var(--mono); font-size: 12.5px;
  color: var(--text-muted); white-space: nowrap;
}
.recent-lost {
  grid-column: 1 / -1;
  font-size: 11.5px; color: var(--text-faint);
  overflow-wrap: anywhere;
}

/* ---- where you stand -------------------------------------------------- */
/* The one strip on the board that is about the person holding the phone.
   Above the courts and outside the tab body, so switching to Results to
   settle an argument never costs you the knowledge that you are up. */
.you {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px 14px;
  margin: 0 0 14px;
  padding: 13px 16px;
  border-radius: var(--r-md);
  background: var(--surface);
  border: 1px solid var(--border);
  box-shadow: var(--shadow);
}
.you-label {
  font-size: 10.5px; font-weight: 600; letter-spacing: .1em;
  text-transform: uppercase; color: var(--text-muted);
}
.you-pos {
  font-family: var(--display); font-weight: 600; font-size: 26px;
  line-height: 1; color: var(--brand); font-variant-numeric: tabular-nums;
}
.you-eta, .you-ahead { font-size: 13px; color: var(--text-muted); }
.you-ahead { margin-left: auto; font-family: var(--mono); font-size: 12px; }

/* One game away. Loud enough to catch someone sitting down, quiet enough
   not to be mistaken for the court actually being ready. */
.you-next { border-color: var(--accent); background: var(--accent-soft); }
.you-next .you-label { color: var(--accent); }

/* Your court is ready, or you are on it. This is the moment the whole
   product exists for, so it is the only place on the board that fills. */
.you-on {
  background: var(--brand); border-color: var(--brand-deep);
  color: var(--on-brand);
}
.you-on .you-label { color: var(--action-on-dark); }
.you-court {
  font-family: var(--display); font-weight: 600; font-size: 22px;
  letter-spacing: .02em; color: var(--on-brand);
}
.you-on .you-with { font-size: 13px; color: var(--on-brand); opacity: .85; }

.you-rest .btn { margin-left: auto; }

/* ---- the standings table --------------------------------------------- */
/* width:1% plus nowrap is the standard way to say "as narrow as your
   content": the browser gives the column its minimum and hands the slack
   to the one column without it, which here is the name. */
.standings .num {
  width: 1%;
  white-space: nowrap;
  text-align: right;
}
.standings thead .num button { justify-content: flex-end; }
.standings .td-w { color: var(--accent); }
.standings .td-l { color: var(--text-faint); }
/* Context, not achievement: strength describes the draw a player got, so
   it sits back from the numbers they earned. */
.standings td.col-str { color: var(--text-muted); }
.standings .col-win { min-width: 134px; }

/* The column people actually scan, so it gets a shape as well as a
   number: 75% and 50% are two characters apart and half a bar apart.
   The bar is inline-block, not a flex child of the cell: display:flex on a
   <td> takes it out of table layout, and the row's bottom border then drew
   at the flex box's own height instead of the row's - a visible seam down
   the whole column. */
.winbar {
  display: inline-block;
  vertical-align: middle;
  margin-right: 9px;
  width: 46px;
  height: 4px;
  border-radius: var(--r-full);
  background: var(--border);
  overflow: hidden;
}
.standings td.col-win .mono { vertical-align: middle; }
.winbar i {
  display: block;
  height: 100%;
  background: var(--accent);
  border-radius: inherit;
}
/* ---- the podium -------------------------------------------------------- */
/* First three get a trophy and a colour. The trophy is what actually carries
   the meaning: a table that says "third" only in bronze says nothing to
   somebody who cannot tell bronze from grey, so the shape is the signal and
   the colour is the flourish. Each measured against a white row:
   gold 5.54:1, silver 6.00:1, bronze 6.76:1. */
.medal {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; flex: none;
  font-family: var(--mono); font-size: 12px; color: var(--text-faint);
}
.medal-1 { color: #8A6100; }
.medal-2 { color: #5A6472; }
.medal-3 { color: #8A4B1F; }

/* The name takes the same colour, which is the part read from a distance. */
.rank-1 .pl-name, td .medal-1 + .pl-name { color: #8A6100; font-weight: 600; }
td .medal-2 + .pl-name { color: #5A6472; font-weight: 700; }
td .medal-3 + .pl-name { color: #8A4B1F; font-weight: 700; }

/* ---- the player sheet -------------------------------------------------- */
/* A name you can tap. Styled as text rather than as a button, because a row
   of twenty buttons reads as a control panel — but it is a real <button>
   underneath, so it is reachable by keyboard and announced as an action. */
.namebtn {
  border: 0; background: none; padding: 0;
  font: inherit; color: var(--text); font-weight: 600;
  text-align: left; cursor: pointer;
  border-bottom: 1px dotted var(--border-strong);
}
.namebtn:hover { color: var(--accent); border-bottom-color: var(--accent); }
.namebtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.sheet-head h3 { font-size: 20px; }
.sheet-head .sub { font-size: 13px; color: var(--text-muted); }

/* The night in six figures, across the top of the sheet. Three and three,
   at every width: six across squeezes "OPP. STRENGTH" to an ellipsis on a
   laptop, and five across left the sixth alone on a row of its own. */
.sheet-figs {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 6px;
  text-align: center;
}
.sheet-fig {
  background: var(--surface-alt);
  border-radius: var(--r-sm);
  padding: 9px 4px 7px;
  min-width: 0;
}
.sheet-fig-n {
  display: block;
  font-family: var(--display); font-weight: 600; font-size: 19px;
  line-height: 1; color: var(--brand); font-variant-numeric: tabular-nums;
}
.sheet-fig-l {
  display: block; margin-top: 4px;
  font-size: 9.5px; font-weight: 700; letter-spacing: .07em;
  text-transform: uppercase; color: var(--text-muted);
  overflow: hidden; text-overflow: ellipsis;
}
@media (max-width: 380px) {
  /* Three across still fits at 360px; below that the labels win more from
     a second column than the grid loses from a third row. */
  .sheet-figs { grid-template-columns: repeat(2, 1fr); }
}

/* A name is one word for wrapping purposes. Without this the right-hand side
   broke as "Tessa Uy & Ryan / Dizon", splitting a person in half. */
.nextup-teams .namebtn,
.nextup-teams .nextup-name,
.nextup-teams .nextup-hold { white-space: nowrap; }
/* ...but never wider than its side. Unbounded, one twenty-letter name
   pushed the whole rail past the edge of the screen. Cut with an ellipsis
   instead; hovering shows it whole (see the title handler in board.js). */
.nextup-teams { grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); }
.nextup-teams .namebtn,
.nextup-teams .nextup-name,
.nextup-teams .nextup-hold {
  display: inline-block; max-width: 100%; vertical-align: bottom;
  overflow: hidden; text-overflow: ellipsis;
}

/* ---- how this open play runs ------------------------------------------ */
/* A rule per line: the bold half is what the setting is, the rest is what it
   means for somebody who has just walked in. Read once, by a newcomer, and
   never again — so it sits collapsed and costs the regulars nothing. */
.rules { list-style: none; margin: 0; padding: 0; display: grid; gap: 11px; }
.rules li {
  display: grid; gap: 2px;
  padding-left: 12px; border-left: 2px solid var(--border-strong);
}
.rules strong { font-size: 13.5px; color: var(--text); }
.rules span { font-size: 12.5px; line-height: 1.5; color: var(--text-muted); }

.rules-note {
  margin-top: 16px; padding-top: 14px;
  border-top: 1px solid var(--border);
}

/* ---- every game, on the player sheet ---------------------------------- */
/* One row per match: the result, who they had, who they played, the score.
   This replaced two tallies — "with Ben x3", "met Cora x2" — which were the
   same fact torn in half and stripped of the one detail anybody remembers. */
.match {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 11px;
  padding: 10px 12px;
  border-bottom: 1px solid var(--border);
}
.match:last-child { border-bottom: none; }

/* The letter carries the result; colour only reinforces it, so the row
   still reads for anybody who cannot tell the two tints apart. */
.match-wl {
  flex: none;
  width: 22px; height: 22px; border-radius: var(--r-full);
  display: grid; place-items: center;
  font-family: var(--mono); font-size: 11px; font-weight: 500;
  background: var(--surface-alt); color: var(--text-muted);   /* 6.06:1 */
}
.match-won .match-wl { background: var(--accent-soft); color: var(--accent); }  /* 5.45:1 */

.match-who { min-width: 0; display: grid; gap: 1px; }
/* --text-faint measures 3.81:1 on a card and this is 12.5px, so the quieter
   half of the pair is --text-muted at 7.25:1. */
.match-with, .match-against {
  font-size: 12.5px; line-height: 1.35; color: var(--text-muted);
  overflow-wrap: anywhere;
}
.match-with strong, .match-against strong { color: var(--text); font-weight: 700; }

.match-score {
  flex: none;
  font-family: var(--mono); font-size: 13px; font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}
.match-won .match-score { color: var(--text); font-weight: 500; }

/* ---- moving somebody in the queue -------------------------------------- */
/* Up and down rather than dragging: HTML5 drag needs a pile of pointer code
   to work on a phone, and this is a courtside app. Two buttons are also
   reachable by keyboard and say plainly what they do. */

.draghandle {
  width: 30px; height: 30px; flex: none;
  display: grid; place-items: center;
  border: 0; border-radius: var(--r-full);
  background: transparent; color: var(--text-muted);
  cursor: grab;
  /* The page must not scroll while a finger is dragging a row, and this
     is the only element that suppresses it — the list scrolls normally
     everywhere else. */
  touch-action: none;
}
.draghandle:hover { background: var(--surface); color: var(--text); }
.draghandle:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.draghandle:active { cursor: grabbing; }

/* The row under the finger, and the list it is moving through. */
.qrow.is-held {
  background: var(--surface);
  box-shadow: var(--shadow-lift);
  border-radius: var(--r-md);
  position: relative; z-index: 2;
  cursor: grabbing;
}
/* No text selection anywhere in the list while a drag is running — on a
   desktop the pointer would otherwise highlight names as it passes them. */
.is-sorting { user-select: none; }
.is-sorting .qrow:not(.is-held) { transition: transform .12s ease; }

/* ---- the activity log -------------------------------------------------- */
/* One line per thing that happened, newest first. Read when something is in
   dispute, so it favours scanning a column of times over decoration. */
.logrow {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 10px;
  padding: 9px 12px;
  border-bottom: 1px solid var(--border);
  font-size: 13px;
}
.logrow:last-child { border-bottom: none; }
/* The Log tab while the page it asked for is on its way: the old lines stay
   readable, dimmed, rather than blanking out for the length of a request. */
.actlog { transition: opacity .15s; }
.actlog.is-stale { opacity: .55; }
.logtext { color: var(--text); line-height: 1.4; overflow-wrap: anywhere; }
.logtime {
  font-family: var(--mono); font-size: 11.5px;
  color: var(--text-muted); white-space: nowrap;   /* 7.25:1 */
}

/* A dot rather than a word for the kind: the sentence already says what
   happened, and a column of KIND labels would repeat it in shouting. */
.logdot {
  width: 7px; height: 7px; border-radius: var(--r-full);
  background: var(--border-strong); align-self: center;
}
.logdot-game   { background: var(--accent); }
.logdot-checkin{ background: var(--live); }
.logdot-pair   { background: var(--action); }
.logdot-queue  { background: var(--text-faint); }
.logdot-status { background: var(--border-strong); }
/* A correction to somebody's record, and a change to the night itself.
   'session' was already being logged with no colour of its own. */
.logdot-player { background: var(--win); }
.logdot-session{ background: var(--brand); }

/* The button that brings a scheduled night forward sits at the far end of
   the countdown, away from the clock it is about to stop. */
.countdown .btn { margin-left: auto; align-self: center; flex: none; }

/* ---- the tail of a capped list --------------------------------------- */
/* A row, not a floating button: it is the last item of the list it belongs
   to, and anything else makes the card look like it has a footer. Lime as
   text rather than as a fill — this reveals, it does not act on anything. */
.showall {
  display: block; width: 100%;
  padding: 12px 14px;
  font-size: 13.5px; font-weight: 700; letter-spacing: .01em;
  color: var(--action-ink); background: transparent;
  border: none; border-top: 1px solid var(--border);
  text-align: center; cursor: pointer;
}
.showall:hover { background: var(--action-soft); }
.showall:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* ---- the rail's rhythm ------------------------------------------------ */
/* Every section in the rail brought its own margin — 16px here, 0 there,
   whatever the component happened to set for the tab it was also used on.
   That was survivable against the old tight shadow. Against the long soft
   one a card now casts light across the heading beneath it, and an 8px gap
   reads as two things overlapping rather than as two sections.
   One gap between sections, set here, and the components stop deciding. */
.board-aside > * + * { margin-top: var(--s-5); }
/* A heading that opens a section keeps its own small gap to its card, and
   drops the top margin it was carrying for the tab layout. */
.board-aside > * > .spread:first-child { margin-top: 0; }

/* The disc at the head of a player's sheet, and the gap it needs. */
/* The player sheet's header: face, name, and the two marks that qualify
   it -- a run and a rating -- on one line. gap on its own does nothing to
   a block, so without the display each of the four took a line of its
   own and the sheet opened four rows tall. */
.sheet-head { display: flex; align-items: center; gap: var(--s-3); }
/* Named for what it is, not .grow. .grow is the game-log row component --
   a five-column grid with a bottom border -- and borrowing it here to
   mean "this one stretches" drew that border through the middle of the
   sheet header. */
.sheet-who { flex: 1; min-width: 0; }
.sheet-lvl {
  flex: none;
  font-family: var(--mono);
  font-size: 12px;
  padding: 3px 9px;
  border-radius: var(--r-full);
  background: var(--surface-alt);
  color: var(--text-muted);
}
/* In a dense row the disc sits against the position number, so the
   name needs its own breathing room rather than inheriting the gap. */
.qrow > .avatar + .qname { margin-left: 2px; }

/* =====================================================================
   THE BOARD INSIDE THE APP SHELL
   =====================================================================
   The board used to be its own world: full-bleed, its own header, its own
   navigation. It now sits in the same rail as every other screen, so the
   chrome here is only what is specific to one session.
   ===================================================================== */
@media (min-width: 1024px) {
  /* The shell already centres and bounds the working area. */
  .main .board { max-width: none; margin: 0; min-height: 0; }
  /* padding-top back. It was zeroed when a separate top strip sat above
     this and supplied the gap; with the dark bar directly above, zero put
     the first line of content hard against it. */
  .main .board-scroll { padding: 18px 26px 48px; }
}

/* ---- the header, over the session's own cover ------------------------ */
.board-head.has-cover,
.board-head.drawn-cover {
  position: relative;
  background: var(--brand);
  overflow: hidden;
  isolation: isolate;
}

/* The photograph, dimmed hard. This is a banner behind type, not a picture
   to look at — the name, the code and the live dot all have to stay
   readable over whatever the organiser happened to upload. */
.board-head.has-cover::before {
  content: "";
  position: absolute; inset: 0; z-index: -2;
  background-image: var(--cover);
  background-size: cover;
  background-position: center;
}
.board-head.has-cover::after {
  content: "";
  position: absolute; inset: 0; z-index: -1;
  /* A scrim from the bottom, not a wash across the whole banner.
     Flattening the photo until the type was readable made the cover
     pointless -- it read as a plain navy band. Anchoring the content to
     the foot of a taller banner lets the photograph be seen up top while
     every word sits on near-solid ink. */
  background:
    linear-gradient(to top,
      color-mix(in srgb, var(--brand-deep) 97%, transparent) 0%,
      color-mix(in srgb, var(--brand-deep) 92%, transparent) 38%,
      color-mix(in srgb, var(--brand) 55%, transparent) 72%,
      color-mix(in srgb, var(--brand) 30%, transparent) 100%);
}

/* No photograph: the same court drawn in CSS the listing card uses. */
.board-head.drawn-cover::before {
  content: "";
  position: absolute; inset: 0; z-index: -2;
  background: linear-gradient(150deg, var(--c1), var(--c2));
}
.board-head.drawn-cover::after {
  content: "";
  position: absolute; inset: 0; z-index: -1; opacity: .2;
  background:
    linear-gradient(to right, transparent calc(50% - 1px), rgba(255,255,255,.9) 50%, transparent calc(50% + 1px)),
    linear-gradient(to bottom, transparent calc(50% - 1px), rgba(255,255,255,.5) 50%, transparent calc(50% + 1px));
}

@media (min-width: 1024px) {
  .board-head { padding: 20px 26px 22px; }
  .board-head h1 { font-size: 27px; letter-spacing: -.02em; }

  /* Tall enough to be a photograph rather than a tint, with the
     content pushed to the foot where the scrim is solid. */
  .board-head.has-cover {
    min-height: 196px;
    display: flex; flex-direction: column; justify-content: flex-end;
    padding-top: 64px;
  }
}

/* ---- tabs, in the new language --------------------------------------- */
@media (min-width: 1024px) {
  .tabbar {
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    padding: 0 26px;
  }
  .tabbar button { font-size: 12px; letter-spacing: .06em; color: var(--text-muted); }
  .tabbar button[aria-current="page"] { color: var(--text); border-bottom-color: var(--brand); }
}

/* ---- an open court ---------------------------------------------------
   In a row, .court-empty's centred flex column stretched every child to
   the full width of the track — which turned a small "Show the code"
   button into a lime slab wider than the court it belonged to. A waiting
   court is the quietest thing on this page, and should look it. */
@media (min-width: 1024px) {
  .courtgrid.view-rows > .court > .court-body.court-empty {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: flex-start;
    gap: 12px;
    text-align: left;
    padding: 12px 14px;
  }
  .courtgrid.view-rows > .court > .court-body.court-empty > .hint { margin: 0; flex: 1; }
  .courtgrid.view-rows > .court > .court-body.court-empty > .btn { flex: none; width: auto; }
  /* The share prompt is an offer, not the main action on the page. */
  .courtgrid.view-rows > .court > .court-body.court-empty > .btn:not(.btn-ghost) {
    background: var(--surface); color: var(--text);
    border: 1px solid var(--border-strong);
  }
  .courtgrid.view-rows > .court > .court-body.court-empty > .btn:not(.btn-ghost):hover {
    background: var(--surface-alt);
  }
}

/* =====================================================================
   THE TAB PANELS
   =====================================================================
   Queue, Players, Rules, Results and Log are the rest of the session, and
   they were living in a 730px column with the courts' rail beside them
   repeating what they already said. They get the whole width now.
   ===================================================================== */
@media (min-width: 1024px) {
  /* The rail renders nothing outside Courts, so the split collapses to one
     column rather than reserving 320px for an empty aside. */
  .board-split:has(> .board-aside:empty) { grid-template-columns: minmax(0, 1fr); }
  .board-split:has(> .board-aside:empty) > .board-aside { display: none; }

  /* A section heading, matching the rest of the app. */
  .board-scroll .spread > .eyebrow { font-size: 10.5px; letter-spacing: .12em; }

  /* ---- Rules: a reference sheet, not a single column ----
     grid-template-columns, not columns: .rules is already display:grid
     and multi-column layout does not apply to a grid container, so the
     first attempt did nothing at all. */
  .rules { grid-template-columns: 1fr 1fr; gap: 12px 30px; }

  /* ---- Results: a real standings table ---- */
  .board-scroll table { width: 100%; }
  .board-scroll thead th {
    position: sticky; top: 0; z-index: 1;
    background: var(--surface-alt);
    font-size: 10.5px; letter-spacing: .08em; text-transform: uppercase;
    color: var(--text-faint); font-weight: 700;
    padding: 10px 14px; border-bottom: 1px solid var(--border);
  }
  .board-scroll tbody td { padding: 11px 14px; border-bottom: 1px solid var(--border); }
  .board-scroll tbody tr:last-child td { border-bottom: 0; }
  .board-scroll tbody tr:hover { background: var(--surface-alt); }

  /* ---- Players: one row per person, scannable down the name column ---- */
  /* The roster, two abreast on a wide screen. Each row keeps its own
     divider, so the columns read as one list rather than two. */
  .plist { display: grid; grid-template-columns: 1fr 1fr; column-gap: 0; }
  .plist > .empty { grid-column: 1 / -1; }
  .plist > * { border-bottom: 1px solid var(--border); }
  .plist > *:nth-last-child(-n+2) { border-bottom: 0; }
  .plist > *:nth-child(odd) { border-right: 1px solid var(--border); }

  /* Two abreast is right for reading a roster and wrong for ordering one.
     The grid flows row-major, so the person one place further up the line
     sits to the LEFT of the one below them -- and dragging is a vertical
     gesture aimed at row midpoints, which cannot express "move sideways
     and up". Where the organiser can reorder, the list runs in a single
     column so that direction on screen and position in the queue agree. */
  .plist.is-sortable { grid-template-columns: 1fr; }
  .plist.is-sortable > *:nth-child(odd) { border-right: 0; }
  .plist.is-sortable > *:nth-last-child(-n+2) { border-bottom: 1px solid var(--border); }
  .plist.is-sortable > *:last-child { border-bottom: 0; }
}

/* The queue, the roster and the log all read better with a little air and
   a clear divider than as a stack of bordered boxes. */
.board-scroll .card > .prow,
.board-scroll .card > .qrow { border-bottom: 1px solid var(--border); }
.board-scroll .card > .prow:last-child,
.board-scroll .card > .qrow:last-child { border-bottom: 0; }

/* ---- the check-in bar, once ----------------------------------------
   It sits above the tab body, so it appeared on all six tabs. It is a
   real call to action for somebody watching, but it does not need to
   shout it six times. */
@media (min-width: 1024px) {
  .youbar { padding: 10px 14px; }
  .youbar h3, .youbar strong { font-size: 14px; }
}

/* =====================================================================
   THE SESSION RAIL
   =====================================================================
   The session's own views — courts, queue, players, rules, results, log
   and settings — were a strip of tabs across the top. Seven destinations
   is a lot to fit horizontally, and the strip sat in the same band as the
   app rail's own navigation, so the page read as two levels of tabs.

   As a column it is unambiguous: the navy rail is where you are in the
   app, this lighter one is where you are in the session.

   Below 1024px it goes back to being the fixed bottom bar. A rail on a
   phone would take the width the score needs, and the thumb is at the
   bottom of the screen anyway.
   ===================================================================== */
@media (min-width: 1024px) {
  .board-cols {
    display: grid;
    grid-template-columns: 210px minmax(0, 1fr);
    /* stretch, not start: the rail's right border is the edge between
       the session nav and its content, and a border that stops halfway
       down the page looks like something failed to load. Safe now that
       Settings is set apart with a rule instead of margin-top:auto. */
    align-items: stretch;
  }

  /* Every positioning property the mobile bar sets is restated, so the
     bottom bar cannot leak in as a half-applied rail. */
  .board-cols > .tabbar {
    position: sticky;
    top: 0; left: auto; right: auto; bottom: auto;
    z-index: 5;
    width: auto; max-width: none; margin: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 14px 12px 18px;
    background: var(--surface);
    border-top: 0;
    border-right: 1px solid var(--border);
    border-bottom: 0;
    box-shadow: none;
  }

  .board-cols > .tabbar button {
    flex: none;
    flex-direction: row;
    align-items: center;
    justify-content: flex-start;
    gap: 11px;
    width: 100%;
    min-height: 0;
    padding: 9px 11px;
    border: 0;
    border-radius: var(--r-sm);
    margin: 0;
    font-size: 13.5px;
    font-weight: 500;
    letter-spacing: 0;
    text-transform: none;
    color: var(--text-muted);
    text-align: left;
  }
  .board-cols > .tabbar button:hover { background: var(--surface-alt); color: var(--text); }

  .board-cols > .tabbar button[aria-current="page"] {
    background: var(--accent-soft);
    color: var(--text);
    font-weight: 600;
    border: 0;
  }

  /* The count sits at the end of the row, where a rail has room for it,
     rather than clipped to the corner of an icon. */
  .board-cols > .tabbar .tab-ico { position: static; }
  .board-cols > .tabbar .tab-badge {
    position: static; transform: none;
    margin-left: auto;
    background: var(--brand); color: var(--on-brand);
    font-family: var(--body); font-weight: 700;
  }
  .board-cols > .tabbar button[aria-current="page"] .tab-badge {
    background: var(--accent); color: var(--on-accent);
  }

  /* Settings is a destination rather than one of the views you flick
     between, so it is set apart -- but not with margin-top:auto. In a
     rail stretched to the viewport that pushed it to the very bottom,
     roughly 700px below Log and off the screen entirely. A rule and a
     gap say the same thing and keep it reachable. */
  .board-cols > .tabbar .tab-right {
    margin: 10px 0 0;
    padding-top: 10px;
    border-top: 1px solid var(--border);
    border-radius: 0 0 var(--r-sm) var(--r-sm);
  }

  /* The content no longer needs the page gutter on its left: the rail's
     own border is the edge. */
  .main .board-cols > .board-scroll { padding: 18px 26px 48px; }
}

/* ---- the account, in the banner ------------------------------------- */
.head-who {
  flex: none; display: grid; place-items: center;
  width: 40px; height: 40px; border-radius: var(--r-full);
  background: rgba(255,255,255,.14); color: var(--on-brand);
  text-decoration: none;
}
.head-who:hover { background: rgba(255,255,255,.26); }
.head-who .av { font-size: 14px; font-weight: 700; }
.head-signin { flex: none; white-space: nowrap; }

/* ---- the rewards card is not part of the aside's column -------------
   It is a direct child of .board-scroll, so it ran the full width and
   passed 320px underneath the aside — with its top edge finishing one
   pixel below the aside's last card, which is what made the two look
   welded together. It belongs over the courts, so it stops where they do. */
@media (min-width: 1024px) {
  .board-scroll:has(.board-aside:not(:empty)) .board-extra {
    margin-right: 312px; /* 288px aside + the 24px split gap */
  }
  .board-extra { margin-top: 20px; }
}

/* =====================================================================
   RESULTS
   =====================================================================
   The standings were a seven-column table given the whole width of the
   board: a ten-character name floating in 500px, then six two-character
   numbers spread so far apart that reading one player's row meant
   tracking across the screen. A table's columns should be as wide as
   their contents, not as wide as a seventh of the page.
   ===================================================================== */

/* ---- section heading, with room for an action ------------------------ */
.resbar {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0 2px;
}
.resbar > .eyebrow { margin: 0; }
.resbar-note {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--text-faint);
  margin-left: auto;
  white-space: nowrap;
}
/* Sharing is a once-a-night action. As a full-width slab between the
   table and the log it was the largest control on the page and it broke
   the read from "who won" straight into "what was played". */
.resbar-share {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-left: 4px;
}

@media (min-width: 700px) {
}

/* Left as table cells. display:flex on a td takes it out of table layout,
   which put a seam down the column boundary and stopped the header lining
   up with the rows underneath it. The things in the cell are inline
   already; they only needed to agree on a baseline. */
.standings tbody td { vertical-align: middle; }
.standings .pl-name { font-weight: 600; vertical-align: middle; }
.standings tbody .col-name > * { vertical-align: middle; }
.standings tbody .col-name .streak { margin-left: 9px; }

/* ---- the game log ----------------------------------------------------- */
/* A game, written the way it is said: who won, by what, over whom. The old
   row stacked the two sides with each score trailing the last name, so
   telling the winner from the loser meant comparing two numbers at the
   ends of two different lines. */
.grow {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 12px;
  padding: 10px 14px;
  border-bottom: 1px solid var(--border);
}
.glog > .grow:last-child { border-bottom: 0; }
.gteam {
  min-width: 0;
  font-size: 13.5px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.gwin  { font-weight: 700; color: var(--text); text-align: right; }
.glose { color: var(--text-muted); }
/* The score in the middle, where the net is on a court. */
.gscore {
  flex: none;
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  font-family: var(--mono);
  font-size: 14px;
  padding: 3px 10px;
  border-radius: var(--r-full);
  background: var(--surface-alt);
  white-space: nowrap;
}
.gscore b { font-weight: 700; color: var(--text); }
.gscore .gdash { color: var(--text-faint); }
.gscore .glo { color: var(--text-muted); }
.gcourt {
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--text-faint);
  white-space: nowrap;
}
.gtools { display: inline-flex; gap: 2px; }
.gtools .iconbtn { width: 32px; height: 32px; }
.gtools:empty { display: none; }

/* Search, court filter and pages. The log is every game now, not the
   latest forty, so it needs a way through. */
.glog-block { display: flex; flex-direction: column; gap: 10px; min-width: 0; }
.glog-tools { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.glog-tools > .resbar-note:empty { display: none; }
/* A narrowed roster cannot be reordered: the order sent back is read off
   the rows on screen, and the people filtered out would be shuffled. */
.plist.is-narrowed .draghandle { display: none; }
.glog-search { flex: 1 1 160px; min-width: 0; }
.glog-court { flex: 0 1 auto; max-width: 100%; width: auto; }
.glog-pager {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
}
.glog-range {
  font-family: var(--mono); font-size: 11px; color: var(--text-faint);
  white-space: nowrap;
}
.glog-empty {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: 8px; padding: 14px; font-size: 13.5px; color: var(--text-muted);
}

/* The court's name on its card, for the organiser: a way into every game
   played on it and a place to rename it. Still reads as the heading it
   replaces -- underlined on hover, not dressed as a button. */
.court-no-btn {
  background: none; border: 0; padding: 0; margin: 0;
  color: inherit; cursor: pointer; text-align: left;
  text-decoration: underline dotted transparent; text-underline-offset: 3px;
}
.court-no-btn:hover { text-decoration-color: currentColor; }
.court-no-btn:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; border-radius: 3px; }

/* On a phone the two teams stack and the score sits between them, which
   is the same reading order in one column instead of three. */
@media (max-width: 719px) {
  .grow {
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 4px 10px;
  }
  .gwin   { grid-column: 1; text-align: left; }
  .gscore { grid-column: 2; grid-row: 1 / span 2; }
  .glose  { grid-column: 1; }
  .gcourt { grid-column: 1; }
  .gtools { grid-column: 2; }
}

/* A standings table is read across a row, so it is capped rather than
   stretched: at 1170px the name and the numbers describing it were 600px
   apart. But capping it alone left a third of a 1440px screen holding
   nothing at all, so the width the table gives up goes to the game log
   instead of to white space. */
/* The results block takes whatever the boxed column gives it; the cap
   lives on the box now, in one place, for every view that is a document
   rather than a set of courts. */

@media (min-width: 1200px) {
  .rescols {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 400px;
    gap: 22px;
    align-items: start;
  }
  /* No games yet: no log, so no 380px hole beside the table. */
  .rescols:has(> .rescol-side:empty) { grid-template-columns: minmax(0, 1fr); }
  .rescols > .rescol-side:empty { display: none; }

  /* The standings are long; the log is longer. Pinning it means you can
     read down the table without the log scrolling away above you. */
  .rescol-side { position: sticky; top: 16px; }
  /* Less than the full column: the search above it and the pages below
     it have to stay on screen too -- measured, with the page not yet
     scrolled, at 1440x900 and 1280x720, where the column starts lowest. */
  .rescol-side .glog { max-height: calc(100dvh - 310px); overflow-y: auto; }
}

/* The log's rows lay themselves out for the width they are given, not for
   the width of the window: three across in a full-width panel, stacked in
   a 380px column. Same rule, whichever produced the narrow box. */
.glog { container-type: inline-size; }
@container (max-width: 560px) {
  .grow {
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 3px 10px;
  }
  .gwin   { grid-column: 1; grid-row: 1; text-align: left; }
  .gscore { grid-column: 2; grid-row: 1 / span 2; }
  .glose  { grid-column: 1; grid-row: 2; }
  .gcourt { grid-column: 1; grid-row: 3; }
  .gtools { grid-column: 2; grid-row: 3; justify-self: end; }
}

/* ---- stopping a game that cannot be played out ----------------------
   In the court's header beside re-draw, because that is where the things
   you do TO a court live — and because beside Finish it took half the
   action row for something used once a night at most.

   Red only on contact: at rest it is one of two quiet tools on a navy bar,
   which is the weight it deserves. It always asks first, and the dialog
   names the four players it is about to move. */
/* .btn is a flex row with an 8px gap, which is twice a word-space: split
   across two children "Start game" read as "Start  game". */
.btn:has(> .btn-word) { gap: 5px; }

.court-tool-stop { color: rgba(255, 214, 199, .92); }
.court-tool-stop:hover,
.court-tool-stop:focus-visible {
  /* --win is what .btn-danger already uses for "this undoes something";
     there is no separate danger token to invent. */
  background: var(--win);
  color: #fff;
}
/* On a closed court's pale header the same tool needs ink, not tint. */
.court-head-closed .court-tool-stop { color: var(--win-ink); }

/* =====================================================================
   THE BOARD, TO THE MOCKUP
   =====================================================================
   One dark bar across the top carrying the brand, the night, the session
   nav and the account; a strip of counts under it; courts as cards two
   across; a dark queue rail down the right.

   It replaces a cover banner (220px of photograph), a vertical session
   rail (210px of navigation) and four counts tucked into the header's
   leftover space — three pieces of chrome that between them took a third
   of the screen to say what this bar says in 88px.

   Last in the file on purpose: a media query adds no specificity, so
   these only beat the desktop rules above them by coming after.
   ===================================================================== */

/* ---- the bar --------------------------------------------------------- */
.bh {
  /* Pinned. The board is a page you scroll through mid-game — down to the
     queue, back to the courts — and the header carries the session name,
     the live clock, the code people are still arriving with, and on a wide
     screen the tabs themselves. Losing all of that to a scroll is losing
     the way back.
     30 puts it above the content and below the sheets and dialogs, which
     sit at 50 and 60; it is the tier .board-head already uses. sticky does
     not create a containing block for fixed children, so the tab bar stays
     pinned to the bottom of the phone where it belongs. */
  position: sticky;
  top: 0;
  z-index: 30;
  display: grid;
  /* Three blocks, one line: the mark, the night, and you. Two columns put
     the share and sign-in buttons on a row of their own under the brand,
     left-aligned against nothing, which read as a layout that had come
     apart rather than a header. */
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 10px 14px;
  padding: 12px 16px;
  background: var(--brand);
  color: var(--on-brand);
  isolation: isolate;
}

/* The cover is not gone, it is the ground. A session still looks like its
   own night; it just no longer spends a fifth of the screen on it. */
.bh-cover::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--cover) center/cover no-repeat;
  opacity: .22;
}
.bh-cover::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(90deg, var(--brand) 0%, rgba(1,44,94,.82) 55%, rgba(1,44,94,.94) 100%);
}

.bh-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  color: inherit;
  text-decoration: none;
  flex: none;
}
/* The mark draws the ball itself, so this is only a box to size it in. */
.bh-ball {
  display: grid;
  place-items: center;
  width: 51px;
  height: 34px;
}
/* Both dimensions, at the mark's own 3:2. A box left to size itself from
   an image that is in turn sizing itself from the box is a circle the
   browser breaks by using the file's intrinsic 384px -- which is how a
   32px logo became a 400px ball across the sidebar. object-fit keeps the
   artwork undistorted if the box is ever a hair off the ratio. */
.bh-ball .ps-mark {
  width: 100%; height: 100%; max-width: none; object-fit: contain;
}
.bh-brandtext { display: grid; line-height: 1.15; }
/* Lower case, and tracked in rather than out. The old name was set in
   uppercase with .06em of tracking; this one is a domain, and PICOOL.PH
   is a different wordmark from the one on the ball. */
.bh-brandtext b {
  font-family: var(--display);
  font-weight: 700;
  font-size: 16px;
  letter-spacing: -.01em;
}
.bh-brandtext > span {
  font-size: 10px;
  letter-spacing: .14em;
  text-transform: uppercase;
  opacity: .62;
}

.bh-id { min-width: 0; }
.bh-id h1 {
  margin: 0;
  font-family: var(--display);
  font-weight: 600;
  font-size: 19px;
  letter-spacing: -.01em;
  line-height: 1.2;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.bh-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 4px 14px;
  margin-top: 4px;
  font-size: 12px;
  color: rgba(233,240,248,.72);
}
.bh-fact, .bh-live { display: inline-flex; align-items: center; gap: 5px; }
/* The duration is the one fact on this line that changes while you watch
   it, so it is set in the mono the rest of the board uses for live
   numbers -- and tabular figures, so a tick from 9m to 10m does not shunt
   everything after it sideways. */
.bh-elapsed { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.bh-fact svg { opacity: .7; }
.bh-live { color: var(--action-on-dark); font-weight: 700; }

.bh-right { display: flex; align-items: center; gap: 8px; flex: none; }
.bh-who {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 8px 4px 4px;
  border-radius: var(--r-full);
  background: rgba(255,255,255,.12);
  color: var(--on-brand);
  text-decoration: none;
}
.bh-who:hover { background: rgba(255,255,255,.22); }
.bh-who .av {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border-radius: var(--r-full);
  background: rgba(255,255,255,.18);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .02em;
}
.bh-signin { flex: none; white-space: nowrap; }

/* ---- the session nav, in the bar ------------------------------------- */
/* The phone bar.
   The wordmark goes: the ball is the brand at this size, and the ~110px
   the logotype costs belongs to the session's own name, which was being
   clipped to "Tuesday Night ..." to make room for a logo the viewer is
   already holding. The facts then get the full width on a line of their
   own instead of wrapping three deep inside a squeezed column -- which
   needs the h1 and the meta to be grid items in their own right, hence
   display:contents on the block that holds them. */
@media (max-width: 639px) {
  .bh { padding: 10px 14px; gap: 8px 12px; }
  .bh-brandtext { display: none; }
  .bh-ball { width: 45px; height: 30px; }

  .bh-id { display: contents; }
  .bh-brand  { grid-area: 1 / 1 / 2 / 2; }
  .bh-id h1  { grid-area: 1 / 2 / 2 / 3; font-size: 17px; }
  .bh-right  { grid-area: 1 / 3 / 2 / 4; }
  .bh-meta   { grid-area: 2 / 1 / 3 / 4; margin-top: 0; gap: 3px 11px; }
}

@media (min-width: 1024px) {
  .bh {
    /* The night flexes; the tabs do not. It was the other way round, and
       between 1024 and 1200 the tab column collapsed to nothing while its
       buttons kept their width -- so the tabs printed straight over the
       venue and the session code. A tab bar cannot be squeezed: it either
       fits or it does not, and what gives way instead is the name, which
       has an ellipsis and a title attribute for exactly that. */
    grid-template-columns: auto minmax(0, 1fr) auto auto;
    gap: 0 22px;
    padding: 10px 22px;
  }
  /* A hairline between the brand and the night, as the mockup has it. */
  .bh-id {
    padding-left: 22px;
    border-left: 1px solid rgba(255,255,255,.16);
  }

  /* Every positioning property the phone's bottom bar sets is restated,
     so it cannot leak in as a half-applied header nav. */
  .bh .tabbar {
    position: static;
    width: auto; max-width: none; margin: 0; padding: 0;
    justify-self: end;
    display: flex;
    flex-direction: row;
    gap: 2px;
    background: none;
    border: 0;
    box-shadow: none;
  }
  .bh .tabbar button {
    flex: 0 0 auto;
    flex-direction: column;
    gap: 3px;
    min-height: 0;
    padding: 6px 12px 7px;
    margin: 0;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: .01em;
    color: rgba(233,240,248,.66);
    background: none;
    border: 0;
    border-bottom: 2.5px solid transparent;
    border-radius: 0;
  }
  .bh .tabbar button:hover { color: #fff; }
  .bh .tabbar button[aria-current="page"] {
    color: #fff;
    border-bottom-color: var(--action);
  }
  .bh .tabbar .tab-right { margin-left: 10px; }
  .bh .tab-badge { transform: translate(13px, -9px); }

  /* The page below the bar. .board-cols is gone, so the padding that used
     to belong to it lives here. */
  .board-scroll { padding: 18px 22px 48px; }
}

/* The code is read aloud courtside; on the dark bar it needs its own
   contrast rather than the header's. */
.bh .code-chip {
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: .16em;
  padding: 2px 9px;
  border-radius: var(--r-sm);
  background: rgba(255,255,255,.14);
  color: #fff;
}
.bh .code-chip:hover { background: rgba(255,255,255,.26); }

/* ---- the strip of counts --------------------------------------------- */
/* One card, not two. The counts and the next game are the same band of
   information; as separate bordered cards with a gap between them they
   read as two unrelated panels. */
.statstrip {
  display: grid;
  gap: 0;
  margin-bottom: 20px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  box-shadow: var(--shadow);
  overflow: hidden;
}
.stiles {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 2px;
  padding: 10px 4px;
}
/* Centred in its share of the row, and the share is capped: packed left,
   each tile was a disc and a two-word label followed by ~200px of nothing,
   five times over, with the dividers fencing off five mostly-empty boxes.
   Past the cap the set stops spreading and centres in the strip instead. */
.stile {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 4px 10px;
  min-width: 0;
  flex: 1 1 150px;
  max-width: 230px;
}
.stile + .stile { border-left: 1px solid var(--border); }
.stile-n {
  flex: none;
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: var(--r-full);
  font-family: var(--display);
  font-weight: 600;
  font-size: 17px;
}
.stile-l {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text-muted);
  line-height: 1.2;
  min-width: 0;
}
/* Status colour, not decoration: green is a game happening, blue a court
   that could take one, grey a court that cannot. */
.stile-neutral { background: var(--surface-alt); color: var(--text); }
.stile-live    { background: var(--action-soft); color: var(--action-ink); }
.stile-ready   { background: var(--accent-soft); color: var(--accent); }
.stile-off     { background: var(--surface-alt); color: var(--text-faint); }
.stile-wait    { background: var(--accent-soft); color: var(--accent); }
.stile-zero .stile-n { opacity: .55; }
.stile-zero .stile-l { color: var(--text-faint); }

/* The board's live pill -- ON COURT, beside a player's name. The app-wide
   .chip-live it overrides carries the same lime and the same ink; what
   changes here is the type, so a pill sitting in a dense list of names
   reads as a marker on the row rather than as another word in it. */
.chip-live {
  font-family: var(--mono);
  font-size: 9.5px;
  font-weight: 500;
  letter-spacing: .1em;
  padding: 2px 7px;
  border-radius: var(--r-full);
  background: var(--action);
  color: var(--on-action);
}

.ratepill {
  flex: none;
  font-family: var(--mono);
  font-size: 10px;
  padding: 1px 6px;
  border-radius: var(--r-full);
  background: var(--surface-alt);
  color: var(--text-muted);
}

/* ---- the court card head --------------------------------------------- */
.court-ico {
  flex: none;
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: var(--r-sm);
  background: rgba(255,255,255,.14);
}
.court-head-closed .court-ico { background: rgba(27,42,51,.08); }

/* One word for what this court is doing, in the colour that word means. */
.cstate {
  flex: none;
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: .09em;
  text-transform: uppercase;
  padding: 3px 8px;
  border-radius: var(--r-full);
}
.cstate-live   { background: var(--action); color: var(--on-action); }
.cstate-ready  { background: var(--accent-soft); color: var(--accent); }
.cstate-open   { background: rgba(255,255,255,.18); color: #fff; }
.cstate-closed { background: rgba(27,42,51,.10); color: var(--text-faint); }

/* ONE auto margin in this row, not two.
   .court-count and .court-meta both carried margin-left:auto, and flex
   splits the free space between every auto margin it finds — so the
   re-draw button came to rest about 40% across the header, touching
   nothing and belonging to nothing. The clock and the tools travel with
   the count, at the right. */
.court-count {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin-left: 12px;
  font-family: var(--mono);
  font-size: 11px;
  opacity: .78;
}
.court-head .court-meta { margin-left: auto; }

/* ---- the court card body, two abreast -------------------------------- */
@media (min-width: 1024px) {
  /* Names across the top, the two scores meeting in the middle beneath
     them — the shape the mockup draws, and the shape four people are
     actually standing in. display:contents lets the two sides' children be
     placed on one grid without touching the markup board.js builds. */
  .courtgrid.view-cards .sides {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    row-gap: 14px;
    column-gap: 26px;
    padding: 8px 4px 4px;
  }
  .courtgrid.view-cards .side { display: contents; }
  .courtgrid.view-cards .side:first-child .side-names { grid-area: 1 / 1; text-align: left; }
  .courtgrid.view-cards .side:last-child  .side-names { grid-area: 1 / 2; text-align: right; }
  .courtgrid.view-cards .side:first-child .scorer { grid-area: 2 / 1; justify-self: end; }
  .courtgrid.view-cards .side:last-child  .scorer { grid-area: 2 / 2; justify-self: start; }

  /* VS on the halfway line, between the two pairs. */
  .courtgrid.view-cards .playing .sides::before {
    content: "VS";
    position: absolute;
    left: 50%;
    top: 14px;
    transform: translateX(-50%);
    display: grid;
    place-items: center;
    width: 30px;
    height: 30px;
    border: 0;
    border-radius: var(--r-full);
    background: var(--surface);
    color: var(--text-faint);
    font-family: var(--display);
    font-weight: 600;
    font-size: 10.5px;
    letter-spacing: .06em;
    z-index: 2;
  }
  .courtgrid.view-cards .playing .sides::after { display: none; }

  .courtgrid.view-cards .side-names { min-height: 0; gap: 8px; }
  .courtgrid.view-cards .pname {
    display: flex;
    align-items: center;
    gap: 9px;
    font-size: 14px;
    line-height: 1.25;
  }
  .courtgrid.view-cards .side:last-child .pname { flex-direction: row-reverse; }
  /* One line. Wrapped, "Bem Ocampo" became "Bem / Ocampo" stacked over a
     rating pill, which reads as a layout that broke rather than as a name
     too long for its column. */
  .courtgrid.view-cards .pn {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  /* The run streak is on the Players tab, in the Results table and in the
     rail's leaderboard. On the court it was a fourth chip competing with
     the name for 218px. */
  .courtgrid.view-cards .pname .streak { display: none; }
  /* Replace sits on the outside, away from the VS badge on the halfway
     line — which it was landing underneath. */
  .courtgrid.view-cards .pname .swapbtn { order: -1; }
  .courtgrid.view-cards .pname .lvl,
  .courtgrid.view-cards .pname .streak,
  .courtgrid.view-cards .pname .swapbtn { flex: none; margin: 0; }
  /* The rating as a pill, matching every other place a rating appears. */
  .courtgrid.view-cards .pname .lvl {
    font-size: 10px;
    padding: 1px 6px;
    border-radius: var(--r-full);
    background: var(--surface-alt);
    color: var(--text-muted);
  }

  .courtgrid.view-cards .scorer {
    grid-template-columns: auto auto auto;
    grid-template-areas: "minus score plus";
    gap: 10px;
  }
  .courtgrid.view-cards .score { font-size: clamp(38px, 3.4vw, 52px); width: 1.7em; min-width: 0; }
  .courtgrid.view-cards .stepper { width: 38px; height: 38px; font-size: 19px; }

  .courtgrid.view-cards .court-actions {
    margin: 14px 0 0;
    max-width: none;
    gap: 10px;
  }
  .courtgrid.view-cards .court-actions .btn { flex: 1; }
}

/* The Rows view keeps its density: no icon tile, no count. It keeps the
   faces now -- they carry the replace button (see .pav at the end of this
   file). Scoped to the widths where there are rows at all. */
@media (min-width: 1024px) {
  .courtgrid.view-rows .court-ico,
  .courtgrid.view-rows .court-count { display: none; }
}
@media (min-width: 1024px) {
  .courtgrid.view-rows .cstate { font-size: 9.5px; padding: 3px 8px; letter-spacing: .06em; }
  .courtgrid.view-rows .court-head .court-meta { margin-left: 0; }
}

/* ---- the queue rail --------------------------------------------------- */
/* One dark panel instead of four pale cards with an eyebrow caption
   between each. The captions were doing a panel's job — saying "these
   belong together" — and spending a line each to do it. */
.qrail {
  background: var(--brand);
  color: var(--on-brand);
  border-radius: var(--r-lg);
  padding: 14px 14px 16px;
  box-shadow: var(--shadow);
}
.qrail-head {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 2px 2px 12px;
}
.qrail-head h3 {
  margin: 0;
  flex: 1;
  min-width: 0;
  font-family: var(--display);
  font-weight: 600;
  font-size: 15px;
  letter-spacing: -.01em;
}
.qrail-ico {
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  border-radius: var(--r-sm);
  background: rgba(255,255,255,.13);
}
.qrail-more {
  flex: none;
  font-size: 11.5px;
  font-weight: 700;
  color: var(--accent-on-dark);
  background: none;
  padding: 2px 0;
}
.qrail-more:hover { color: #fff; text-decoration: underline; }

.qrail-sub {
  display: flex;
  align-items: center;
  gap: 7px;
  margin: 16px 2px 8px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: rgba(233,240,248,.62);
}
.qrail-sub > span:nth-child(2),
.qrail-sub > span:only-child { flex: 1; min-width: 0; }
.qrail-trophy { display: inline-grid; place-items: center; color: var(--action-on-dark); }

.qrail-list {
  background: rgba(255,255,255,.055);
  border-radius: var(--r-md);
  overflow: hidden;
}
.qrail-row {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 9px 11px;
  font-size: 13.5px;
}
.qrail-row + .qrail-row { border-top: 1px solid rgba(255,255,255,.07); }
/* Whoever is about to be called gets the lime edge, so "who's next" is
   answerable without reading a single name. */
.qrail-row.is-next { background: rgba(190,211,30,.14); box-shadow: inset 3px 0 0 var(--action); }
.qrail-pos {
  flex: none;
  width: 14px;
  font-family: var(--mono);
  font-size: 11px;
  color: rgba(233,240,248,.5);
  text-align: right;
}
.qrail-name {
  flex: 1;
  min-width: 0;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.qrail-games, .qrail-wl {
  flex: none;
  font-family: var(--mono);
  font-size: 11px;
  color: rgba(233,240,248,.55);
}
/* How many games short of the table, on the dark rail. Quiet by default,
   lime at one away -- the same rule as the Results tab, in less room. */
.qrail-gap {
  flex: none;
  font-family: var(--mono);
  font-size: 10.5px;
  padding: 1px 6px;
  border-radius: var(--r-full);
  background: rgba(255,255,255,.1);
  color: rgba(233,240,248,.7);
}
.qrail-next {
  flex: none;
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  padding: 2px 7px;
  border-radius: var(--r-full);
  background: var(--action);
  color: var(--on-action);
}
.qrail-empty { margin: 0; padding: 16px; text-align: center; font-size: 13px; color: rgba(233,240,248,.6); }
/* The rating pill reads on white by default; on navy it needs its own. */
.qrail .ratepill { background: rgba(255,255,255,.12); color: rgba(233,240,248,.8); }
.qrail .streak { background: rgba(255,255,255,.14); color: var(--on-brand); }

/* The next-game panel is already a dark card of its own; inside the rail it
   is a section of this one. */
.qrail .nextup-rail {
  margin: 0;
  border-radius: var(--r-md);
  background: rgba(255,255,255,.07);
  border: 1px solid rgba(255,255,255,.1);
}

/* ---- queue cards: the line as games ---------------------------------- */
/* Light on the phone's courts tab; restated for the navy rail below. */
.qcards { display: grid; gap: 8px; }
/* The courts tab's copy is the phone's: from 768px the rail carries the same
   cards. Restated here because the display above comes after the general
   .mobile-only rule at the same weight, and beat it. */
@media (min-width: 768px) { .qcards.mobile-only { display: none; } }
.qcards-head {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  margin: 0 2px;
}
.qcards-head > .eyebrow { margin: 0; }
.qcards-count {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
  color: var(--text-muted);
}
.qcards-select {
  font: inherit; font-size: 12px; letter-spacing: 0; text-transform: none;
  padding: 3px 6px; border-radius: var(--r-sm);
  border: 1px solid var(--border); background: var(--surface); color: var(--text);
}
.qcard {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-md); padding: 10px 12px;
}
.qcard-top { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.qcard-top > .eyebrow { margin: 0; }
.qcard-note {
  font-family: var(--mono); font-size: 10.5px; color: var(--text-faint);
  text-transform: uppercase; letter-spacing: .06em;
}
/* Two by two: four names read as a game, not as a list. */
.qcard-names {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 6px 10px; margin-top: 8px;
}
.qcard-who { display: flex; align-items: center; gap: 7px; min-width: 0; }
.qcard-name {
  flex: 1; min-width: 0; font-size: 13.5px; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Games so far, beside each name: on the cards and on the Queue 1 panel. */
.qcard-games {
  flex: none; font-family: var(--mono); font-size: 10.5px; color: var(--text-muted);
}
.qcards-dark .qcard-games { color: rgba(233,240,248,.6); }
.nextup-games {
  margin-left: 4px; font-family: var(--mono); font-size: 10.5px; font-weight: 400;
  color: var(--on-brand); opacity: .7; vertical-align: 1px;
}

/* Who on the card has partnered before. Quiet when nobody has; the lime
   (or orange, on white) edge only when there is a repeat to weigh. */
.qcard-teamed, .nextup-teamed { margin: 8px 0 0; font-size: 11.5px; line-height: 1.35; }
.qcard-teamed { color: var(--text-faint); }
.qcard-teamed.is-repeat { color: var(--text-muted); }
.qcard-teamed.is-repeat > span:first-child { font-weight: 700; color: var(--warn, #b45309); }
.qcards-dark .qcard-teamed { color: rgba(233,240,248,.5); }
.qcards-dark .qcard-teamed.is-repeat { color: rgba(233,240,248,.8); }
.qcards-dark .qcard-teamed.is-repeat > span:first-child { color: var(--action-on-dark); }
.nextup-teamed { color: var(--on-brand); opacity: .7; }
.nextup-teamed.is-repeat { opacity: .9; }
.nextup-teamed.is-repeat > span:first-child { font-weight: 700; color: var(--action-on-dark); }

/* A card the organiser set by hand, and the way back to the draw. */
.qcard.is-held { border-style: dashed; border-color: var(--accent); }
.qcards-dark .qcard.is-held { border-color: var(--accent-on-dark); }
.qcard-held { display: inline-flex; align-items: baseline; gap: 8px; }
.qcard-undo {
  background: none; padding: 0; font-size: 11px; font-weight: 700;
  color: var(--accent); text-decoration: underline;
}
.qcards-dark .qcard-undo { color: var(--accent-on-dark); }
/* Names that open the swap, on the navy rail. */
.qcards-dark .qcard .namebtn { color: #fff; border-bottom-color: rgba(255,255,255,.35); max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: inline-block; vertical-align: bottom; }
.qcard .namebtn { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  display: inline-block; vertical-align: bottom; }

/* Each name's place in the line, on the Queue tab. */
.qcard-pos {
  flex: none; font-family: var(--mono); font-size: 10.5px; color: var(--text-faint);
}
/* The card you are in, found without reading every name. */
.qcard.is-you { border-color: var(--accent); box-shadow: inset 3px 0 0 var(--accent); }
.qcards-dark .qcard.is-you { border-color: var(--accent-on-dark); box-shadow: inset 3px 0 0 var(--accent-on-dark); }

/* The Queue tab: Queue 1 across the top, the rest as many across as fit. */
.qcards-all { grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 10px; }
/* One name a row here: each card is a column of the grid, and two names a
   row beside a place and a rating cut every one of them to "Play…". */
.qcards-all .qcard-names { grid-template-columns: minmax(0, 1fr); }
.qcards-all > .nextup,
.qcards-all > .qcards-empty { grid-column: 1 / -1; }

.qcards-empty, .qcards-note { margin: 0; font-size: 12.5px; color: var(--text-muted); }
.qcards-empty { padding: 12px; text-align: center; }
.qcards-more {
  justify-self: start; background: none; padding: 2px 2px;
  font-size: 12px; font-weight: 700; color: var(--accent);
}
.qcards-more:hover { text-decoration: underline; }

/* Inside the navy rail. */
.qcards-dark .qcards-head > .eyebrow,
.qcards-dark .qcards-count { color: rgba(233,240,248,.62); }
.qcards-dark .qcards-select {
  background: rgba(255,255,255,.1); border-color: rgba(255,255,255,.18); color: #fff;
}
.qcards-dark .qcards-select option { color: #0b1b2e; }
.qcards-dark .qcard { background: rgba(255,255,255,.055); border-color: rgba(255,255,255,.08); }
.qcards-dark .qcard-top > .eyebrow { color: rgba(233,240,248,.6); }
.qcards-dark .qcard-note { color: rgba(233,240,248,.5); }
.qcards-dark .qcard-name { color: #fff; }
/* The rail is ~270px: two names a row and a rating pill each left
   "Play…" for every player. The name is what is being read here; the
   ratings are on the Queue 1 card, where the split is actually made. */
.qcards-dark .qcard .ratepill { display: none; }
.qcards-dark .qcard-names { gap: 6px 8px; }
.qcards-dark .qcards-empty,
.qcards-dark .qcards-note { color: rgba(233,240,248,.6); }
.qcards-dark .qcards-more { color: var(--accent-on-dark); }
.qcards-dark .qcards-more:hover { color: #fff; }

/* ---- what just happened ---------------------------------------------- */
.feed {
  margin-top: 14px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  box-shadow: var(--shadow);
  overflow: hidden;
}
.feed-head {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 12px 14px;
  border-bottom: 1px solid var(--border);
}
.feed-head h3 {
  margin: 0;
  flex: 1;
  font-family: var(--display);
  font-weight: 600;
  font-size: 14px;
}
.feed-head .qrail-more { color: var(--accent); }
.feed-ico {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: var(--r-sm);
  background: var(--action-soft);
  color: var(--action-ink);
}
.feed-row { display: flex; gap: 10px; padding: 11px 14px; }
.feed-row + .feed-row { border-top: 1px solid var(--border); }
.feed-dot {
  flex: none;
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  margin-top: 1px;
  border-radius: var(--r-full);
  background: var(--action-soft);
  color: var(--action-ink);
}
.feed-body { display: grid; gap: 2px; min-width: 0; }
.feed-body b { font-size: 12.5px; font-weight: 700; }
.feed-detail {
  font-size: 12.5px;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
}
.feed-score { font-family: var(--mono); font-size: 11.5px; color: var(--text-faint); }

/* ---- a closed court, said properly ----------------------------------- */
/* "Not in use." in 12px grey, centred in a panel the size of a court, was
   indistinguishable from something that had failed to load. */
@media (min-width: 1024px) {
  .courtgrid.view-cards > .court > .court-body.court-empty {
    gap: 10px;
    padding: 30px 20px;
  }
  .courtgrid.view-cards .court-empty .hint { font-size: 13.5px; }
}

/* ---- gaps the VS badge needs ----------------------------------------- */
@media (min-width: 1024px) {
  .courtgrid.view-cards .sides { column-gap: 52px; }
}

/* ---- the next-game panel, inside the dark rail ----------------------- */
/* It was built as a pale card for a pale rail. Inside the navy panel its
   own borders, its lime caption and its dark text all had to be restated
   for the ground it now sits on. */
.qrail .nextup-rail .eyebrow { color: rgba(233,240,248,.6); }
.qrail .nextup-rail .nextup-name,
.qrail .nextup-rail .nextup-hold { color: #fff; }
.qrail .nextup-rail .nextup-rate,
.qrail .nextup-rail .nextup-meter-l { color: rgba(233,240,248,.6); }
.qrail .nextup-rail .vs { color: rgba(233,240,248,.45); }
.qrail .nextup-rail .nextup-meter { background: rgba(255,255,255,.14); }

/* ---- a court that is closed ------------------------------------------ */
.court-lock {
  display: grid;
  place-items: center;
  width: 52px;
  height: 52px;
  border-radius: var(--r-full);
  background: var(--surface-alt);
  color: var(--text-faint);
}
.court-closed-t {
  font-family: var(--display);
  font-weight: 600;
  font-size: 15.5px;
  color: var(--text-muted);
}
/* In a row the lock and the sentence are a strip, not a panel. */
@media (min-width: 1024px) {
  .courtgrid.view-rows .court-lock { width: 30px; height: 30px; }
  .courtgrid.view-rows .court-lock svg { width: 15px; height: 15px; }
  .courtgrid.view-rows .court-closed-t { font-size: 13.5px; }
}

/* ---- the row, with the card's new head ------------------------------- */
/* The head gained a status chip, an icon tile and a player count. In a
   column-flex head each of those is its own line, so a row that was 65px
   became 100px of navy holding three words. Wrapped instead: the court
   number takes the first line, the chip and the clock share the second. */
@media (min-width: 1024px) {
  .courtgrid.view-rows > .court > .court-head {
    flex-flow: row wrap;
    align-content: center;
    align-items: center;
    row-gap: 4px;
    column-gap: 5px;
  }
  .courtgrid.view-rows > .court > .court-head .court-no { flex: 1 0 100%; }

  /* A closed court is a strip too. Its body carries .court-empty without
     .court-body, so the row rules written for an open court never reached
     it — and the lock, the heading, the sentence and the button stacked
     into a panel four times the height of the rows either side. */
  .courtgrid.view-rows > .court > .court-empty {
    flex-direction: row;
    align-items: center;
    justify-content: flex-start;
    gap: 12px;
    padding: 10px 14px;
    text-align: left;
  }
  .courtgrid.view-rows > .court > .court-empty .hint { margin: 0; flex: 1; }
  .courtgrid.view-rows > .court > .court-empty .btn { flex: none; width: auto; }
  .courtgrid.view-rows > .court > .court-empty .court-lock { width: 28px; height: 28px; }
  .courtgrid.view-rows > .court > .court-empty .court-lock svg { width: 14px; height: 14px; }
}

/* ---- rank as a column of numbers -------------------------------------
   This has been a trophy glyph and then a filled disc; both were the same
   mistake. A ranking is an ordered list and what makes one readable is the
   numbers lining up — a disc for the top three and a bare numeral for
   everybody else changed the column's shape at row four, and next to the
   player's face it put two circles of almost the same size in every row.

   One numeral per row, right-aligned, under a header that names it. The
   top three are coloured, not shaped. */
/* app.css aligns every td right and every FIRST td left. Adding a rank
   column in front of the name meant the name cell stopped being first and
   silently inherited right-alignment — the header, the face and the name
   all slid to the middle of a 274px column. Both columns say what they
   want by name now, rather than by their position in the row. */
.standings .col-rank {
  width: 1%;
  white-space: nowrap;
  text-align: right;
  padding-left: 18px;
  padding-right: 6px;
}
.standings .col-name { text-align: left; padding-left: 10px; }
.rk {
  font-family: var(--mono);
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
  color: var(--text-faint);
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  gap: 4px;
}
/* Only the top three carry a cup, and the column is sized for it, so the
   numerals below still line up under each other instead of stepping in at
   row four. */
/* max-width and flex-shrink both have to be turned off here.
   app.css sets `img, svg { max-width: 100% }`, and a percentage max-width
   inside a parent that is itself sizing to its content has nothing to
   resolve against: the 19px trophy collapsed to 3.5px and read as a speck
   of dust beside the numeral. The size is stated here instead, and the
   stroke is heavier than the board's other icons because a 2px stroke on
   a 24-unit viewBox is a hairline once it is this small. */
.rk-cup { display: inline-grid; place-items: center; flex: none; }
.rk-cup svg {
  width: 17px; height: 17px; max-width: none;
  stroke-width: 2.4;
}
.rk-1 { color: #A57C12; font-weight: 500; }
.rk-2 { color: #6E7987; font-weight: 500; }
.rk-3 { color: #A0623A; font-weight: 500; }

/* The row carries the same colour as an edge, so the top three are
   findable without the numeral having to be a different shape. */
.standings tbody tr.rank-top > td:first-child { box-shadow: inset 3px 0 0 currentColor; }
.standings tbody tr.rank-1 > td:first-child { color: #C79A2E; }
.standings tbody tr.rank-2 > td:first-child { color: #97A3B2; }
.standings tbody tr.rank-3 > td:first-child { color: #B4713C; }

/* ---- the standings table, with the slack put to work ------------------
   The Player column was 484px holding 21px of content. A face and a wider
   win bar take ~110px of that back and both earn it: the face matches
   every other list of people on this board, and a 46px bar cannot show the
   difference between 67% and 75%. */
/* The numbers get room to breathe rather than huddling at the right-hand
   end of the table while the name column holds 300px of nothing. Each is
   still sized to its content — this is a floor, not a fixed width. */
.standings .num { min-width: 62px; }
.standings .col-win { min-width: 200px; }
.winbar { width: 108px; }
/* .avatar is display:grid — block-level, so in a table cell (which is a
   block formatting context, not a flex row) it takes a line of its own and
   puts the face above the name. Inline-grid keeps the disc and puts it
   back on the baseline. */
.standings tbody .col-name .avatar {
  display: inline-grid;
  vertical-align: middle;
  margin-right: 9px;
}

/* The same table on a phone.
   Those floors add up: five numbers at 62px, a 200px win column and a
   108px bar come to ~630px of table inside a 385px screen. A table that
   cannot fit does not shrink -- it widens the page, and the whole board
   then scrolled sideways with the sticky header stretched across it, which
   is how a header ends up 620px wide on a 400px phone.
   The bar is the column to lose: it is a picture of the number printed
   next to it. The floors come off with it, and the card keeps a scroll of
   its own so a long name can never widen the page again. */
@media (max-width: 719px) {
  .standings .num     { min-width: 0; }
  .standings .col-win { min-width: 0; }
  .standings .winbar  { display: none; }

  /* Nine columns do not fit a phone, and strength is the one a player
     reads last -- unless it is what the night is being ranked by, and
     then it is the one they came for. */
  .standings:not(.by-str) .col-str { display: none; }

  /* Six numeric columns at 8px a side is 96px of padding on a 390px
     screen, and it came out of the only column that needed it: the name
     wrapped under its own face, three rows running. */
  .standings th, .standings td { padding-left: 5px; padding-right: 5px; }
  .standings th:first-child, .standings td:first-child { padding-left: 12px; }
  .standings th:last-child,  .standings td:last-child  { padding-right: 12px; }
  /* A smaller face, so the name beside it fits on the line rather than
     under it. The podium above is where somebody is recognised; down here
     the disc is a colour to follow down the column. */
  .standings tbody .col-name .avatar {
    width: 22px; height: 22px; font-size: 9.5px; margin-right: 6px;
  }
  .standings .col-name { padding-left: 6px; }
}

/* The winner's row, lit just enough to find. The full-width wash competed
   with the podium directly above it, which is the thing doing the
   celebrating. */
.standings tbody tr.rank-1 { background: linear-gradient(90deg, #FDF8EC, transparent 42%); }

/* =====================================================================
   ONE BOX FOR EVERY VIEW THAT IS A DOCUMENT
   =====================================================================
   Courts expands: six courts and a queue rail earn the whole screen. The
   Queue, the roster, the rules, the standings, the log and the settings
   are documents, and a document stretched across 1900px is unreadable —
   it is how the Player column came to be 484px holding 21px of content.

   width:100% is load-bearing and not redundant with max-width.
   .board-scroll is a COLUMN flex container, and `margin-inline: auto` on a
   flex item cancels the cross-axis stretch: the item falls back to
   fit-content. With container-type on the groups inside it, their contents
   contribute nothing to intrinsic width either, and the settings form
   collapsed to 421px — two columns of 202px. An explicit width makes the
   item definite before the auto margins centre it.
   ===================================================================== */
@media (min-width: 1024px) {
  .board-scroll.boxed > * {
    width: 100%;
    max-width: 1240px;
    margin-inline: auto;
  }
}

/* The three sizes the board actually uses. avatar-xs was referenced by the
   next-game strip and the queue rail before it existed, so both were
   drawing the 34px default. */
/* ---- "keep your record" -------------------------------------------- */
/* Shown only to a signed-out visitor who has played. An offer, so it is
   tinted with the action colour rather than bordered like a warning —
   nothing is wrong, there is something to gain. Sits above whichever tab
   is open, because the moment it is trading on is the night itself. */
.claimoffer {
  display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 3px solid var(--action);
  border-radius: var(--r-md);
  box-shadow: var(--shadow);
  padding: 13px 16px;
  margin-bottom: 16px;
}
.claimoffer-say { flex: 1 1 340px; min-width: 0; }
.claimoffer-say strong { display: block; font-size: 14.5px; letter-spacing: -.01em; }
.claimoffer-say .hint { display: block; margin-top: 3px; }
.claimoffer-do { display: flex; gap: 8px; flex: none; flex-wrap: wrap; }

.avatar-xs { width: 22px; height: 22px; font-size: 9px; }
.avatar-md { width: 42px; height: 42px; font-size: 15px; }

/* =====================================================================
   THE COURT CARD
   =====================================================================
   Four things were wrong with it, and three of them were about hierarchy:
   the score — the only number anybody is looking at — was the faintest
   thing on the card, while the draw-quality band was the loudest; the
   green appeared twice, as a body tint and again as the court inside it;
   and the card stood 367px tall for about 280px of content.
   ===================================================================== */
@media (min-width: 1024px) {
  /* One green, and it is the court. The body tint underneath it made the
     card three stacked surfaces — navy, green, green — with the bordered
     play area reading as a panel floating on a panel. */
  .courtgrid.view-cards > .court > .court-body {
    background: var(--surface);
    padding: 12px 13px 13px;
    min-height: 0;
    /* Top, not centre. The grid stretches every card to the tallest, and
       centring then floated each card's contents in whatever slack it had
       left — so the court with the "or just score the first point" line
       under its button sat 12px higher than the one without, and no two
       balance strips, courts or buttons lined up across the row. */
    justify-content: flex-start;
  }
  .courtgrid.view-cards .playing .sides {
    background: var(--court-surface);
    border-color: rgba(255, 255, 255, .85);
    padding: 11px 12px 12px;
  }

  /* The draw quality is a footnote about the match, not a banner across
     it. As a full-width filled band in 11px caps it was the first thing
     the eye landed on, above four names and a score. */
  .courtgrid.view-cards .balance {
    margin-bottom: 12px;
    padding: 0 2px;
    background: none;
    gap: 8px;
  }
  .courtgrid.view-cards .balance-num { font-size: 11px; }
  .courtgrid.view-cards .balance-txt { font-size: 9.5px; opacity: .75; }
  /* The bar runs the width of the card. Capped at 180px it stopped short
     of the card's edge with nothing after it, which read as a bar that had
     failed to load rather than as a measure of anything. */
  .courtgrid.view-cards .balance-meter { height: 3px; max-width: none; }

  /* The hero, at last. It is the one number on this card anybody is
     reading from the far side of a net. */
  .courtgrid.view-cards .score { font-size: clamp(46px, 4vw, 60px); width: 1.6em; }
  .courtgrid.view-cards .scorer { gap: 10px; }
  .courtgrid.view-cards .stepper { width: 34px; height: 34px; font-size: 18px; }
  .courtgrid.view-cards .sides { row-gap: 8px; }
  .courtgrid.view-cards .side-names { gap: 6px; }
  .courtgrid.view-cards .court-actions { margin-top: 12px; }

  /* A court nobody has served on yet.
     Doto draws zero as an open ring of dots, and at 44% opacity that is a
     smudge — four of them across two courts read as a board that had
     failed to load rather than as two games waiting to start. At full
     strength in a muted ink it reads as what it is: nil. */
  .courtgrid.view-cards .score-idle { opacity: 1; color: var(--text-faint); }

  /* Said once under the first court is enough; under every court it is the
     same sentence three times, in the space the card was using to breathe.

     Not the "Scoring opens once the game starts" note. It explains why THAT
     court has no score buttons, and "once, under the first court" meant
     nowhere at all whenever court 1 was already playing and court 2 was
     waiting to start. */
  .courtgrid.view-cards > .court:not(:first-child) .court-note:not(.court-note-start) { display: none; }
  .courtgrid.view-cards .court-note { margin-top: 7px; font-size: 11.5px; }
}

/* =====================================================================
   SCOREBOARD MODE  (?tv=<court>)
   =====================================================================
   One court, on a screen across the room. Everything else on the page is
   navigation for somebody holding a device, and none of it applies to a
   television on a wall — so none of it is here.

   It rides on the board's own payload and polling rather than being a
   second page: a separate implementation of "what is the score" is a
   second thing that can disagree with the board.
   ===================================================================== */
.tv-mode { background: var(--brand-deep); }

/* Everything the scoreboard is not.
   This used to be a list of class names to hide, which is fragile in the
   one direction that matters: anything added to the board later appears
   on the television. That is how a "Check-in is closed" card ended up on
   a fifty-inch screen at the side of a court — it was a plain .card, so
   no rule covered it.
   Hidden by position instead. In tv mode the page is the scoreboard and
   its ancestors, and nothing else, whatever it is called. */
.tv-mode .board        > *:not(.board-scroll),
.tv-mode .board-scroll > *:not(#board),
.tv-mode #board        > *:not(.board-split),
.tv-mode .board-split  > *:not(#board-body) { display: none !important; }

/* Kept as well, for anything that lives outside .board entirely. */
.tv-mode #checkin,
.tv-mode .flash { display: none !important; }

.tv-mode .board,
.tv-mode .board-scroll,
.tv-mode .board-split,
.tv-mode #board,
.tv-mode #board-body {
  display: block;
  max-width: none;
  width: auto;
  margin: 0;
  padding: 0;
  height: 100%;
}
.tv-mode, .tv-mode body { height: 100%; }

.tv {
  /* height, not min-height: a television cannot scroll, so anything past
     the fold is simply lost — which is how the bottom row of the standings
     ended up cut in half. Fixed to the screen, with the court absorbing
     whatever is left over. */
  height: 100dvh;
  overflow: hidden;
  /* Pinned to the viewport. Without this the board was laying out wider
     than the screen on a phone — a flex child that will not shrink widens
     its container, the container widened the page, and the whole
     scoreboard then sat to the right of where it looked centred. Every
     row below carries min-width:0 for the same reason: the default
     min-width:auto on a flex item is its content, and a name that is not
     allowed to shrink is a page that is not allowed to shrink. */
  width: 100%;
  max-width: 100vw;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: clamp(16px, 3vh, 40px);
  padding: clamp(18px, 4vh, 54px) clamp(20px, 4vw, 72px);
  background: var(--brand-deep);
  color: var(--on-brand);
}

.tv-head {
  display: flex;
  align-items: baseline;
  gap: clamp(12px, 2vw, 28px);
  flex: none;
  min-width: 0;
}
.tv-court {
  font-family: var(--display);
  font-weight: 600;
  font-size: clamp(20px, 2.6vw, 44px);
  letter-spacing: -.01em;
}
.tv-clock {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-family: var(--mono);
  font-size: clamp(15px, 1.7vw, 30px);
  color: var(--action-on-dark);
}
.tv-clock-idle { color: rgba(233, 240, 248, .55); }
.tv-sess {
  margin-left: auto;
  font-size: clamp(12px, 1.2vw, 20px);
  color: rgba(233, 240, 248, .5);
  text-align: right;
}

/* The court: two sides, the net between them. */
.tv-board {
  flex: 1;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: clamp(16px, 3vw, 56px);
  min-height: 0;
}
.tv-net {
  align-self: stretch;
  width: 2px;
  background: linear-gradient(rgba(255,255,255,.04), rgba(255,255,255,.22), rgba(255,255,255,.04));
}
/* Names outside, scores meeting at the net — the shape of every
   scoreboard ever hung on a wall, and the reason the two numbers can be
   compared without moving your eyes across the room. */
.tv-side {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: clamp(14px, 2.5vw, 44px);
  min-width: 0;
}
.tv-names { min-width: 0; display: grid; gap: clamp(4px, .8vh, 12px); }
.tv-side-r { grid-template-columns: auto minmax(0, 1fr); text-align: right; }
.tv-side-r .tv-names { order: 2; }
.tv-side-r .tv-score { order: 1; }

.tv-name {
  font-family: var(--display);
  font-weight: 600;
  font-size: clamp(19px, 2.5vw, 46px);
  line-height: 1.1;
  letter-spacing: -.015em;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tv-rate {
  font-family: var(--mono);
  font-weight: 400;
  font-size: .42em;
  margin-left: .5em;
  color: rgba(233, 240, 248, .45);
}

/* The number somebody is reading from the far wall.
   Sized well past what the figure suggests, because Doto is a dot-matrix
   face and its digits sit small inside their em box — at 12vw the painted
   glyph was about two thirds the height of the type size, which on a
   television across a hall is the difference between a score and a smudge.
   The names give up a little width for it: on a scoreboard beside a court
   the number is the point and the names are the label. */
.tv-score {
  font-family: var(--dot);
  font-weight: 900;
  font-size: clamp(104px, 21vw, 380px);
  line-height: .88;
  letter-spacing: .01em;
  font-variant-numeric: tabular-nums;
  color: #fff;
}

.tv-foot {
  flex: none;
  min-width: 0;
  display: flex;
  align-items: baseline;
  gap: clamp(12px, 2vw, 28px);
  flex-wrap: wrap;
  font-size: clamp(12px, 1.3vw, 22px);
  color: rgba(233, 240, 248, .55);
}
.tv-next { margin-left: auto; color: var(--action-on-dark); }

.tv-idle {
  flex: 1;
  display: grid;
  place-items: center;
  font-size: clamp(18px, 2.4vw, 40px);
  color: rgba(233, 240, 248, .5);
}
.tv-idle p { margin: 0; }

/* A wall screen is landscape and a phone is not; stacked, the two sides
   still read top-to-bottom with the net between them. */
/* ---- the scoreboard on a phone --------------------------------------
   Held upright, there is no room for names and a score side by side: the
   names took the width and the score was squeezed into a column too
   narrow to paint anything in, so the one number the screen exists for
   was the one thing missing.
   Stacked instead. Each side is its names and then its score, centred,
   with the two scores meeting at the net. */
@media (max-aspect-ratio: 3/4) {
  .tv { padding: clamp(14px, 2.4vh, 28px) clamp(14px, 4vw, 28px); gap: clamp(10px, 1.8vh, 22px); }

  .tv-board { grid-template-columns: 1fr; grid-auto-rows: 1fr auto 1fr; }
  .tv-net { width: auto; height: 2px; align-self: auto;
            background: linear-gradient(90deg, rgba(255,255,255,.04), rgba(255,255,255,.22), rgba(255,255,255,.04)); }

  /* stretch, not center: justify-items:center shrink-wraps every item to
     its own content, so each row centred itself inside a box only as wide
     as it needed — and the score, whose box is then the width of one
     digit, sat wherever that box landed rather than on the middle of the
     court. Full-width rows, text centred inside them. */
  .tv-side, .tv-side-r {
    grid-template-columns: minmax(0, 1fr);
    justify-items: stretch;
    align-content: center;
    text-align: center;
    gap: clamp(4px, .8vh, 12px);
  }
  .tv-names { justify-items: stretch; width: 100%; }
  /* Names above the net on the top half, below it on the bottom, so the
     two scores sit either side of the line the way they do on a court. */
  .tv-side .tv-names { order: 0; }
  .tv-side .tv-score { order: 1; }
  .tv-side-r .tv-score { order: 0; }
  .tv-side-r .tv-names { order: 1; }

  /* The full width is the score's now. Centred explicitly rather than by
     justify-items alone: the score is the one thing on the screen that has
     to sit on the middle of the court, and a grid item that shrink-wraps
     is centred only as long as nothing else in the chain says otherwise. */
  .tv-side, .tv-side-r { justify-content: center; }
  .tv-names, .tv-name { text-align: center; }
  .tv-score {
    width: 100%;
    text-align: center;
    font-size: clamp(104px, 36vw, 280px);
    line-height: .9;
  }
  .tv-name { font-size: clamp(18px, 5.6vw, 30px); }

  /* The head and foot stop competing for one line. */
  .tv-head { flex-wrap: wrap; gap: 4px clamp(8px, 2vw, 16px); }
  .tv-sess { margin-left: 0; width: 100%; text-align: left; font-size: clamp(11px, 3.2vw, 15px); }
  .tv-foot { flex-direction: column; align-items: flex-start; gap: 3px; font-size: clamp(11px, 3.2vw, 15px); }
  .tv-next { margin-left: 0; }

  /* Two to a row rather than five squeezed into one, which cropped every
     name to a single letter. */
  .tv-ranks { flex-wrap: wrap; gap: 6px; padding-top: clamp(8px, 1.2vh, 14px); }
  .tv-ranks-label { width: 100%; padding: 0 0 2px; }
  .tv-rank { flex: 1 1 calc(50% - 6px); padding: 7px 10px; }
  /* Top three on a phone. Five in two-up rows costs three rows of height,
     and the screen has to fit without scrolling — the label is the first
     child, so the ranks are children two onward. */
  .tv-rank:nth-child(n+5) { display: none; }
  .tv-rank-name { font-size: clamp(12px, 3.6vw, 17px); }
  .tv-rank-pct  { font-size: clamp(11px, 3.4vw, 16px); }
}

/* ---- a clock that can be stopped -------------------------------------
   Paused is a third state, not "running" with a different label: a court
   with four people standing on it and the clock stopped is a real thing
   an organiser needs to see from across the room. */
.cstate-paused { background: var(--win-soft); color: var(--win-ink); }

/* The live dot stops pulsing when the clock does — it is the thing that
   says "this is happening now", and during a pause it is not. */
.livedot-held { background: var(--win); animation: none; opacity: .9; }

.court-pause {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  white-space: nowrap;
}
.court-pause svg { flex: none; }
/* While the clock is stopped the control is the loud one on the card: it
   is the state you have to get out of, and Finish is not what you want. */
.court-pause.is-paused {
  color: var(--win-ink);
  border-color: var(--win);
  background: var(--win-soft);
}
.court-pause.is-paused:hover,
.court-pause.is-paused:focus-visible { background: var(--win); color: #fff; border-color: var(--win); }

@media (min-width: 1024px) {
  /* Finish keeps the width; the clock control takes what it needs. */
  .courtgrid.view-cards .court-actions > .btn:first-child { flex: 1; }
  .courtgrid.view-cards .court-pause { flex: none; }
  /* In a row there is no width to spare, so it is the icon and its title. */
  .courtgrid.view-rows .court-pause .btn-word { display: none; }
  .courtgrid.view-rows .court-pause { padding-inline: 8px; }
}

/* On the wall, a stopped clock says so in words — a dot nobody can see
   from thirty feet is not a state. */
.tv-clock-held { color: #F0A183; }

/* ---- the table, on the television -----------------------------------
   A scoreboard beside a court is read by the people waiting for it, and
   the question they ask each other is who is winning tonight — not just
   who is winning this game.

   Each player is a block. As a flat row of text the third player's win
   rate ran straight into the fourth player's rank number, and five
   entries read as one long sentence. */
.tv-ranks {
  flex: none;
  display: flex;
  align-items: stretch;
  gap: clamp(6px, .7vw, 14px);
  border-top: 1px solid rgba(233,240,248,.14);
  padding-top: clamp(10px, 1.6vh, 20px);
  min-width: 0;
}
.tv-ranks-label {
  flex: none;
  align-self: center;
  font-size: clamp(9px, .72vw, 13px);
  font-weight: 600;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: rgba(233,240,248,.4);
  padding-right: clamp(4px, .6vw, 12px);
}
.tv-rank {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  align-items: baseline;
  gap: clamp(5px, .6vw, 11px);
  padding: clamp(7px, 1vh, 13px) clamp(9px, .9vw, 18px);
  background: rgba(255,255,255,.055);
  border-radius: var(--r-sm);
}
.tv-rank-no {
  flex: none;
  font-family: var(--mono);
  font-size: clamp(10px, .85vw, 16px);
  color: rgba(233,240,248,.38);
}
.tv-rank-name {
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(13px, 1.25vw, 24px);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tv-rank-pct {
  margin-left: auto;
  flex: none;
  font-family: var(--mono);
  font-size: clamp(12px, 1.15vw, 22px);
  color: var(--action-on-dark);
  display: inline-flex; align-items: baseline; gap: .35em;
}
/* Points need their unit; a rate and a difference carry their own. */
.tv-rank-unit { font-size: .62em; color: rgba(233,240,248,.5); }
/* Before anybody is ranked, the figure beside a name is noise. What the
   room wants off a screen at twenty feet is how many games are left. */
.tv-rank-gap {
  margin-left: auto;
  flex: none;
  font-family: var(--mono);
  font-size: clamp(11px, 1vw, 19px);
  color: rgba(233,240,248,.62);
}
/* The leader, so a glance finds them without reading the numbers. */
.tv-rank-top { background: rgba(190,211,30,.13); }
.tv-rank-top .tv-rank-name,
.tv-rank-top .tv-rank-no { color: var(--action-on-dark); }

/* The ranking basis, beside the "Standings" heading. Set in the ink rather
   than the muted grey the rest of the note uses: it is the sentence that
   makes every rank below it mean something. */
.resbar-by { color: var(--text); font-weight: 700; }

/* Open, but not yet. Deliberately quiet: the live chip beside it pulses and
   claims the night is happening, and this is the one that says it is not. */
.bh-soon { font-weight: 700; color: var(--accent-on-dark); }

/* Said where the name is actually taken, not on a page the player will never
   open. Quiet enough not to be a warning, present enough to be a notice. */
.checkin-note { margin-top: 10px; font-size: 12.5px; line-height: 1.5; }
.checkin-note a { color: inherit; text-decoration: underline; }

/* The standings switch. A label wrapping its own checkbox, so the whole
   block is the hit target -- a 13px box on a phone at the side of a court
   is not something anybody taps first time. */
.rankpairs {
  display: flex; align-items: flex-start; gap: 10px;
  margin-top: 12px; padding: 12px 14px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-md); cursor: pointer;
}
.rankpairs input { margin-top: 2px; flex: none; width: 17px; height: 17px; }
.rankpairs strong { font-size: 14px; }
.rankpairs .hint { margin-top: 3px; }

/* Finish is the one irreversible button on a live court, so it takes the
   SOLID form of the danger colour rather than the outline .btn-danger wears
   elsewhere: at full width under the scores an outline reads as secondary,
   which is the opposite of what this button is. --win is the token already
   used for "this undoes something"; there is no separate danger token. */
.court-actions .btn-danger {
  background: var(--win); border-color: var(--win); color: #fff;
}
.court-actions .btn-danger:hover,
.court-actions .btn-danger:focus-visible {
  background: var(--win-ink); border-color: var(--win-ink); color: #fff;
}

/* ---- has this court's score been saved? -------------------------------
   A few words per court (board.js, saveStatus). Quiet when the score on
   screen is the one everybody sees; a pill only when it is not, because
   that is the moment a scorer on a bad signal needs to notice.

   board.js draws it twice. Under the buttons by default: a phone's court
   header has no room left, and putting it there wrapped the court name and
   pushed the player count off the edge. In the desktop Rows view the
   buttons sit in a tight row and the header is a column with height to
   spare, so there it moves up beside the clock and is allowed to wrap. */
.save-state { font-family: var(--body); font-size: 11.5px; }
.save-state.is-saved  { opacity: .75; }
.save-state.is-saving { font-style: italic; }
.save-state.is-stuck {
  background: var(--win-soft); color: var(--win-ink);
  padding: 1px 8px; border-radius: 8px; font-weight: 600;
}
.save-line {
  margin-top: 8px; text-align: center; color: var(--text-muted);
  position: relative; z-index: 1;
}
.save-head { display: none; }

@media (min-width: 1024px) {
  .courtgrid.view-rows .save-line { display: none; }
  .courtgrid.view-rows .save-head {
    display: inline-block; max-width: 100%;
    font-size: 11px; line-height: 1.3; white-space: normal;
    /* A line of its own, which is also what keeps the organiser's three
       tools together: sharing a line with the clock, the first tool
       squeezed in beside it and the other two wrapped below. */
    flex-basis: 100%;
  }
}

/* ---- a player's face, with the replace button on its corner -------------
   board.js wraps each face and its replace button in .pav. The face is a
   size up from avatar-sm (27px, which read as a dot beside the name), and
   the button sits on its OUTER top corner -- left for the left side, right
   for the right -- so it never crowds the name. A ring in the court's own
   colour makes it read as a badge on the face rather than a sticker
   half off it. */
.pav { position: relative; display: block; width: fit-content; flex: none; }
.pav .avatar { width: 36px; height: 36px; font-size: 13px; }
.pav .swapbtn {
  position: absolute; top: -6px; left: -6px; z-index: 1;
  width: 20px; height: 20px; margin: 0;
  box-shadow: 0 0 0 2px var(--court-surface);
}
.side:last-child .pav .swapbtn { left: auto; right: -6px; }
/* 20px to look at, 30px to hit: it is pressed with a thumb, courtside. */
.pav .swapbtn::after { content: ""; position: absolute; inset: -5px; border-radius: 50%; }
/* Room for it. The badge stands 6px proud of the face, into the gap above,
   and at the old 2px (phone) and 6px (desktop cards) it sat on the name or
   face of the player listed above. */
.side-names { gap: 8px; }
@media (min-width: 1024px) {
  .courtgrid.view-cards .side-names { gap: 12px; }
}

/* The Rows view, a size up and with faces.
   It used to drop the faces to keep each row short, with the replace button
   as a separate chip beside the name. Now it matches the cards: a face on
   the outside of each name, the button on the face's outer corner, and a
   little more room for both. Everything here is the Rows view only. */
@media (min-width: 1024px) {
  .courtgrid.view-rows .pav .avatar { width: 32px; height: 32px; font-size: 12px; }
  /* The Rows rules above size the button at 22px for its old place in
     line; as a badge it is the cards' 20px. */
  .courtgrid.view-rows .pav .swapbtn { width: 20px; height: 20px; }
  /* The right-hand team reads from the net outwards, so its face goes last
     -- on the outside edge, with the button on that side of it. */
  .courtgrid.view-rows .sides > .side:last-child .pname .pav { order: 1; }

  .courtgrid.view-rows .pname { font-size: 15px; gap: 9px; }
  .courtgrid.view-rows .pname .lvl { font-size: 11px; }
  /* .pname clips its overflow (a long name must not push the score), and
     the badge stands 6px proud of the face: top and outside. This padding
     is the room it stands in, and doubles as the space between names. */
  .courtgrid.view-rows .pname { padding: 6px 0 0 6px; }
  .courtgrid.view-rows .sides > .side:last-child .pname { padding: 6px 6px 0 0; }
  .courtgrid.view-rows .side-names { gap: 6px; }
}

/* ---- rows, when the courts get less than 860px -------------------------
   An in-progress row puts two scorers (129px each), Finish and Pause
   (111px) and four names on one line. With the side panel open, a laptop
   under ~1240px leaves the courts less than 860px, and the names got what
   was left: at 1024 that was nothing -- four faces and no names.

   Measured on the courts' own width, not the screen's: the panel is only
   there on the Courts tab, and the board can be embedded narrower. Below
   the line the buttons drop under the play area and the scorer tightens,
   which at 1024 gives each name about 50px and at 1152 about 115px.
   A browser without container queries keeps the one-line row. */
@media (min-width: 1024px) {
  .courtgrid.view-rows { container: courts / inline-size; }
}
@container courts (max-width: 860px) {
  .courtgrid.view-rows > .court > .court-body {
    grid-template-columns: minmax(0, 1fr);
    row-gap: 10px;
  }
  .courtgrid.view-rows .court-actions { justify-self: end; }
  .courtgrid.view-rows .score { font-size: 26px; }
  .courtgrid.view-rows .stepper { width: 26px; height: 26px; font-size: 15px; }
}

/* ---- how the rotation works -------------------------------------------
   The Rules tab answers "what have I walked into". This answers the other
   question, the one asked out loud at the side of a court: why them and
   not me. Words alone were not enough for it -- "up to four places further
   down" is a thing to see, so the queue it describes is drawn with the
   people actually standing in it. */
.rothow-top {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; flex-wrap: wrap;
}
.rothow-mode {
  font-weight: 700; font-size: 12.5px;
  color: var(--accent); background: var(--accent-soft);
  padding: 3px 11px; border-radius: var(--r-full);
}
.rothow-lede { margin: 9px 0 0; }

.rothow-dia {
  display: flex; align-items: flex-end; gap: 14px; flex-wrap: wrap;
  margin: 15px 0 2px; padding: 13px;
  background: var(--surface-alt); border-radius: var(--r-md);
}
.rothow-col { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
.rothow-label {
  font-size: 9.5px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--text-faint); white-space: nowrap;
}
.rothow-discs { display: flex; gap: 5px; flex-wrap: wrap; }
.rothow-disc { display: inline-grid; place-items: center; border-radius: var(--r-full); }
/* The ring says which of these is a seat in the next game; the faded ones
   are the places the draw may reach into, and no further. */
.rothow-disc.is-front .avatar { box-shadow: 0 0 0 2px var(--accent); }
.rothow-disc.is-reach { opacity: .5; }
.rothow-disc.is-reach .avatar { box-shadow: 0 0 0 2px var(--border); }
.rothow-disc-empty {
  width: 26px; height: 26px;
  font-family: var(--mono); font-size: 11px; color: var(--text-faint);
  border: 1px dashed var(--border); border-radius: var(--r-full);
}
.rothow-disc-empty.is-front { border-style: solid; border-color: var(--accent); color: var(--accent); }
.rothow-arrow { color: var(--text-faint); align-self: center; flex: none; display: inline-grid; }
.rothow-next { display: flex; gap: 5px; flex-wrap: wrap; }
.rothow-name {
  font-size: 12.5px; font-weight: 600;
  padding: 3px 9px; border-radius: var(--r-full);
  background: var(--accent-soft); color: var(--accent);
}
.rothow-name-empty { background: none; color: var(--text-faint); font-weight: 400; padding-left: 0; }
.rothow-court {
  display: inline-grid; place-items: center;
  width: 34px; height: 34px; border-radius: var(--r-md);
  background: var(--brand); color: var(--on-brand);
}

/* The steps, numbered down a rail: the order is the content here, so it is
   drawn rather than left to the reader to infer from the order of the
   paragraphs. */
.rothow-steps { margin: 16px 0 0; padding: 0; list-style: none; counter-reset: rothow; }
.rothow-steps li { position: relative; padding: 0 0 15px 34px; counter-increment: rothow; }
.rothow-steps li::before {
  content: counter(rothow);
  position: absolute; left: 0; top: 0;
  width: 22px; height: 22px; border-radius: var(--r-full);
  background: var(--accent-soft); color: var(--accent);
  font: 600 11.5px/22px var(--mono); text-align: center;
}
.rothow-steps li::after {
  content: ""; position: absolute; left: 11px; top: 25px; bottom: 3px;
  width: 1px; background: var(--border);
}
.rothow-steps li:last-child { padding-bottom: 0; }
.rothow-steps li:last-child::after { display: none; }
.rothow-steps strong { display: block; font-size: 14px; }
.rothow-steps span { display: block; margin-top: 3px; font-size: 13px; color: var(--text-muted); }

/* On a phone the three parts stack, so the arrows turn to match. Without
   this they point off the side of a column that is now above the next. */
@media (max-width: 560px) {
  .rothow-dia { flex-direction: column; align-items: stretch; gap: 10px; }
  .rothow-arrow { transform: rotate(90deg); align-self: flex-start; margin-left: 6px; }
}

/* "Why this order?", beside the queue heading it explains. A link, not a
   button: it goes somewhere to read, and a button here would compete with
   the organiser's controls in the rows below. */
.qhead-left { display: inline-flex; align-items: center; gap: 10px; min-width: 0; }
.linkq {
  display: inline-flex; align-items: center; gap: 5px;
  background: none; border: 0; padding: 2px 0; cursor: pointer;
  font: inherit; font-size: 12px; font-weight: 600;
  color: var(--accent); text-decoration: underline; text-underline-offset: 2px;
}
.linkq:hover, .linkq:focus-visible { color: var(--brand); }

/* =====================================================================
   THE SUMMARY TAB — what a finished open play opens on

   The board used to end the way it began: Courts first, six empty boxes
   and a grey line saying the results below were final. This is the other
   half of that fix; the tab itself is renderSummary() in board.js.

   Deliberately close in shape to recap.css, which draws the printable
   sheet from the same data. They are separate files because they are
   separate documents — the sheet loads no board.css and the board loads
   no recap.css — but somebody who prints the PDF after reading this tab
   should recognise what they are holding.
   ===================================================================== */

/* ---- the closing line, and the way out with the night in hand -------- */

.wrapcard {
  display: flex; align-items: center; justify-content: space-between;
  gap: 14px; flex-wrap: wrap;
}
.wrap-say { min-width: 0; }
.wrap-h {
  margin: 4px 0 2px;
  font-family: var(--display); font-weight: 700;
  font-size: 22px; line-height: 1.15;
}
.wrap-acts { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.wrap-acts .btn { display: inline-flex; align-items: center; gap: 6px; }

/* ---- where the reader came in it ------------------------------------- */

/* For the one person reading their own name. A spectator gets nothing
   here, and neither does the organiser, who did not play. */
.yournight {
  display: flex; align-items: center; gap: 12px;
  border-color: var(--accent);
}
.yn-txt { flex: 1; min-width: 0; }
.yn-txt strong { display: block; font-size: 15px; }
.yn-big {
  flex: none;
  font-family: var(--display); font-weight: 700;
  font-size: 27px; line-height: 1; color: var(--accent);
}

/* ---- podium ------------------------------------------------------------ */

/* Second, first, third, left to right. The list is in rank order in the
   DOM — so the reading order and a screen reader both get 1-2-3 — and
   only the drawing is reordered. A podium is a shape people read before
   they read the numbers on it, and the winner has to be in the middle for
   it to be one. */
.podium {
  display: grid; grid-template-columns: repeat(3, 1fr);
  gap: 10px; align-items: end;
  margin: 0; padding: 0; list-style: none;
}
.podium-1 { grid-template-columns: 1fr; }
.podium-2 { grid-template-columns: repeat(2, 1fr); }

.pod {
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  padding: 15px 10px 13px;
  border: 1px solid var(--border); border-radius: var(--r-md);
  background: var(--surface);
  text-align: center;
}
.pod-1 { order: 2; border-color: var(--action); background: var(--action-soft); padding-top: 21px; }
.pod-2 { order: 1; }
.pod-3 { order: 3; }

.pod-place {
  display: grid; place-items: center;
  width: 23px; height: 23px; border-radius: var(--r-full);
  font-family: var(--mono); font-size: 12px; font-weight: 700;
  background: var(--text); color: var(--surface);
}
/* The ball colour with the ink app.css pairs with it. --action-ink is lime
   as TEXT on white; using it as the fill here puts navy on dark olive and
   the numeral disappears. */
.pod-1 .pod-place { background: var(--action); color: var(--on-action); }

.pod-name { margin-top: 2px; font-size: 15px; line-height: 1.2; overflow-wrap: anywhere; }
.pod-line { font-size: 11.5px; color: var(--text-muted); }
.pod-big {
  font-family: var(--display); font-weight: 700;
  font-size: 22px; line-height: 1; color: var(--accent);
}

@media (max-width: 420px) {
  /* Three cards in 390px gives each one 110px, which a two-syllable name
     and a percentage do not fit into. Stacked, ranked, winner first. */
  .podium, .podium-2 { grid-template-columns: 1fr; }
  .pod    { flex-direction: row; gap: 10px; text-align: left; padding: 11px 13px; }
  .pod-1, .pod-2, .pod-3 { order: 0; padding-top: 11px; }
  .pod-name { flex: 1; margin-top: 0; }
  .pod-line { flex: none; }
}

/* ---- the night's awards ------------------------------------------------ */

.awards { overflow: hidden; }

.award {
  display: grid;
  grid-template-columns: 148px minmax(0, 1fr) auto;
  align-items: center; gap: 3px 12px;
  padding: 10px 13px;
  border-bottom: 1px solid var(--border);
}
.award:last-child { border-bottom: 0; }

.award-l {
  font-size: 10.5px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
  color: var(--text-faint);
}
.award-who { display: inline-flex; align-items: center; gap: 7px; min-width: 0; }
.award-who b { font-size: 14px; overflow-wrap: anywhere; }
.award-v {
  font-family: var(--mono); font-size: 14px; font-weight: 500;
  white-space: nowrap; color: var(--accent);
}
/* Under the name, in the name's column: it qualifies the who, not the
   label to its left. */
.award-d { grid-column: 2 / 4; font-size: 11.5px; color: var(--text-muted); }

@media (max-width: 560px) {
  .award   { grid-template-columns: minmax(0, 1fr) auto; }
  .award-l { grid-column: 1 / 3; }
}

/* ---- through to the full table ----------------------------------------- */

/* The same shape as .joinbar, because it is the same idea: one line that
   says what is on the other side of it. */
.seeall {
  display: flex; align-items: center; gap: 12px; width: 100%;
  padding: 11px 14px;
  border: 1px solid var(--border); border-radius: var(--r-md);
  background: var(--surface); color: var(--text);
  font: inherit; text-align: left; cursor: pointer;
}
.seeall:hover { border-color: var(--accent); }
.seeall > span:first-child { flex: 1; min-width: 0; }
.seeall strong { display: block; font-size: 14.5px; }
.seeall-go {
  flex: none; padding: 8px 15px; border-radius: var(--r-full);
  background: var(--accent); color: var(--on-accent);
  font-size: 13px; font-weight: 700; white-space: nowrap;
}

/* A value that is not a count. The disc is sized to two digits; "2h 42m"
   inside it wrapped onto two lines and broke the row's baseline. Same
   colours, same height, sized to its text. */
.stile-wide {
  width: auto; min-width: 40px;
  padding: 0 11px;
  border-radius: var(--r-full);
  font-size: 15px; letter-spacing: -.01em;
  white-space: nowrap;
}

/* renderStatStrip() leaves this container empty on a finished open play --
   "0 waiting" about a night that ended hours ago is noise. Empty, it still
   drew its border, its shadow and 20px of margin: a thin blank card under
   the header with nothing in it. */
.statstrip:empty { display: none; }

/* ---- add a player: finding their account ------------------------------ */
/* Suggestions under the name box while typing, and the line that says an
   account is linked. Both are empty -- and take no room -- for a guest. */
.acct-suggest { display: grid; gap: 2px; }
.acct-suggest:empty { display: none; }
.acct-row {
  display: flex; align-items: center; gap: 9px; width: 100%;
  padding: 8px 10px; border-radius: var(--r-sm);
  background: var(--surface-alt); color: var(--text); text-align: left;
  font: inherit; font-size: 14px;
}
.acct-row:hover:not(:disabled), .acct-row:focus-visible { background: var(--accent-soft, #e6eefb); }
.acct-row:disabled { opacity: .55; cursor: default; }
.acct-name { flex: 1; min-width: 0; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.acct-here { flex: none; font-size: 11px; font-weight: 600; color: var(--text-muted); }
.acct-none { margin: 0; font-size: 12.5px; color: var(--text-muted); }
.acct-linked {
  display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px;
  padding: 8px 10px; border-radius: var(--r-sm);
  border: 1px solid var(--accent); background: var(--surface);
  font-size: 13px;
}
.acct-linked[hidden] { display: none; }
.acct-linked-text { display: inline-flex; align-items: center; gap: 6px; color: var(--accent); }
.acct-linked-text b { color: var(--text); }
.acct-unlink { background: none; padding: 0; font-size: 12px; font-weight: 700; color: var(--accent); text-decoration: underline; }

/* The account list is a dropdown under the name box, over the rest of the
   form, rather than a block that pushed the form down while typing. */
.acct-field { position: relative; }
.acct-drop {
  position: absolute; left: 0; right: 0; top: calc(100% + 4px); z-index: 30;
  max-height: 320px; overflow-y: auto;
  padding: 6px; border-radius: var(--r-md);
  background: var(--surface); border: 1px solid var(--border);
  box-shadow: 0 12px 28px rgba(15, 30, 60, .16);
}
.acct-drop[hidden] { display: none; }
.acct-drop-head {
  padding: 4px 8px 6px; font-size: 11px; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: var(--text-muted);
}
.acct-drop .acct-none { padding: 4px 8px 8px; }
.acct-drop .acct-row { background: transparent; }
.acct-drop .acct-row:hover:not(:disabled),
.acct-drop .acct-row:focus-visible { background: var(--accent-soft); }
.acct-who { flex: 1; min-width: 0; display: grid; }
.acct-who .acct-name { font-weight: 600; }
.acct-known { font-size: 11.5px; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.acct-guest { border-top: 1px solid var(--border); border-radius: 0 0 var(--r-sm) var(--r-sm); margin-top: 4px; }
.acct-guest-ico {
  flex: none; display: grid; place-items: center; width: 27px; height: 27px; border-radius: 50%;
  background: var(--surface-alt); color: var(--text-muted); font-weight: 700;
}

/* ---- import a list of players ---------------------------------------- */
.import-box { width: min(560px, 100%); }
.import-area { width: 100%; min-height: 150px; font-family: var(--mono); font-size: 13px; resize: vertical; }
.import-file-label { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 10px 0 4px;
  font-size: 12.5px; color: var(--text-muted); }
.import-file { font-size: 12.5px; max-width: 100%; }
.import-result { margin-top: 12px; max-height: 220px; overflow-y: auto; }
.import-result[hidden] { display: none; }
.import-box .import-sum { margin: 0 0 6px; font-weight: 700; color: var(--text); }
.import-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; font-size: 13px; }
.import-list li { padding: 5px 8px; border-radius: var(--r-sm); overflow-wrap: anywhere; }
.import-ok { background: var(--surface-alt); }
.import-skip { background: #fff4e5; color: #7a4a00; }
