/* Bricolage Grotesque, variable, self-hosted. Never fonts.googleapis.com at
   runtime - a third-party font request is the slowest thing on a page people
   open on club wifi. */
@font-face {
  font-family: 'Bricolage Grotesque';
  src: url('/fonts/bricolage-grotesque-latin.woff2') format('woff2-variations');
  font-weight: 200 800;
  font-stretch: 75% 100%;
  font-display: swap;
}

:root {
  color-scheme: dark;

  --page-bg: #0f2318;
  --card: #1c3a27;
  --card-pick: #22492f;
  --card-live: #1c3a27;

  --line: #2e4536;
  --line-soft: #263b2d;
  --line-live: #4a5c3c;
  --line-box: var(--ink);

  --ink: #f2ecdc;
  --dim: #9db3a3;
  --faint: #9db3a3;
  --fainter: #7c9186;
  --faintest: #65796c;
  --muted: #9db3a3;

  /* Acid is the live feed and the alert system. Nothing else. */
  --acid: #d8ff3f;
  --on-acid: #12241a;
  --acid-dim: #4d5e1f;
  --acid-text: var(--acid);

  /* Live is what's happening on court right now - a distinct signal from
     acid, which stays reserved for the alert system (a pick, a push). */
  --live: #ff6b4a;
  --on-live: #12241a;
  --live-text: #ff7a5c;

  /* Solid, un-blurred offset shadow behind cards/panels/buttons. */
  --ink-shadow: rgba(0, 0, 0, .45);

  /* The footer bar is always a dark slab in both themes - the mockup's one
     fixed anchor - so it carries its own pair of tokens rather than borrowing
     the theme's foreground. */
  --bar-bg: #131313;
  --bar-ink: #f4efe2;
  --bar-dim: #9a9a90;

  --portrait-border: var(--ink);

  /* Occasion: gold. A third accent, admitted on one condition - gold marks
     the OCCASION (a final, the clasico), never the STATE. It is allowed on a
     border, a court engraving, a struck medallion, a foil band; it is never
     allowed on the bell, the live badge or a status pill. That is what keeps
     "acid = alerts, coral = live" meaning what a visitor already learned. */
  --gold: #e3b04b;
  --gold-deep: #a97d24;
  --gold-ink: #2a1e08;
  --gold-hair: rgba(227, 176, 75, .55);
  --gold-line: rgba(227, 176, 75, .34);
  --gold-flash: #fff4d2;

  /* The card's court panel - a green court in both themes, with cream
     lines and text sized to read on the green. The score board is the
     dark net-post slab; the trailing team's numbers sit greyer still. */
  --court: #1f4030;
  --court-line: rgba(242, 236, 220, .24);
  --court-net: rgba(242, 236, 220, .62);
  --court-ink: #f2ecdc;
  --court-dim: #9db3a3;
  --court-lose: #7f958a;
  --board-bg: #12241a;
  /* the detail modal's headshot fallbacks: a plain silhouette on the court,
     an initials chip in the line-up strip. Court-green in both themes. */
  --court-mono: #2b5040;
  --court-mono-fg: #4a6d5c;
  --mono-chip: #d8d0b4;
  --mono-chip-fg: #4a5340;

  --display: 'Bricolage Grotesque', system-ui, -apple-system, sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  --shell-w: 468px;
  /* the court card splits its width in two, so the desktop columns need
     more room than the old stacked card did */
  --col-w: 344px;
}

/* Light is an explicit choice (see the inline script in index.html, which
   resolves system preference before first paint and sets this attribute) -
   never a bare prefers-color-scheme block, so the toggle and the initial
   system read share one source of truth. */
:root[data-theme="light"] {
  color-scheme: light;

  --page-bg: #f2ecdc;
  --card: #fffdf6;
  --card-pick: #f5f7dc;
  --card-live: #fffdf6;

  --line: #e2dcc6;
  --line-soft: #ece5d2;
  --line-live: #b7c76a;
  --line-box: var(--ink);

  --ink: #12241a;
  --dim: #5c6b5e;
  --faint: #5c6b5e;
  --fainter: #74836f;
  --faintest: #8b9686;
  --muted: #5c6b5e;

  --acid: #d8ff3f;
  --on-acid: #12241a;
  --acid-dim: #55680f;
  --acid-text: #55680f;

  --live: #ff6b4a;
  --on-live: #12241a;
  --live-text: #b23a1e;

  --ink-shadow: var(--ink);

  --portrait-border: var(--ink);

  /* The court stays green in both themes, so the court's own gold (the rays,
     the hairline) does not move; these are the values for gold on cream -
     deep enough to read as metal rather than as highlighter. */
  --gold: #c2901f;
  --gold-deep: #8c6212;
  --gold-ink: #2a1e08;
  --gold-hair: rgba(140, 98, 18, .6);
  --gold-line: rgba(140, 98, 18, .38);
  --gold-flash: #7a5209;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background-color: var(--page-bg);
  background-image:
    repeating-linear-gradient(0deg, var(--line) 0 1.5px, transparent 1.5px 27px),
    repeating-linear-gradient(90deg, var(--line) 0 1.5px, transparent 1.5px 27px);
  color: var(--ink);
  font: 400 15px/1.55 var(--display);
  -webkit-font-smoothing: antialiased;
}

:focus-visible { outline: 2px solid var(--acid); outline-offset: 2px; }

.lbl {
  font: 700 10px/1 var(--display);
  letter-spacing: .1em;
  text-transform: uppercase;
  margin: 0;
}
.mono { font: 400 11.5px/1.6 var(--mono); margin: 0; }
.faint { color: var(--faintest); }
.sd { color: var(--dim); font-weight: 500; }
.ico { flex: none; vertical-align: -2px; }

/* ---------- shell: one centred column ---------- */
.shell {
  max-width: var(--shell-w);
  margin: 0 auto;
  padding: 0 20px 148px;
  display: flex;
  flex-direction: column;
  gap: 22px;
  min-height: 100vh;
}

/* ---------- top bar ---------- */
.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding-top: 22px;
}
.brand { display: flex; align-items: center; gap: 10px; }
.brand-mark { display: block; flex: none; border-radius: 7px; }
.wordmark {
  position: relative;
  margin: 0;
  font: 800 15px/1 var(--display);
  font-stretch: 82%;
  letter-spacing: .03em;
  text-transform: uppercase;
}
/* Editorial "BETA" kicker hugging the wordmark - no box, carried by the
   acid colour. --acid-text (not raw --acid) keeps it legible on light. */
.beta-flag {
  position: absolute;
  top: -7px;
  right: -30px;
  font: 700 8px/1 var(--display);
  letter-spacing: .16em;
  color: var(--acid-text);
}

.toggles { display: flex; align-items: center; gap: 8px; flex: none; }
.pill-btn {
  appearance: none;
  -webkit-appearance: none;
  height: 38px;
  min-width: 44px;
  padding: 0 14px;
  flex: none;
  border: 2px solid var(--ink);
  background: var(--card);
  color: var(--ink);
  font: 400 14px/1 var(--mono);
  letter-spacing: .04em;
  display: grid;
  place-items: center;
  cursor: pointer;
  border-radius: 999px;
  box-shadow: 3px 3px 0 var(--ink-shadow);
}
.pill-btn.round { width: 38px; min-width: 0; padding: 0; }
.pill-btn:hover { color: var(--acid-text); }

/* ---------- tournament badge ---------- */
.now-badge {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  align-self: flex-start;
  background: var(--acid);
  color: var(--on-acid);
  font: 700 12px/1 var(--display);
  letter-spacing: .04em;
  text-transform: uppercase;
  border: 2px solid var(--ink);
  border-radius: 999px;
  padding: 8px 14px 8px 10px;
  margin: 0;
}
.now-badge .dot { background: var(--on-acid); border-color: var(--on-acid); }
.dot { width: 8px; height: 8px; border-radius: 50%; background: var(--acid); border: 1.5px solid var(--ink); flex: none; }

/* ---------- hero ---------- */
.hero-wrap { position: relative; }
.hero {
  font-family: var(--display);
  font-size: 66px;
  line-height: .88;
  font-weight: 800;
  font-stretch: 78%;
  letter-spacing: -.045em;
  text-transform: uppercase;
  margin: 0;
}
.hero-on { color: var(--acid-text); }

.hero-line1 { position: relative; display: inline-block; }
.squiggle {
  position: absolute;
  left: 1%;
  bottom: -.14em;
  width: 98%;
  height: .16em;
  overflow: visible;
}
.squiggle path { fill: none; stroke: var(--acid); stroke-width: 7; stroke-linecap: round; }
.squiggle-live path { stroke: var(--live); }

/* The one handwritten touch - it sits in the whitespace to the right of the
   two-word "Live now" headline. */
.hero-note {
  position: absolute;
  right: 0;
  top: 6px;
  max-width: 42%;
  margin: 0;
  text-align: right;
  font: italic 600 15px/1.15 var(--display);
  color: var(--live-text);
  transform: rotate(-4deg);
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ---------- onboarding: search + quick picks ---------- */
.search {
  display: flex;
  align-items: center;
  gap: 12px;
  background: var(--card);
  border: 2.5px solid var(--ink);
  border-radius: 999px;
  padding: 12px 14px 12px 20px;
  box-shadow: 3px 3px 0 var(--ink-shadow);
  color: var(--dim);
}
.search-input {
  appearance: none;
  -webkit-appearance: none;
  flex: 1 1 auto;
  min-width: 0;
  border: 0;
  background: none;
  color: var(--ink);
  font: 700 15px/1.2 var(--display);
}
.search-input:focus { outline: none; }
.search-input::placeholder { color: var(--faintest); font-weight: 400; }
.search-input::-webkit-search-cancel-button { -webkit-appearance: none; display: none; }
.search-clear {
  appearance: none;
  -webkit-appearance: none;
  flex: none;
  width: 26px;
  height: 26px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 50%;
  background: var(--line-soft);
  color: var(--ink);
  cursor: pointer;
}

/* On the intro screen the search sits in the flow (the wrapper collapses out);
   on the board and filtered list the wrapper is an opaque band that pins the
   pill to the top so it is always one tap away. Court headers park just below
   it, hence the --dock-h other rules key their sticky offset off. */
:root { --dock-h: 66px; }
.search-dock { display: contents; }
.search-dock[data-stuck="true"] {
  display: block;
  position: sticky;
  top: 0;
  z-index: 4;
  margin: 0 -20px;
  padding: 9px 20px 11px;
  background: var(--page-bg);
}

.chips-pick { display: flex; flex-wrap: wrap; gap: 8px; }
.chip-pick {
  appearance: none;
  -webkit-appearance: none;
  display: inline-flex;
  align-items: center;
  gap: 9px;
  min-height: 40px;
  padding: 9px 15px;
  border: 2px solid var(--ink);
  border-radius: 999px;
  background: var(--card);
  color: var(--ink);
  font: 700 14px/1 var(--display);
  cursor: pointer;
}
.chip-pick:hover { background: var(--card-pick); }

.link-out {
  appearance: none;
  -webkit-appearance: none;
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 0 0 2px;
  border: 0;
  border-bottom: 2px solid currentColor;
  background: none;
  color: var(--acid-text);
  font: 700 14px/1 var(--display);
  cursor: pointer;
}

/* the intro screen has no fixed footer bar to clear */
.shell[data-mode="onboarding"] { padding-bottom: 40px; }

/* ---------- board ---------- */
.board { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
.board-when { color: var(--faint); }
/* the poll countdown lives at the very end of the card, a quiet "still live" */
.board-foot { color: var(--faintest); text-align: center; padding-top: 2px; }
/* ...unless the feed has gone stale, when it is the one warning on the board */
.board-foot.is-stale { color: var(--live-text); }

/* court index - a horizontal pill row, the current court filled */
.index { display: flex; gap: 8px; overflow-x: auto; scrollbar-width: none; padding-bottom: 2px; margin: 0 -20px; padding-left: 20px; padding-right: 20px; }
.index::-webkit-scrollbar { display: none; }
.chip {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 9px 14px;
  border: 2px solid var(--line);
  border-radius: 999px;
  background: var(--card);
  flex: none;
  text-decoration: none;
  min-height: 40px;
  font: inherit;
  cursor: pointer;
  appearance: none;
  -webkit-appearance: none;
}
.chip .lbl { color: var(--dim); }
.chip .cdot { width: 7px; height: 7px; border-radius: 50%; background: var(--line-box); border: 1.3px solid var(--ink); flex: none; }
.chip[data-picked="true"]:not([data-live="true"]) .cdot { background: var(--ink); }
.chip[data-live="true"] .cdot { background: var(--live-text); }
.chip[data-live="true"] .lbl,
.chip[data-picked="true"] .lbl { color: var(--ink); }
.chip[aria-current="true"] { background: var(--ink); border-color: var(--ink); }
.chip[aria-current="true"] .lbl { color: var(--page-bg); }
.chip[aria-current="true"] .cdot { border-color: var(--page-bg); }

/* court sections: one court per screen below desktop, swiped between with
   native scroll-snap - the index chips above double as tabs into the same
   scroller, so tap-to-jump and swipe land on the same mechanism. */
.columns {
  display: flex;
  flex-direction: row;
  gap: 0;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  margin: 0 -20px;
}
.columns::-webkit-scrollbar { display: none; }
.col {
  display: flex;
  flex-direction: column;
  gap: 12px;
  min-width: 0;
  flex: none;
  width: 100%;
  padding: 0 20px;
  box-sizing: border-box;
  scroll-snap-align: start;
  scroll-snap-stop: always;
  scroll-margin-left: 20px;
  /* .colhead can't self-correct via sticky here (see below), so the anchor
     jump needs its own allowance for the dock or the heading lands under it. */
  scroll-margin-top: var(--dock-h);
}
/* .columns' own overflow-x makes it a scroll container, which hijacks any
   descendant's position:sticky to stick relative to *it* instead of the
   page - .colhead would render permanently offset by --dock-h rather than
   actually sticking as you scroll. One court per screen rarely runs long
   enough to need the hand-off anyway, so switch it off here the same way
   the desktop grid already does below. */
.colhead {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  position: static;
  padding-top: 0;
  padding-bottom: 9px;
  background: none;
  border-bottom: 2.5px solid var(--ink);
}
.courtname {
  font-family: var(--display);
  font-size: 22px;
  line-height: 1;
  font-weight: 800;
  font-stretch: 80%;
  letter-spacing: -.025em;
  text-transform: uppercase;
  margin: 0;
}
.colhead .lbl { color: var(--faint); }

/* A search narrows the board in place - same columns, only the matching
   cards. On a phone the one-court-per-screen scroller would strand a lone
   result on an empty page, so a filtered board stacks its courts instead. */
.shell[data-mode="filtered"] .columns {
  flex-direction: column;
  gap: 20px;
  overflow: visible;
  scroll-snap-type: none;
}
.shell[data-mode="filtered"] .col {
  width: 100%;
  scroll-snap-align: none;
}
/* the court index is the desktop rail's jump nav; on a phone the court
   headers in the stack already say which court, so drop the extra strip */
.shell[data-mode="filtered"] > .index { display: none; }

/* ---------- cards ---------- */
.card {
  background: var(--card);
  border: 2.5px solid var(--ink);
  border-radius: 18px;
  /* tighter than the old stacked card - the reclaimed room goes to the
     court so the pitch reads with some depth, not as a thin strip */
  padding: 12px 17px;
  display: flex;
  flex-direction: column;
  gap: 9px;
  min-height: 44px;
  box-shadow: 5px 5px 0 var(--ink-shadow);
}
.card[role="button"] { cursor: pointer; }
.card[role="button"]:focus-visible { outline: 2px solid var(--acid); outline-offset: 2px; }
/* min-height locks the row to the tallest control it can hold (the bell
   pill), so a card showing only the live badge - or nothing - is the same
   height as one with the pill. Part of keeping every card identical. */
.card-top { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-height: 34px; }
.card-top .lbl { color: var(--dim); }
.card-ctl { display: flex; align-items: center; gap: 10px; flex: none; }
.pair { font-size: 15px; line-height: 1.4; margin: 0; }

/* ---------- the court: the two teams across a top-down padel court ----------
   The players band is drawn as a court seen from above - a team on each
   half, the net down the centre, service lines at the quarters. */
.court {
  position: relative;
  border-radius: 12px;
  padding: 11px;
  background-color: var(--court);
  background-image:
    linear-gradient(90deg, transparent calc(25% - .75px), var(--court-line) calc(25% - .75px), var(--court-line) calc(25% + .75px), transparent calc(25% + .75px)),
    linear-gradient(90deg, transparent calc(75% - .75px), var(--court-line) calc(75% - .75px), var(--court-line) calc(75% + .75px), transparent calc(75% + .75px));
  box-shadow: inset 0 0 0 1.5px var(--court-line);
  overflow: hidden;
}
.court-net {
  position: absolute;
  inset: 0 auto 0 50%;
  width: 2px;
  transform: translateX(-1px);
  background: var(--court-net);
}
.court-tee {
  position: absolute;
  left: 25%;
  right: 25%;
  top: 50%;
  height: 1.5px;
  transform: translateY(-.75px);
  background: var(--court-line);
}
.card[data-state="live"] .court-net { background: var(--live); }

/* The board sits dead-centre on the net; the two players of each team
   sit at the front and back of their half, so the board drops into the
   gap between the name rows instead of colliding with them. A small
   centre gap keeps a long name off the board's corners. */
.halves { position: relative; display: grid; grid-template-columns: 1fr 1fr; column-gap: 28px; }
/* deep enough that the two players read as sitting at their own baselines,
   with open court between them - and, on live/finished, room for the board.
   Fixed for every state (and for the wider deep-third-set board, which
   gains columns sideways, not height) so all cards match. */
.half { display: flex; align-items: stretch; min-height: 96px; min-width: 0; }
.names { display: flex; flex-direction: column; justify-content: space-between; gap: 6px; min-width: 0; width: 100%; }
.half-r .names { align-items: flex-end; text-align: right; }
.pline { display: flex; align-items: center; gap: 8px; min-width: 0; max-width: 100%; }
/* the right team mirrors - flag to the outside */
.half-r .pline { flex-direction: row-reverse; }
.pname {
  font-size: 14px;
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -.005em;
  color: var(--court-ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.pname .sd { color: var(--court-dim); font-weight: 500; }

.flag {
  width: 16px;
  height: 11px;
  flex: none;
  border-radius: 2px;
  border: 1px solid rgba(18, 36, 26, .55);
  object-fit: cover;
  display: block;
}
.flag-blank { background: var(--court-line); }

/* the score: an umpire's board on the net - centred on it, a row per
   team, a column per set played (the score up to now). */
.court-board {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 2;
  display: grid;
  align-items: center;
  gap: 3px 8px;
  padding: 5px 9px;
  background: var(--board-bg);
  border: 1.5px solid var(--court-net);
  border-radius: 8px;
  font: 600 12px/1 var(--mono);
}
.bcell { color: var(--court-ink); text-align: center; min-width: 9px; }
/* the trailing team reads one step quieter */
.bcell-dim { color: var(--court-lose); }

.card-foot {
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--faint);
  border-top: 1.5px solid var(--line-soft);
  /* one line of text, sitting in equal air on three sides: the same gap
     court-to-divider (negative margin against the card's stack gap),
     divider-to-text, and text-to-card-edge (negative margin against the
     card's pad). line-height near 1 so the padding is the visible gap. */
  margin-top: -3px;
  padding-top: 6px;
  margin-bottom: -6px;
  line-height: 1.1;
}
/* a fixed start time ("Starts 9:00") reads firmer than a soft "Not before" */
.card-foot-firm { color: var(--ink); }
/* a card with nothing to say in the foot (no clock, no scheduled time) still
   reserves the exact same row - same markup, placeholder text - so it stands
   as tall as one that does. visibility:hidden keeps the box (and its space)
   while hiding the divider and the dummy time. */
.card-foot-blank { visibility: hidden; }

.card[data-state="finished"] { border-color: var(--line-soft); opacity: .4; box-shadow: none; }
.card[data-state="live"] {
  border-color: var(--ink);
  border-left: 5px solid var(--live);
  padding-left: 13px;
}
.card[data-state="upNext"] .card-top .lbl { color: var(--ink); }
.card.is-picked { background: var(--card-pick); }

/* ---------- the occasion: a final, or the clasico ----------
   Direction G. The foil band across the head of the card is the light source
   and the court is the surface the light lands on, which is the reading that
   makes the two belong to each other rather than merely sit together. See
   public/occasion.js for which matches get here - almost none, deliberately. */
.card-occasion { border-color: var(--gold); }

/* The band. Hard-edged facets rather than a soft gradient: that is how a real
   foil stamp catches light, at one angle and not as a wash. Full-bleed, so the
   negative margins cancel the card's padding. */
.bill {
  position: relative;
  overflow: hidden;
  margin: -12px -17px 0;
  padding: 11px 17px 10px;
  background: linear-gradient(100deg, var(--gold-deep) 0 34%, var(--gold) 34% 61%, var(--gold-deep) 61%);
  color: var(--gold-ink);
  /* the card's 18px radius less its 2.5px border */
  border-radius: 15.5px 15.5px 0 0;
  display: flex;
  align-items: center;
  gap: 11px;
}
/* a live card carries a 5px coral edge, so its band starts further in */
.card[data-state="live"] .bill { margin-left: -13px; padding-left: 13px; }
.bill-rays {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: repeating-conic-gradient(from 0deg at 50% 130%,
    rgba(42, 30, 8, .11) 0 1.4deg, transparent 1.4deg 7.5deg);
}
.bill-t {
  position: relative;
  margin: 0;
  font: 800 21px/1 var(--display);
  font-stretch: 80%;
  letter-spacing: -.02em;
  text-transform: uppercase;
}
/* "The final" beside "El Clásico" - the second fact, when there are two */
.bill-sub {
  position: relative;
  margin: 0 0 0 auto;
  color: var(--gold-ink);
  opacity: .66;
}
.bill .mark { width: 22px; height: 22px; flex: none; position: relative; }

/* The court, engraved. The rays are the bottom layer of the background stack
   so both service lines still paint over them and the pitch reads as a pitch;
   they radiate from the centre of the net, which is the point both teams face.
   Court colours do not change with the theme, so neither do these. */
.court-occasion {
  background-image:
    linear-gradient(90deg, transparent calc(25% - .75px), var(--court-line) calc(25% - .75px), var(--court-line) calc(25% + .75px), transparent calc(25% + .75px)),
    linear-gradient(90deg, transparent calc(75% - .75px), var(--court-line) calc(75% - .75px), var(--court-line) calc(75% + .75px), transparent calc(75% + .75px)),
    repeating-conic-gradient(from 0deg at 50% 50%, rgba(227, 176, 75, .15) 0 1.6deg, transparent 1.6deg 8.5deg);
  box-shadow:
    inset 0 0 0 1.5px rgba(227, 176, 75, .55),
    inset 0 0 0 5px var(--court),
    inset 0 0 0 6.5px rgba(227, 176, 75, .3);
}

/* The medallion, struck on the net - the umpire's board's own slot, using the
   same knock-out so the court lines stop cleanly at its edge. It only appears
   on a match still to play: once there is a score the score wins that slot,
   and every corner of this little court already has a name in it, so there is
   nowhere honest to park a mark instead. */
.crest {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 2;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  overflow: hidden;
  background: var(--board-bg);
  border: 1.5px solid rgba(227, 176, 75, .85);
  display: grid;
  place-items: center;
  color: #e3b04b;
  box-shadow: inset 0 0 0 2.5px var(--board-bg), inset 0 0 0 3.5px rgba(227, 176, 75, .45);
}
.crest .mark { width: 26px; height: 26px; display: block; }

/* ---------- the glint ----------
   Light passing across the gold, once, when the card arrives on screen (the
   IntersectionObserver in public/app.js adds .is-lit). A card that glitters on
   a loop is a slot machine; a card that catches the light as you scroll onto
   it is foil. Nothing else moves: not the court, not the score, and never the
   bell - an animated alert control would read as a control that is doing
   something. */
.sheen {
  position: absolute;
  top: -30%;
  bottom: -30%;
  left: 0;
  width: 26%;
  z-index: 3;
  pointer-events: none;
  opacity: 0;
  background: linear-gradient(90deg, transparent, rgba(255, 252, 238, .6) 48%, transparent);
  /* only legible over gold; over anything dark a screen blend is a white bar */
  mix-blend-mode: screen;
  transform: translateX(-170%) skewX(-18deg);
}
.is-lit .sheen { animation: glint .86s cubic-bezier(.25, .7, .3, 1) forwards; }
/* struck metal, so the medallion's sweep is wider and lands late - the same
   light reaching a curved surface after the flat one */
.crest .sheen { width: 46%; animation-delay: .26s; }
@keyframes glint {
  from { opacity: 1; transform: translateX(-170%) skewX(-18deg); }
  to { opacity: 1; transform: translateX(330%) skewX(-18deg); }
}

@media (prefers-reduced-motion: reduce) {
  .is-lit .sheen { animation: none; }
}

/* live badge - fill + ink text, legible on paper without darkening the fill */
.badge-live {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  background: var(--live);
  color: var(--on-live);
  font: 800 11px/1 var(--display);
  letter-spacing: .05em;
  text-transform: uppercase;
  border: 1.5px solid var(--ink);
  border-radius: 999px;
  padding: 5px 10px 5px 7px;
  flex: none;
}
.badge-live .dot-live { background: var(--on-live); border-color: var(--on-live); }

/* the pick control: the acid bell pill is now a real button and the *only*
   alert toggle - tapping the rest of the card opens the detail modal. live
   and finished matches have nothing left to alert about, so they show no
   control at all. Shared by the card and the modal footer. */
.bell-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex: none;
  border: 2px solid var(--ink);
  border-radius: 999px;
  background: var(--card);
  color: var(--ink);
  font: 800 11px/1 var(--display);
  letter-spacing: .01em;
  padding: 7px 12px 7px 10px;
  white-space: nowrap;
  cursor: pointer;
  /* the same hard offset the cards and chips carry - gives the control a bit
     of volume so pressing it lands with a click */
  box-shadow: 3px 3px 0 var(--ink-shadow);
  transition: transform .07s ease, box-shadow .07s ease;
}
.bell-pill .ico { width: 14px; height: 14px; }
.bell-pill.on { background: var(--acid); color: var(--on-acid); }
.bell-pill:active { transform: translate(3px, 3px); box-shadow: 0 0 0 var(--ink-shadow); }
.bell-pill:focus-visible { outline: 2px solid var(--acid); outline-offset: 2px; }
.bell-pill.on:focus-visible { outline-color: var(--ink); }

/* once the visitor knows the control (has a pick or a subscription) the word
   drops and it shrinks to the bell alone */
.bell-pill.pick-compact {
  width: 34px;
  height: 34px;
  padding: 0;
  justify-content: center;
}
.bell-pill.pick-compact .ico { width: 16px; height: 16px; }

/* ---------- match detail modal ----------
   Direction B: the board card's top-down court, opened up - a bigger
   headshot on each half, the umpire's board on the net, the full line-up
   below. Opened by tapping a card; the acid bell is the only control that
   does not open it. */
.m-scrim {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: grid;
  place-items: start center;
  overflow-y: auto;
  padding: 20px;
  background: rgba(0, 0, 0, .55);
}
.m-modal {
  width: 100%;
  max-width: 390px;
  margin: auto;
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 18px 18px 16px;
  background: var(--card);
  border: 2.5px solid var(--ink);
  border-radius: 18px;
  box-shadow: 6px 6px 0 var(--ink-shadow);
  animation: modal-in .16s ease-out;
}
@keyframes modal-in {
  from { transform: translateY(8px); }
}
@media (prefers-reduced-motion: reduce) { .m-modal { animation: none; } }

.m-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; }
.m-round {
  margin: 4px 0 0;
  font-family: var(--display);
  font-size: 19px;
  line-height: 1.15;
  font-weight: 800;
  font-stretch: 82%;
  letter-spacing: -.02em;
  text-transform: uppercase;
  color: var(--ink);
}
.m-close {
  flex: none;
  width: 30px;
  height: 30px;
  padding: 0;
  border: 2.5px solid var(--ink);
  border-radius: 50%;
  background: var(--card);
  color: var(--ink);
  display: grid;
  place-items: center;
  cursor: pointer;
}
.m-close:focus-visible { outline: 2px solid var(--acid); outline-offset: 2px; }

.m-status {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  align-self: flex-start;
  padding: 5px 10px;
  border: 1.5px solid var(--ink);
  border-radius: 999px;
  background: var(--line-soft);
  color: var(--ink);
  font: 800 10.5px/1 var(--display);
  letter-spacing: .08em;
  text-transform: uppercase;
}
.m-status.live { background: var(--live); color: var(--on-live); }
.m-status.next { background: var(--acid); color: var(--on-acid); }
.m-status .m-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }

/* Unlike the card, the modal's court stands on its end: a real ~10x20m
   court seen from behind the baseline, one team on the near half and one on
   the far. The width is capped and centred so the panel never runs the full
   modal height, and the service lines sit at the real ~15% / ~85% of the
   court's length rather than the card's looser quarters. */
.m-court {
  position: relative;
  width: min(100%, 255px);
  margin-inline: auto;
  aspect-ratio: 5 / 9;
  display: flex;
  border-radius: 12px;
  padding: 16px 14px;
  background-color: var(--court);
  background-image:
    linear-gradient(180deg, transparent calc(15% - .75px), var(--court-line) calc(15% - .75px), var(--court-line) calc(15% + .75px), transparent calc(15% + .75px)),
    linear-gradient(180deg, transparent calc(85% - .75px), var(--court-line) calc(85% - .75px), var(--court-line) calc(85% + .75px), transparent calc(85% + .75px));
  box-shadow: inset 0 0 0 1.5px var(--court-line);
  overflow: hidden;
}
.m-net {
  position: absolute;
  inset: 50% 0 auto 0;
  height: 2px;
  transform: translateY(-1px);
  background: var(--court-net);
}
.m-court.is-live .m-net { background: var(--live); }
.m-tee {
  position: absolute;
  top: 15%;
  bottom: 15%;
  left: 50%;
  width: 1.5px;
  transform: translateX(-.75px);
  background: var(--court-line);
}
.m-court .court-board { font: 600 13px/1 var(--mono); padding: 6px 9px; gap: 3px 9px; }

/* the occasion, in the modal: the same band and the same engraving, one size
   up. Opening the card should feel like opening the same object. */
.m-modal-occasion { border-color: var(--gold); }
.m-bill {
  position: relative;
  overflow: hidden;
  margin: -18px -18px 0;
  padding: 13px 18px 12px;
  background: linear-gradient(100deg, var(--gold-deep) 0 34%, var(--gold) 34% 61%, var(--gold-deep) 61%);
  color: var(--gold-ink);
  border-radius: 15.5px 15.5px 0 0;
  display: flex;
  align-items: center;
  gap: 12px;
}
.m-bill .mark { width: 24px; height: 24px; flex: none; position: relative; }
.m-bill .bill-t { font-size: 23px; }

.m-court-occasion {
  background-image:
    linear-gradient(180deg, transparent calc(15% - .75px), var(--court-line) calc(15% - .75px), var(--court-line) calc(15% + .75px), transparent calc(15% + .75px)),
    linear-gradient(180deg, transparent calc(85% - .75px), var(--court-line) calc(85% - .75px), var(--court-line) calc(85% + .75px), transparent calc(85% + .75px)),
    repeating-conic-gradient(from 0deg at 50% 50%, rgba(227, 176, 75, .15) 0 1.6deg, transparent 1.6deg 8.5deg);
  box-shadow:
    inset 0 0 0 1.5px rgba(227, 176, 75, .55),
    inset 0 0 0 5px var(--court),
    inset 0 0 0 6.5px rgba(227, 176, 75, .3);
}
/* At 64px the drawing reads as a drawing rather than as a mark - the card's
   26px version only has to be recognisable. */
.m-crest { width: 64px; height: 64px; }
.m-crest .mark { width: 32px; height: 32px; }

.m-halves {
  position: relative;
  flex: 1;
  display: grid;
  grid-template-rows: 1fr 1fr;
  row-gap: 20px;
}
.m-half {
  position: relative;
  display: flex;
  justify-content: space-evenly;
  gap: 14px;
}
/* each player takes an equal share of the half and no more, so a long
   surname wraps by its headshot instead of stretching the block wide
   and being clipped by the court's edge */
.m-cp { flex: 1 1 0; min-width: 0; max-width: 140px; }
.m-half-t { align-items: flex-start; }
.m-half-b { align-items: flex-end; }
.m-half.lost { opacity: .5; }
.m-won {
  position: absolute;
  left: 0;
  padding: 3px 5px;
  border-radius: 4px;
  background: var(--acid);
  color: var(--on-acid);
  font: 800 8.5px/1 var(--display);
  letter-spacing: .1em;
  text-transform: uppercase;
}
/* the players sit at the back of each half, so the winner tag goes the
   other way - tucked against the net on the winning side. */
.m-half-t .m-won { bottom: -4px; }
.m-half-b .m-won { top: -4px; }

.m-cp { display: flex; flex-direction: column; align-items: center; gap: 6px; text-align: center; }
/* The near half is flipped so the name sits above the headshot, pointing at
   the net - the mirror of the far half, whose name sits below for the same
   reason. Both teams' avatars stay anchored to their own baselines. */
.m-half-b .m-cp { flex-direction: column-reverse; }
.m-av-wrap {
  position: relative;
  width: 58px;
  height: 58px;
  flex: none;
  border: 2px solid var(--court-net);
  border-radius: 50%;
  overflow: hidden;
  background: var(--court-mono);
}
.m-av { display: block; width: 100%; height: 100%; object-fit: cover; }

/* Loading state: a pulsing tinted disc over the wrapper while the profile
   resolves or the headshot decodes. Same opacity pulse the live dots use. */
.m-av-skel {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  animation: pulse 1.9s ease-in-out infinite;
}
.m-av-wrap .m-av-skel { background: var(--court-mono); color: var(--court-mono-fg); }
.m-lu-av .m-av-skel { background: var(--mono-chip); color: var(--mono-chip-fg); }
.m-av-skel svg { display: block; width: 62%; height: 62%; }
/* The flag is pulled out of the name's own box and hung in the left margin, so
   the surname stays centred on the headshot however many lines it wraps to. */
.m-cp-row { position: relative; display: flex; justify-content: center; max-width: 100%; min-width: 0; }
.m-cp-row .flag { position: absolute; right: 100%; margin-right: 6px; top: 1px; }
.m-sname {
  font: 700 13.5px/1.1 var(--display);
  letter-spacing: -.01em;
  color: var(--court-ink);
  min-width: 0;
  overflow-wrap: break-word;
}
.m-sname .sd { color: var(--court-dim); font-weight: 500; }

.m-lineup { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 14px; }
.m-lu { display: flex; align-items: center; gap: 9px; min-width: 0; }
.m-lu-av {
  position: relative;
  width: 32px;
  height: 32px;
  flex: none;
  border: 1.5px solid var(--ink);
  border-radius: 50%;
  overflow: hidden;
  background: var(--mono-chip);
}
.m-mono {
  display: grid;
  place-items: center;
  width: 100%;
  height: 100%;
  background: var(--mono-chip);
  color: var(--mono-chip-fg);
  font: 800 11px/1 var(--display);
  letter-spacing: .02em;
}
.m-lu-txt { min-width: 0; }
.m-full {
  margin: 0;
  font-size: 12.5px;
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -.01em;
  color: var(--ink);
}
.m-nick { margin: 1px 0 0; font-size: 11px; color: var(--faint); }
.m-nick::before { content: '\201C'; }
.m-nick::after { content: '\201D'; }

.m-foot {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  padding-top: 12px;
  border-top: 1.5px solid var(--line-soft);
  font: 400 11.5px/1.4 var(--mono);
  color: var(--faint);
}
.m-foot .ico { flex: none; }
.m-foot-row { justify-content: space-between; }
.m-foot-row > span { display: inline-flex; align-items: center; gap: 6px; min-height: 20px; }

/* first-run nudge above the court list - see BoardHint in app.js */
.board-hint {
  display: flex;
  gap: 8px;
  align-items: flex-start;
  margin: 0;
  padding: 10px 12px;
  border: 1.5px solid var(--acid-dim);
  border-radius: 12px;
  color: var(--faint);
  font: 400 12px/1.5 var(--mono);
}
.board-hint .ico { flex: none; margin-top: 1px; color: var(--acid-text); }

/* iOS Home Screen install guidance - see IosInstallCard in app.js. A real
   card at the top of the board on mobile; hidden on desktop (below). */
.ios-install {
  display: flex;
  flex-direction: column;
  gap: 12px;
  background: var(--card);
  border: 2.5px solid var(--ink);
  border-radius: 18px;
  padding: 16px;
  box-shadow: 5px 5px 0 var(--ink-shadow);
}
.ios-kicker {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  font: 800 11px/1 var(--display);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--acid-text);
}
.ios-kicker .ico { flex: none; color: var(--acid-text); }
.ios-lead { margin: 0; font-size: 14px; line-height: 1.45; color: var(--ink); }
.ios-copy {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font: 800 12px/1 var(--display);
  letter-spacing: .02em;
  text-transform: uppercase;
  background: var(--acid);
  color: var(--on-acid);
  border: 2px solid var(--ink);
  border-radius: 999px;
  padding: 10px 15px;
  cursor: pointer;
  box-shadow: 3px 3px 0 var(--ink-shadow);
}
.ios-copy.is-done {
  background: transparent;
  color: var(--acid-text);
  box-shadow: none;
}
.ios-steps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 9px;
}
.ios-steps li {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  font: 400 12.5px/1.5 var(--mono);
  color: var(--faint);
}
.ios-n {
  flex: none;
  width: 19px;
  height: 19px;
  margin-top: 1px;
  border: 1.5px solid var(--acid-dim);
  border-radius: 50%;
  color: var(--acid-text);
  display: grid;
  place-items: center;
  font: 700 10px/1 var(--display);
}
.ios-n .ico { width: 11px; height: 11px; }
.ios-glyph { vertical-align: -2px; margin-left: 3px; color: var(--ink); }

.board-note { color: var(--faint); font: 400 11.5px/1.7 var(--mono); max-width: 46ch; }
.page-note { text-align: center; color: var(--faintest); padding: 4px 0 0; }
.page-note a { color: inherit; text-decoration: underline; }

/* ---------- skeleton ---------- */
.skel { background: var(--card); border: 2.5px solid var(--line); border-radius: 18px; height: 118px; }
.skel-head { height: 33px; border-bottom: 2.5px solid var(--line); }

/* ---------- fixed footer bar ---------- */
.rail-action {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 5;
  background: var(--bar-bg);
  color: var(--bar-ink);
  border-top: 2.5px solid #000;
  padding: 13px 20px calc(13px + env(safe-area-inset-bottom));
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}
.action-status,
.action-row { width: 100%; max-width: var(--shell-w); }
.action-status { color: var(--bar-dim); }
.action-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
.count-label { color: var(--bar-dim); flex: none; }
.cta {
  font: 800 15px/1 var(--display);
  letter-spacing: .01em;
  text-transform: uppercase;
  background: var(--acid);
  color: var(--on-acid);
  border: 2.5px solid #000;
  border-radius: 999px;
  padding: 15px 22px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  cursor: pointer;
  box-shadow: 4px 4px 0 #000;
}
.cta:disabled { opacity: .45; cursor: not-allowed; box-shadow: none; }
.cta-ghost {
  background: transparent;
  color: var(--bar-ink);
  border-color: var(--bar-ink);
  box-shadow: none;
}

/* ---------- motion: a few things, once each ---------- */
@keyframes pulse { 0%, 100% { opacity: 1 } 50% { opacity: .25 } }
@keyframes rise { from { opacity: 0; transform: translateY(14px) } to { opacity: 1; transform: none } }

.dot, .chip[data-live="true"] .cdot, .badge-live .dot-live { animation: pulse 1.9s ease-in-out infinite; }
.col { animation: rise .6s cubic-bezier(.2,.7,.3,1) both; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; }
}

/* ---------- phone ---------- */
@media (max-width: 480px) {
  .shell { padding: 0 16px 148px; gap: 20px; }
  .search-dock[data-stuck="true"] { margin: 0 -16px; }
  .index { margin: 0 -16px; padding-left: 16px; padding-right: 16px; }
  .columns { margin: 0 -16px; }
  .col { padding: 0 16px; scroll-margin-left: 16px; }
  .hero { font-size: 54px; }
  .hero-note { font-size: 13px; top: 2px; }

  /* tighten the court a notch for the narrowest screens */
  .court { padding: 10px; }
  .halves { column-gap: 24px; }
  .half { min-height: 84px; }
  .pname { font-size: 13px; letter-spacing: -.01em; }
  .court-board { gap: 3px 6px; padding: 5px 7px; font-size: 11.5px; }
  .bcell { min-width: 8px; }
}

/* ---------- desktop: split the shell - a fixed left rail, courts fill the rest ----------
   Below 1000px the board is one court per screen, swiped between. On a wide
   screen the brand, headline, search and court list move into a rail that
   never scrolls, and the courts take the width that frees up - as many as
   fit at full card size, the rest a horizontal scroll away, each column
   scrolling its own cards under a sticky header. */
@media (min-width: 1000px) {
  :root { --shell-w: 1140px; }

  /* the intro screen keeps its single centred column and oversized headline */
  .shell[data-mode="onboarding"] { gap: 26px; }
  .shell[data-mode="onboarding"] .hero { font-size: 78px; }
  .shell[data-mode="onboarding"] .hero-wrap { align-self: flex-start; }
  /* the intro column runs the full 1140px, which would stretch the install
     card into a banner; hold it to a readable measure under the headline.
     It stays visible here (unlike the board below) because onboarding has no
     action bar whose reason line could carry the message instead - and an
     iPad in landscape lands on this screen at this width. */
  .shell[data-mode="onboarding"] .ios-install {
    align-self: flex-start;
    width: 100%;
    max-width: 520px;
  }

  /* --- the split: the board, and a search that narrows it in place --- */
  .shell[data-mode="board"],
  .shell[data-mode="filtered"] {
    max-width: 2200px;
    height: 100vh;
    min-height: 0;
    padding: 22px 32px;
    overflow: hidden;
    display: grid;
    grid-template-columns: 312px minmax(0, 1fr);
    grid-template-rows: auto auto auto auto minmax(0, 1fr) auto auto;
    grid-template-areas:
      "brand   board"
      "badge   board"
      "hero    board"
      "search  board"
      "nav     board"
      "action  board"
      "note    board";
    column-gap: 42px;
    row-gap: 14px;
    align-items: start;
  }

  .shell[data-mode="board"] > .topbar,
  .shell[data-mode="filtered"] > .topbar { grid-area: brand; padding-top: 0; }
  .shell[data-mode="board"] > .now-badge,
  .shell[data-mode="filtered"] > .now-badge { grid-area: badge; justify-self: start; }
  .shell[data-mode="board"] > .hero-wrap,
  .shell[data-mode="filtered"] > .hero-wrap { grid-area: hero; }
  .shell[data-mode="board"] > .search-dock,
  .shell[data-mode="filtered"] > .search-dock { grid-area: search; }
  .shell[data-mode="board"] > .index,
  .shell[data-mode="filtered"] > .index { grid-area: nav; align-self: stretch; min-height: 0; }
  .shell[data-mode="board"] > .rail-action,
  .shell[data-mode="filtered"] > .rail-action { grid-area: action; }
  .shell[data-mode="board"] > .board,
  .shell[data-mode="filtered"] > .board { grid-area: board; align-self: stretch; min-height: 0; }
  /* the credit line sits at the foot of the rail, under the action button */
  .shell[data-mode="board"] > .page-note,
  .shell[data-mode="filtered"] > .page-note {
    grid-area: note;
    align-self: end;
    justify-self: start;
    text-align: left;
    padding: 2px 0 0;
  }
  /* the first-run nudge would be a full-width banner over the courts; on
     desktop the bell buttons still carry their "Alert me" word, which is
     enough of a cue */
  .shell[data-mode="board"] .board-hint { display: none; }
  /* the install card is a mobile concern - the only desktop route to it is an
     iPad in this layout, and the action-bar reason line already says it */
  .shell[data-mode="board"] .ios-install,
  .shell[data-mode="filtered"] .ios-install { display: none; }

  /* rail headline: a fraction of the phone size so it fits the column, its
     hand-note dropping below it instead of floating off to the side */
  .shell[data-mode="board"] .hero,
  .shell[data-mode="filtered"] .hero { font-size: 44px; }
  .shell[data-mode="board"] .hero-note,
  .shell[data-mode="filtered"] .hero-note {
    position: static;
    transform: none;
    margin: 10px 0 0;
    max-width: none;
    white-space: normal;
    text-align: left;
  }

  /* search sits in the rail flow - drop the sticky band treatment */
  .shell[data-mode="board"] > .search-dock[data-stuck="true"],
  .shell[data-mode="filtered"] > .search-dock[data-stuck="true"] {
    position: static;
    margin: 0;
    padding: 0;
    background: none;
  }

  /* court index: a vertical list filling the rail width, the current court
     filled, scrolling inside the rail if the day runs long */
  .shell[data-mode="board"] > .index,
  .shell[data-mode="filtered"] > .index {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 3px;
    overflow-x: visible;
    overflow-y: auto;
    padding: 2px 0;
    margin: 0;
  }
  .shell[data-mode="board"] .chip,
  .shell[data-mode="filtered"] .chip {
    width: 100%;
    justify-content: flex-start;
    border-color: transparent;
    background: none;
    box-shadow: none;
    min-height: 34px;
    padding: 7px 12px;
  }
  .shell[data-mode="board"] .chip:hover,
  .shell[data-mode="filtered"] .chip:hover { background: var(--card-pick); }
  .shell[data-mode="board"] .chip .lbl,
  .shell[data-mode="filtered"] .chip .lbl { flex: 1 1 auto; }
  .shell[data-mode="board"] .chip[aria-current="true"],
  .shell[data-mode="filtered"] .chip[aria-current="true"] { background: var(--ink); }

  /* the action bar stops spanning the viewport - it becomes a dark slab
     pinned to the foot of the rail: same tokens, same single anchor */
  .shell[data-mode="board"] > .rail-action,
  .shell[data-mode="filtered"] > .rail-action {
    position: static;
    inset: auto;
    align-self: end;
    align-items: stretch;
    gap: 10px;
    border: 2.5px solid #000;
    border-radius: 16px;
    box-shadow: 4px 4px 0 #000;
    padding: 14px 15px 15px;
  }
  .shell[data-mode="board"] .action-status,
  .shell[data-mode="board"] .action-row,
  .shell[data-mode="filtered"] .action-status,
  .shell[data-mode="filtered"] .action-row { max-width: none; }
  /* a 312px rail can't sit the count beside the button - stack them */
  .shell[data-mode="board"] .rail-action .action-row,
  .shell[data-mode="filtered"] .rail-action .action-row {
    flex-direction: column;
    align-items: stretch;
    gap: 10px;
  }
  .shell[data-mode="board"] .rail-action .cta,
  .shell[data-mode="filtered"] .rail-action .cta { width: 100%; }

  /* the courts: one horizontal row of columns at the phone's card width
     (--col-w). The row itself is the single scroll region - wheel runs
     down the day, trackpad or shift-wheel moves across courts - so there
     are no per-column scrollers to swallow a vertical wheel. Column
     headers stick to the top of that one scrollport as their court
     scrolls past. */
  .shell[data-mode="board"] > .board,
  .shell[data-mode="filtered"] > .board {
    display: flex;
    flex-direction: column;
    gap: 12px;
    height: 100%;
    overflow: hidden;
  }
  .shell[data-mode="board"] .columns,
  .shell[data-mode="filtered"] .columns {
    flex: 1 1 auto;
    flex-direction: row;
    align-items: flex-start;
    min-height: 0;
    gap: 18px;
    margin: 0;
    padding: 0 4px 4px 0;
    overflow: auto;
    scroll-snap-type: none;
    scrollbar-width: thin;
  }
  .shell[data-mode="board"] .columns::-webkit-scrollbar,
  .shell[data-mode="filtered"] .columns::-webkit-scrollbar { display: block; width: 8px; height: 8px; }
  /* full card width, never crushed - as many as the screen holds show at
     once, the rest a scroll away (a wide monitor shows the whole day) */
  .shell[data-mode="board"] .col,
  .shell[data-mode="filtered"] .col {
    flex: 0 0 var(--col-w);
    width: var(--col-w);
    min-width: 0;
    padding: 0;
    scroll-snap-align: none;
  }
  .shell[data-mode="board"] .colhead,
  .shell[data-mode="filtered"] .colhead {
    position: sticky;
    top: 0;
    z-index: 2;
    background: var(--page-bg);
    padding-top: 2px;
  }
}
