/* ============================================================
   Pick a Court — Cebu
   Clean, light-first, Airbnb-inspired: white ground, hairline
   borders, generous radii, one coral accent reserved for primary
   actions and favourites. Green is semantic (booked/played) only.
   Information hierarchy comes from weight and spacing, not hue.
   ============================================================ */

:root {
  --ground: #ffffff;
  --surface: #ffffff;
  --surface-2: #f7f7f7;
  --ink: #222222;
  --ink-2: #5e5e5e;
  /* #8a8a8a was only 3.45:1 on white and 3.22:1 on --surface-2, so every
     stat label, result count and helper note sat under AA. #6e6e6e holds
     5.10:1 on white and 4.76:1 on the grey track. */
  --ink-3: #6e6e6e;
  --line: #ebebeb;
  --line-2: #dddddd;

  /* White-on-accent is the primary CTA. #e0455c gave only 4.06:1 at 14px;
     #d13a52 keeps the coral and reaches 4.73:1, hover 5.84:1. */
  --accent: #d13a52;
  --accent-2: #b8324a;
  --accent-ink: #ffffff;
  --accent-wash: #fdf0f2;

  /* #0e8a5f was 4.36:1 on white — just under AA at the 12.5px used by
     "Free" rates and the booked/played badges. */
  --good: #0c7b54;
  --good-wash: #e9f6f0;

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.06);
  --shadow-md: 0 6px 16px rgba(0, 0, 0, 0.12);
  --shadow-lg: 0 8px 28px rgba(0, 0, 0, 0.18);

  --font-body: "Figtree", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;

  --r-card: 16px;
  --r-input: 12px;
  --r-pill: 999px;
  --wrap: 1180px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --ground: #141414;
    --surface: #1e1e1e;
    --surface-2: #262626;
    --ink: #f0f0f0;
    --ink-2: #b3b3b3;
    --ink-3: #8c8c8c;
    --line: #2e2e2e;
    --line-2: #404040;

    --accent: #ff5a76;
    --accent-2: #ff7089;
    --accent-ink: #1a1a1a;
    --accent-wash: #33212530;

    --good: #34c88a;
    --good-wash: #1d322833;

    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
    --shadow-md: 0 6px 16px rgba(0, 0, 0, 0.5);
    --shadow-lg: 0 8px 28px rgba(0, 0, 0, 0.6);
  }
}

/* The page follows the OS theme by default; the header toggle stamps
   data-theme on <html>, which must beat the media query both ways —
   hence full redeclaration, not partial override. */
:root[data-theme="light"] {
  --ground: #ffffff; --surface: #ffffff; --surface-2: #f7f7f7;
  --ink: #222222; --ink-2: #5e5e5e; --ink-3: #6e6e6e;
  --line: #ebebeb; --line-2: #dddddd;
  --accent: #d13a52; --accent-2: #b8324a; --accent-ink: #ffffff; --accent-wash: #fdf0f2;
  --good: #0c7b54; --good-wash: #e9f6f0;
  --shadow-sm: 0 1px 2px rgba(0,0,0,.06);
  --shadow-md: 0 6px 16px rgba(0,0,0,.12);
  --shadow-lg: 0 8px 28px rgba(0,0,0,.18);
}
:root[data-theme="dark"] {
  --ground: #141414; --surface: #1e1e1e; --surface-2: #262626;
  --ink: #f0f0f0; --ink-2: #b3b3b3; --ink-3: #8c8c8c;
  --line: #2e2e2e; --line-2: #404040;
  --accent: #ff5a76; --accent-2: #ff7089; --accent-ink: #1a1a1a; --accent-wash: #33212530;
  --good: #34c88a; --good-wash: #1d322833;
  --shadow-sm: 0 1px 2px rgba(0,0,0,.4);
  --shadow-md: 0 6px 16px rgba(0,0,0,.5);
  --shadow-lg: 0 8px 28px rgba(0,0,0,.6);
}

* { box-sizing: border-box; }

/* The hidden attribute must always win. Without this, any element that
   also has an explicit display (e.g. .grid { display: grid }) ignores
   hidden entirely — the UA's [hidden] rule loses to any authored display. */
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 15px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

.wrap { width: 100%; max-width: var(--wrap); margin-inline: auto; padding-inline: 24px; }

.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;
}
.skip {
  position: absolute; left: -9999px; top: 8px; z-index: 20;
  background: var(--ink); color: var(--ground); padding: 8px 14px; border-radius: var(--r-pill);
}
.skip:focus { left: 24px; }

:where(a, button, input, select, textarea):focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
  border-radius: 4px;
}

button { font-family: inherit; }

/* ─────────────── masthead ─────────────── */
.masthead {
  background: var(--ground);
  border-bottom: 1px solid var(--line);
  position: sticky; top: 0; z-index: 10;
}
/* padding-block, not min-height alone: this wraps onto two lines on a
   phone, and once it does the min-height stops governing — leaving the
   tab pill sitting directly on the header's bottom border. */
.masthead-in {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px 16px; min-height: 68px; flex-wrap: wrap;
  padding-block: 10px;
}
.brand { display: flex; align-items: center; gap: 10px; }
.brand-mark { color: var(--accent); display: block; }
.brand-mark svg { width: 28px; height: 28px; display: block; }
.brand-text { display: flex; flex-direction: column; line-height: 1.2; }
.brand-text strong { font-size: 17px; font-weight: 800; letter-spacing: -0.01em; }
.brand-sub { font-size: 12px; color: var(--ink-3); font-weight: 500; }

.header-tools { display: flex; align-items: center; gap: 8px; }

.tabs { display: flex; gap: 4px; background: var(--surface-2); padding: 4px; border-radius: var(--r-pill); }
.tab {
  font-size: 13.5px; font-weight: 600;
  background: none; border: 0; color: var(--ink-2);
  padding: 8px 16px; border-radius: var(--r-pill); cursor: pointer;
  display: flex; align-items: center; gap: 6px;
  transition: color .15s ease, background-color .15s ease;
}
.tab:hover { color: var(--ink); }
.tab[aria-selected="true"] { color: var(--ground); background: var(--ink); }
.tab-count {
  font-family: var(--font-mono); font-size: 10.5px;
  background: var(--accent); color: var(--accent-ink);
  padding: 1px 6px; border-radius: var(--r-pill);
}

.theme-btn {
  background: none; border: 1px solid var(--line-2); color: var(--ink-2);
  width: 38px; height: 38px; border-radius: 50%; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  transition: border-color .15s ease, color .15s ease;
}
.theme-btn:hover { border-color: var(--ink-3); color: var(--ink); }
.theme-btn svg { width: 16px; height: 16px; }
/* No data-theme: follow the system. */
.ico-sun { display: none; }
.ico-moon { display: block; }
@media (prefers-color-scheme: dark) {
  .ico-sun { display: block; }
  .ico-moon { display: none; }
}
:root[data-theme="dark"] .ico-sun { display: block; }
:root[data-theme="dark"] .ico-moon { display: none; }
:root[data-theme="light"] .ico-sun { display: none; }
:root[data-theme="light"] .ico-moon { display: block; }

/* ─────────────── stat strip ───────────────
   One quiet line of number+label pairs instead of KPI tiles: these are
   mostly facts, not metrics, and typography carries them fine. Hairline
   dividers between items; wraps naturally on narrow screens. */
.statbar {
  display: flex; flex-wrap: wrap; align-items: baseline;
  row-gap: 8px; margin: 20px 0 18px;
}
.stat-item {
  display: inline-flex; align-items: baseline; gap: 6px;
  font-size: 13px; color: var(--ink-3); white-space: nowrap;
}
.stat-item b {
  font-size: 16px; font-weight: 700; color: var(--ink);
  letter-spacing: -0.01em; font-variant-numeric: tabular-nums;
  max-width: 180px; overflow: hidden; text-overflow: ellipsis;
}
.stat-item + .stat-item { margin-left: 14px; padding-left: 14px; border-left: 1px solid var(--line); }
.stat-item[title] { cursor: help; }

/* ─────────────── controls ─────────────── */
.controls { display: flex; gap: 10px; margin-bottom: 14px; flex-wrap: wrap; }
.field { position: relative; display: flex; }
.field.search { flex: 1 1 300px; }
.search-ico {
  position: absolute; left: 16px; top: 50%; transform: translateY(-50%);
  width: 15px; height: 15px; color: var(--ink-3); pointer-events: none;
}

input, select, textarea {
  font-family: var(--font-body); font-size: 14.5px; color: var(--ink);
  background: var(--surface); border: 1px solid var(--line-2);
  border-radius: var(--r-input); padding: 10px 14px; width: 100%;
  transition: border-color .15s ease, box-shadow .15s ease;
}
input:hover, select:hover, textarea:hover { border-color: var(--ink-3); }
input:focus, select:focus, textarea:focus { border-color: var(--ink); }
textarea { resize: vertical; }
input::placeholder, textarea::placeholder { color: var(--ink-3); }
select {
  cursor: pointer;
  padding-right: 42px;
  appearance: none;
  background-image:
    linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position:
    calc(100% - 20px) 50%,
    calc(100% - 15px) 50%;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}

input[type="search"] {
  padding-left: 42px; border-radius: var(--r-pill);
  box-shadow: var(--shadow-sm);
}
input[type="search"]:focus { box-shadow: var(--shadow-md); }
.field select { border-radius: var(--r-pill); }

/* ─────────────── catalog layout: filter rail + content ─────────────── */
.catalog {
  display: grid; grid-template-columns: 248px minmax(0, 1fr);
  gap: 32px; align-items: start;
}
.main-col { min-width: 0; }

.sidebar {
  position: sticky; top: 84px;
  max-height: calc(100vh - 100px); overflow-y: auto;
  padding-right: 6px;
}
.sidebar-head {
  display: flex; align-items: baseline; justify-content: space-between;
  margin-bottom: 6px;
}
.sidebar-title { font-size: 16px; font-weight: 700; letter-spacing: -0.01em; margin: 0; }

.facet { padding: 14px 0; border-bottom: 1px solid var(--line); }
.facet:last-of-type { border-bottom: 0; }
.facet-title {
  font-size: 13px; font-weight: 700; color: var(--ink);
  margin: 0 0 8px;
}
.facet-list { display: flex; flex-direction: column; gap: 2px; }

.facet-row {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  width: 100%; text-align: left;
  font-size: 14px; font-weight: 500; color: var(--ink-2);
  background: none; border: 0; cursor: pointer;
  padding: 7px 10px; border-radius: 8px;
  transition: background-color .12s ease, color .12s ease;
}
.facet-row:hover { background: var(--surface-2); color: var(--ink); }
/* Selected facet goes solid ink — the accent stays reserved for actions. */
.facet-row[aria-pressed="true"] { background: var(--ink); color: var(--ground); }
.facet-n { font-family: var(--font-mono); font-size: 11px; opacity: .6; }

.facet-check {
  display: flex; align-items: center; gap: 9px;
  font-size: 14px; font-weight: 500; color: var(--ink-2); cursor: pointer;
  padding: 7px 10px; border-radius: 8px;
  transition: background-color .12s ease, color .12s ease;
}
.facet-check:hover { background: var(--surface-2); color: var(--ink); }
.facet-check input { width: 16px; height: 16px; margin: 0; accent-color: var(--ink); flex-shrink: 0; }

/* The Apply button exists only for the mobile sheet. */
.sidebar-apply { display: none; }
.sidebar-backdrop { display: none; }

/* Mobile: the rail becomes a bottom sheet behind the Filters button. */
.filters-btn {
  display: none;
  align-items: center; gap: 7px;
  font-size: 14px; font-weight: 600; color: var(--ink);
  background: var(--surface); border: 1px solid var(--line-2);
  padding: 10px 16px; border-radius: var(--r-pill); cursor: pointer;
}
.filters-btn svg { width: 15px; height: 15px; }
.filter-badge {
  font-family: var(--font-mono); font-size: 10.5px;
  background: var(--ink); color: var(--ground);
  padding: 1px 6px; border-radius: var(--r-pill);
}

@media (max-width: 900px) {
  .catalog { display: block; }
  .filters-btn { display: inline-flex; }

  .sidebar { display: none; }
  .sidebar.open {
    display: block;
    position: fixed; left: 0; right: 0; bottom: 0; top: auto; z-index: 40;
    max-height: 78vh; overflow-y: auto;
    background: var(--surface);
    border-radius: 20px 20px 0 0;
    padding: 20px 20px calc(16px + env(safe-area-inset-bottom));
    box-shadow: var(--shadow-lg);
  }
  .sidebar.open .sidebar-apply {
    display: block; width: 100%;
    position: sticky; bottom: 0; margin-top: 10px;
  }
  .sidebar-backdrop.show {
    display: block;
    position: fixed; inset: 0; z-index: 39;
    background: rgba(0, 0, 0, .45);
  }
}

/* ─────────────── sync ─────────────── */
.sync-btn {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 13px; font-weight: 600; color: var(--ink-2);
  background: var(--surface); border: 1px solid var(--line-2);
  padding: 10px 14px; border-radius: var(--r-pill); cursor: pointer;
  transition: color .15s ease, border-color .15s ease;
}
.sync-btn:hover { color: var(--ink); border-color: var(--ink-3); }
.sync-btn:disabled { opacity: .55; cursor: default; }
.sync-btn svg { width: 14px; height: 14px; flex-shrink: 0; }
.sync-btn.busy svg { animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

.share-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; flex: 0 0 auto;
  background: var(--surface); color: var(--ink-2);
  border: 1px solid var(--line-2); border-radius: var(--r-pill);
  cursor: pointer;
  transition: color .15s ease, border-color .15s ease, background-color .15s ease;
}
.share-btn:hover { color: var(--ink); border-color: var(--ink-3); background: var(--surface-2); }
.share-btn svg { width: 16px; height: 16px; }

/* ─────────────── show more ─────────────── */
.more-wrap { display: flex; justify-content: center; margin-top: 24px; }
.show-more {
  font-size: 14px; font-weight: 600;
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--ink); border-radius: 10px;
  padding: 11px 22px; cursor: pointer;
  transition: background-color .15s ease;
}
.show-more:hover { background: var(--surface-2); }

.btn {
  font-size: 14px; font-weight: 600;
  background: var(--accent); color: var(--accent-ink);
  border: 1px solid var(--accent); border-radius: 10px;
  padding: 10px 18px; cursor: pointer;
  transition: background-color .15s ease, border-color .15s ease, transform .1s ease;
}
.btn:hover { background: var(--accent-2); border-color: var(--accent-2); }
.btn:active { transform: scale(.98); }
.btn-ghost {
  font-size: 13px; font-weight: 500;
  background: none; border: 1px solid transparent; color: var(--ink-2);
  padding: 6px 10px; border-radius: 8px; cursor: pointer;
  text-decoration: underline; text-underline-offset: 2px; text-decoration-color: transparent;
  transition: color .15s ease, text-decoration-color .15s ease;
}
.btn-ghost:hover { color: var(--ink); text-decoration-color: var(--ink-3); }

.result-line {
  font-size: 13px; color: var(--ink-3); margin: 0 0 16px;
  font-variant-numeric: tabular-nums;
}

.notice {
  display: inline-flex; align-items: center; gap: 7px;
  width: fit-content;
  font-size: 12.5px; font-weight: 700; color: var(--ink);
  background: var(--surface-2); border: 1px solid var(--line-2);
  padding: 6px 10px; border-radius: var(--r-pill);
}
.offline-pill { margin: -4px 0 14px; }
/* Coral, not grey: unsent work is the one state here worth chasing. */
.pending-pill {
  margin: -4px 0 14px;
  color: var(--accent); background: var(--accent-wash); border-color: var(--accent);
}
.notice-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-wash);
}

/* ─────────────── time context ───────────────
   Sits above the sort and filters because it changes the question they
   answer. Deliberately not a <select>: the whole value is seeing that 7pm is
   two taps from 5pm, and that some hours are busier than others. */
.at-bar { margin: 0 0 14px; }
.at-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 7px; }
.at-label {
  font-size: 11.5px; font-weight: 700; color: var(--ink-3);
  text-transform: uppercase; letter-spacing: .07em;
}
.at-clear {
  font-family: var(--font-body); font-size: 12px; font-weight: 600;
  background: none; border: 0; cursor: pointer; padding: 0;
  color: var(--accent);
}
.at-clear:hover { color: var(--accent-2); text-decoration: underline; }

.at-strip {
  display: flex; gap: 5px;
  overflow-x: auto; scroll-behavior: smooth;
  padding-bottom: 3px;
  scrollbar-width: none;
}
.at-strip::-webkit-scrollbar { display: none; }
.at-slot {
  flex: none; cursor: pointer; white-space: nowrap;
  font-family: var(--font-mono); font-size: 12px; font-weight: 500;
  padding: 8px 13px; border-radius: var(--r-pill);
  background: var(--surface-2); color: var(--ink-2);
  border: 1px solid transparent;
  transition: color .15s ease, border-color .15s ease, background-color .15s ease;
}
.at-slot:hover { color: var(--ink); border-color: var(--line-2); }
/* The current hour is marked even when unselected — it is the one people
   mean by "now", and finding it in a scrolling strip should be free. */
.at-slot.is-now { border-color: var(--line-2); color: var(--ink); }
.at-slot.is-on { background: var(--ink); border-color: var(--ink); color: var(--ground); font-weight: 600; }
.at-note { font-size: 12px; color: var(--ink-3); margin: 7px 2px 0; min-height: 1em; }

.near-btn {
  display: inline-flex; align-items: center; gap: 7px; white-space: nowrap;
  font-family: var(--font-body); font-size: 13px; font-weight: 600;
  padding: 9px 15px; border-radius: var(--r-pill); cursor: pointer;
  background: var(--surface); color: var(--ink-2);
  border: 1px solid var(--line-2);
  transition: color .15s ease, border-color .15s ease, background-color .15s ease;
}
.near-btn:hover { color: var(--ink); border-color: var(--ink-3); }
.near-btn svg { width: 15px; height: 15px; flex-shrink: 0; }
.near-btn.is-on { background: var(--ink); border-color: var(--ink); color: var(--ground); }

/* ─────────────── court grid ─────────────── */
.grid {
  /* Default (stretch) alignment on purpose: every card in a row gets the
     row's height, and .court-foot's margin-top:auto absorbs the difference —
     so a 1-tier card and a 3-tier card sit flush top and bottom. */
  display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 16px;
}
.court {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-card); padding: 18px;
  display: flex; flex-direction: column; gap: 11px;
  transition: box-shadow .2s ease, transform .2s ease, border-color .2s ease;
  content-visibility: auto;
  contain-intrinsic-size: 260px;
  /* Named so the query below reads against the box the card was placed in
     rather than the viewport. This is what lets the same markup serve the
     grid and the map popup: the popup is not narrow because the phone is
     narrow, it is narrow because it is a popup — which a media query has no
     way to express. */
  container: court / inline-size;
}

/* Compact form. Everything the card would show is still there; the parts
   that need room are dropped, so a pin gives you the name, the price, a
   booking link, the favourite and Log — never a dead end. */
@container court (max-width: 260px) {
  .court { padding: 0; border: 0; background: none; gap: 8px; }
  .court:hover { box-shadow: none; transform: none; }
  /* Only one rate fits. With an hour set that has to be the tier live at
     that hour, not whichever tier happens to be listed first — showing
     6am–4pm while the badge says "Open 7pm" would contradict itself. */
  .court .rates:has(.is-now) .rate:not(.is-now) { display: none; }
  .court .rates:not(:has(.is-now)) .rate:nth-child(n + 2) { display: none; }
  .court .badges:last-child { display: none; }
  .court .court-name { font-size: 15px; }
  .court .book { padding: 6px 12px; font-size: 12.5px; }
  .court .court-foot { padding-top: 2px; }
}
.court:hover {
  box-shadow: var(--shadow-md);
  border-color: var(--line-2);
  transform: translateY(-2px);
}

.court-top { display: flex; justify-content: space-between; gap: 10px; align-items: flex-start; }
.court-name {
  font-size: 16.5px; font-weight: 700; letter-spacing: -0.01em;
  margin: 0; line-height: 1.3; text-wrap: balance;
}
.court-loc { font-size: 13px; color: var(--ink-3); margin: 2px 0 0; }

.fav {
  background: none; border: 0; cursor: pointer; padding: 4px;
  color: var(--ink-3); line-height: 0; flex-shrink: 0;
  transition: color .15s ease, transform .1s ease;
}
.fav:hover { color: var(--ink); transform: scale(1.1); }
.fav svg { width: 18px; height: 18px; }
.fav[aria-pressed="true"] { color: var(--accent); }
.fav[aria-pressed="true"] svg { fill: currentColor; }

.badges { display: flex; gap: 6px; flex-wrap: wrap; }
.badge {
  font-size: 12px; font-weight: 600;
  padding: 3px 10px; border-radius: var(--r-pill);
  background: var(--surface-2); color: var(--ink-2);
}
.badge.type-Indoor, .badge.type-Outdoor, .badge.type-Covered { background: var(--surface-2); color: var(--ink-2); }
.badge.booked { background: var(--good-wash); color: var(--good); }
/* Open at the chosen hour. Green is already this app's "yes, and it counts"
   colour, so it carries over from played/booked without a new hue. */
.badge-open { background: var(--good-wash); color: var(--good); }
/* Not a warning — the venue may well be open. It says the data doesn't know,
   which is why these are never filtered out. */
.badge-unknown { background: var(--surface-2); color: var(--ink-3); font-weight: 500; }

.court-dist { font-family: var(--font-mono); font-size: 12px; color: var(--ink-2); }

/* The tier that actually applies at the chosen hour, so a changed headline
   price points at its own reason instead of just being different. */
.rate.is-now { color: var(--ink); font-weight: 600; }
.rate.is-now .rate-amt { color: var(--accent); }

.rates { display: flex; flex-direction: column; gap: 4px; margin-top: 2px; }
.rate {
  display: flex; justify-content: space-between; gap: 10px;
  font-size: 13px; color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}
.rate-amt { font-family: var(--font-mono); font-size: 12.5px; color: var(--ink); font-weight: 500; }
.rate-free { color: var(--good); }

.court-foot {
  display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
  margin-top: auto; padding-top: 6px;
}
.book {
  font-size: 13px; font-weight: 600;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 8px 14px; border-radius: 10px; text-decoration: none;
  background: var(--accent); color: var(--accent-ink); border: 1px solid var(--accent);
  transition: background-color .15s ease, border-color .15s ease;
}
.book:hover { background: var(--accent-2); border-color: var(--accent-2); }
/* Social is a fallback — it must not compete with the primary path. */
.book.social, .book.phone {
  background: none; color: var(--ink-2); border-color: var(--line-2);
}
.book.social:hover, .book.phone:hover { color: var(--ink); border-color: var(--ink-3); background: var(--surface-2); }
.book svg { width: 12px; height: 12px; }

.log-btn {
  font-size: 13px; font-weight: 600;
  background: none; border: 1px solid var(--line-2); color: var(--ink-2);
  padding: 8px 13px; border-radius: 10px; cursor: pointer; margin-left: auto;
  transition: border-color .15s ease, color .15s ease, background-color .15s ease;
}
.log-btn:hover { border-color: var(--ink); color: var(--ink); }

.verified { font-size: 11.5px; color: var(--ink-3); font-variant-numeric: tabular-nums; }
.walkin { font-size: 11.5px; color: var(--ink-3); }

.empty { text-align: center; color: var(--ink-3); padding: 56px 0; }

/* ─────────────── bookings ─────────────── */
.booking-layout {
  display: grid; grid-template-columns: minmax(280px, 350px) 1fr;
  gap: 20px; align-items: start;
}
@media (max-width: 820px) { .booking-layout { grid-template-columns: 1fr; } }

.card {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-card); padding: 20px;
}
.card-title {
  font-size: 15px; font-weight: 700; letter-spacing: -0.01em;
  margin: 0 0 14px; color: var(--ink);
}
.past-head { margin-top: 30px; }

.form-card { display: flex; flex-direction: column; gap: 12px; position: sticky; top: 84px; }
.field-row { display: flex; flex-direction: column; gap: 5px; }
.field-row > label,
.field-label { font-size: 12.5px; font-weight: 600; color: var(--ink-2); }
.field-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }

/* ── segmented radio (booking status) ──────────────────── */
/* Native radios sharing a name, so arrow-key navigation and form reset come
   free; the inputs are hidden and their labels do the drawing. */
.seg {
  display: flex; gap: 4px; background: var(--surface-2);
  padding: 4px; border-radius: var(--r-pill);
}
.seg input { position: absolute; opacity: 0; width: 0; height: 0; }
.seg label {
  flex: 1; text-align: center; cursor: pointer;
  font-size: 12.5px; font-weight: 600; color: var(--ink-3);
  padding: 6px 4px; border-radius: var(--r-pill);
  transition: color .15s ease, background-color .15s ease;
}
.seg label:hover { color: var(--ink); }
.seg input:checked + label { background: var(--ink); color: var(--ground); }
.seg input:focus-visible + label { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ── star rating ───────────────────────────────────────── */
/* Stars sit in reverse DOM order (5→1) and row-reverse flips them back, so a
   plain `input:checked ~ label` fills every star to the left of the checked
   one. Avoids fifteen :has() selectors for the same effect. */
.stars { display: flex; flex-direction: row-reverse; justify-content: flex-end; gap: 2px; padding: 3px 0; }
.stars input { position: absolute; opacity: 0; width: 0; height: 0; }
.stars label {
  cursor: pointer; font-size: 20px; line-height: 1.1;
  font-size: 0;                       /* the ★ in the markup; ::before draws it */
  transition: color .12s ease;
}
/* Unselected stars are drawn HOLLOW rather than dimmed. A faded solid star
   reads as "disabled" — at 4 stars the fifth looked greyed-out and broken
   rather than simply not chosen. An outline says "empty" unambiguously and
   needs no opacity trick. */
.stars label::before {
  content: '☆'; font-size: 20px; color: var(--ink-3);
}
.stars input:checked ~ label::before,
.stars label:hover::before,
.stars label:hover ~ label::before { content: '★'; color: var(--accent); }
.stars input:focus-visible + label { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }
.form-actions { display: flex; gap: 10px; align-items: center; margin-top: 4px; }
.form-msg { font-size: 13px; margin: 0; color: var(--ink-3); min-height: 1em; }
.form-msg.ok { color: var(--good); }
.form-msg.err { color: var(--accent); }

.list-head { display: flex; justify-content: space-between; align-items: center; }
.list-head .card-title { margin-bottom: 14px; }

.bk {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-input); padding: 14px 16px; margin-bottom: 10px;
  display: flex; gap: 14px; align-items: center;
  transition: box-shadow .15s ease, border-color .15s ease;
}
.bk:hover { box-shadow: var(--shadow-sm); border-color: var(--line-2); }
.bk.is-past { opacity: .62; }
.bk-when {
  font-family: var(--font-mono); font-size: 11px; color: var(--ink-2);
  text-align: center; line-height: 1.3; flex-shrink: 0;
  min-width: 54px; padding: 8px 6px;
  background: var(--surface-2); border-radius: 10px;
  font-variant-numeric: tabular-nums;
}
.bk-day { font-family: var(--font-body); font-size: 19px; font-weight: 700; color: var(--ink); display: block; }
.bk-body { flex: 1; min-width: 0; }
.bk-court { font-size: 14.5px; font-weight: 600; margin: 0; }
.bk-meta { font-size: 12.5px; color: var(--ink-3); margin: 2px 0 0; }

/* A cancelled row stays legible but visibly out of the running — it is
   excluded from every total, so it should not read as a normal session. */
.bk.is-off { opacity: .5; }
.bk.is-off .bk-cost { text-decoration: line-through; }
.bk-tag {
  display: inline-block; vertical-align: 1px; margin-left: 6px;
  font-size: 10.5px; font-weight: 700; letter-spacing: .02em; text-transform: uppercase;
  color: var(--ink-3); background: var(--surface-2);
  border: 1px solid var(--line); border-radius: var(--r-pill); padding: 1px 7px;
}
.bk-tag.is-pending {
  color: var(--accent); background: var(--accent-wash); border-color: var(--accent);
}
.bk-stars { margin-left: 6px; font-size: 12px; color: var(--accent); letter-spacing: 1px; }
.bk-cost { font-family: var(--font-mono); font-size: 13px; font-variant-numeric: tabular-nums; }
.bk-acts { display: flex; gap: 2px; flex-shrink: 0; }

.foot {
  margin: 48px auto 32px; padding-top: 20px; border-top: 1px solid var(--line);
  font-size: 12.5px; color: var(--ink-3);
}
.foot p { margin: 0 0 4px; }
.foot a { color: var(--ink-2); }
.foot-sub { display: flex; gap: 6px; align-items: center; }
.sync { font-family: var(--font-mono); font-size: 11px; }
.sync.on { color: var(--good); }
.sync.off { color: var(--ink-3); }

/* ─────────────── dialog ─────────────── */
.dlg {
  border: 1px solid var(--line); border-radius: var(--r-card);
  background: var(--surface); color: var(--ink);
  padding: 24px; max-width: 360px; width: calc(100% - 32px);
  box-shadow: var(--shadow-lg);
}
.dlg::backdrop { background: rgba(0, 0, 0, .5); }
.dlg h2 { font-size: 17px; font-weight: 700; letter-spacing: -0.01em; margin: 0 0 6px; }
.dlg p { font-size: 13px; color: var(--ink-2); margin: 0 0 14px; }
.dlg code { font-family: var(--font-mono); font-size: 12px; background: var(--surface-2); padding: 1px 5px; border-radius: 4px; }
.dlg form { display: flex; flex-direction: column; gap: 10px; }

/* ─────────────── view switcher ─────────────── */
.views {
  display: flex; gap: 4px; background: var(--surface-2);
  padding: 4px; border-radius: var(--r-pill); align-self: center;
}
.view-btn {
  background: none; border: 0; color: var(--ink-3); cursor: pointer;
  width: 36px; height: 32px; border-radius: var(--r-pill);
  display: inline-flex; align-items: center; justify-content: center;
  transition: color .15s ease, background-color .15s ease;
}
.view-btn:hover { color: var(--ink); }
.view-btn[aria-pressed="true"] { background: var(--ink); color: var(--ground); }
.view-btn svg { width: 15px; height: 15px; }

/* ─────────────── list view ─────────────── */
.table-wrap {
  border: 1px solid var(--line); border-radius: var(--r-card);
  overflow-x: auto; background: var(--surface);
}
.courts-table { border-collapse: collapse; width: 100%; min-width: 840px; }
.courts-table th {
  font-size: 12px; font-weight: 600; color: var(--ink-3); text-align: left;
  padding: 12px 14px; border-bottom: 1px solid var(--line);
  position: sticky; top: 0; background: var(--surface); z-index: 1;
}
.courts-table td {
  font-size: 13.5px; padding: 11px 14px;
  border-bottom: 1px solid var(--line); vertical-align: middle;
}
.courts-table tr:last-child td { border-bottom: 0; }
.courts-table tbody tr { transition: background-color .12s ease; }
.courts-table tbody tr:hover { background: var(--surface-2); }
.courts-table .num { font-family: var(--font-mono); font-size: 12.5px; font-variant-numeric: tabular-nums; white-space: nowrap; }
.t-name { display: block; font-weight: 600; }
.t-loc { display: block; font-size: 12px; color: var(--ink-3); }
.t-check { font-family: var(--font-mono); font-size: 11.5px; color: var(--ink-3); white-space: nowrap; }
/* The row has no room for the open/unknown badge, so it gets the same
   meaning as a dot. Colour is backed by the title attribute, never alone. */
.t-dot { font-size: 15px; line-height: 0; vertical-align: middle; }
.t-dot.is-open { color: var(--good); }
.t-dot.is-unknown { color: var(--ink-3); }
.book.sm { padding: 5px 11px; font-size: 12.5px; border-radius: 8px; white-space: nowrap; }
.courts-table .fav { padding: 2px; }
.courts-table .log-btn { margin-left: 0; padding: 5px 11px; font-size: 12.5px; border-radius: 8px; }

/* ─────────────── map view ─────────────── */
.map-wrap {
  border: 1px solid var(--line); border-radius: var(--r-card);
  overflow: hidden; background: var(--surface-2);
  /* Leaflet numbers its own layers in the hundreds — panes at 400-600,
     controls at 800-1000 — and #map is position:relative with z-index:auto,
     which is explicitly NOT a stacking context. So those layers escaped into
     the root one and painted over everything this app owns, all of which sits
     at z-index <= 40: on a phone the pins and the zoom control drew straight
     over the fixed bottom tab bar as the map scrolled past it. Isolating here
     traps Leaflet's whole scale inside this box, where it can only order the
     map against itself. */
  isolation: isolate;
}
#map { height: min(62vh, 640px); min-height: 420px; width: 100%; }
.map-note { font-size: 12px; color: var(--ink-3); margin: 8px 2px 0; }

/* Leaflet chrome, restyled to match the cards. */
.leaflet-popup-content-wrapper {
  background: var(--surface); color: var(--ink);
  border-radius: 12px; box-shadow: var(--shadow-md);
}
.leaflet-popup-tip { background: var(--surface); }
/* The popup holds a .court now, so it only has to be a container and get out
   of the way — the card styles itself down via the @container rule above. */
.leaflet-popup-content {
  font-family: var(--font-body); margin: 14px 16px; line-height: 1.45;
  width: 214px !important;   /* Leaflet sets this inline; the card needs it stable */
}
.leaflet-popup-content .court { color: var(--ink); }
.leaflet-container .court-name, .leaflet-container .court-loc { margin: 0; }
.leaflet-container a.book { color: var(--accent-ink); }
.leaflet-container a.book.social { background: none; border: 1px solid var(--line-2); color: var(--ink-2); }
.leaflet-bar a { background: var(--surface); color: var(--ink); border-bottom-color: var(--line); }
.leaflet-bar a:hover { background: var(--surface-2); }
.leaflet-control-attribution { background: color-mix(in srgb, var(--surface) 80%, transparent); color: var(--ink-3); }
.leaflet-control-attribution a { color: var(--ink-2); }

/* ─────────────── view transitions ───────────────
   The three court views and the three tabs used to swap in a single frame.
   These names give the browser something to pair across the swap so it can
   tween rather than cut.

   Named at panel level on purpose. Naming each card would let a card morph
   into its own list row, but 40 named elements is 40 snapshot layers on a
   phone, and this app is used on phones — the panel cross-fade reads almost
   the same for a fraction of the work. */
.grid, .table-wrap, .map-wrap { view-transition-name: court-view; }
[role="tabpanel"] { view-transition-name: tab-panel; }

::view-transition-old(court-view),
::view-transition-new(court-view),
::view-transition-old(tab-panel),
::view-transition-new(tab-panel) {
  animation-duration: .22s;
  animation-timing-function: cubic-bezier(.4, 0, .2, 1);
}

/* The root cross-fade would run over the masthead and filter rail too, which
   is a lot of movement for switching a list to a map. */
::view-transition-old(root),
::view-transition-new(root) { animation: none; }

@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; }
  /* withTransition() already bails out here, but a page loaded mid-flight
     shouldn't animate either. */
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
}

@media (max-width: 640px) {
  body { font-size: 14px; }
  .wrap { padding-inline: 14px; }

  .masthead {
    top: 0;
    padding-top: env(safe-area-inset-top);
  }
  /* Only the brand and the theme toggle stay up here now — the tabs move
     to the bottom (below), so this never wraps and stays one compact row. */
  .masthead-in {
    min-height: 52px;
    gap: 10px;
    padding-block: 8px;
    flex-wrap: nowrap;
  }
  .brand-mark svg { width: 25px; height: 25px; }
  .brand-text strong { font-size: 16px; }
  .brand-sub { display: none; }
  .header-tools { margin-left: auto; }
  /* Primary navigation belongs at the bottom on a phone.
     Three destinations is the case both Apple's HIG and Material put in a
     bottom bar, and the bottom third is the only area a thumb reaches
     comfortably one-handed — which is how this gets used courtside. It
     also hands the header back its second row.
     z-index sits under the filter sheet (40) and its backdrop (39), so
     opening filters correctly covers the bar. */
  .tabs {
    position: fixed;
    left: 0; right: 0; bottom: 0;
    z-index: 30;
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 2px;
    border-radius: 0;
    background: var(--ground);
    border-top: 1px solid var(--line);
    padding: 6px 10px calc(6px + env(safe-area-inset-bottom));
  }
  .tab {
    justify-content: center;
    min-height: 46px;
    padding: 7px 2px;
    font-size: 12.5px;
    gap: 3px;
    position: relative;
    white-space: nowrap;
  }
  /* Four tabs leave no room for the badge beside the label. */
  .tab-count { position: absolute; top: 2px; right: 0; font-size: 9.5px; padding: 0 5px; }

  /* Content has to clear the fixed bar, or the last row of anything sits
     underneath it and cannot be reached. This goes on body rather than the
     footer: the footer is not always the last painted box, and padding it
     left the sync line tucked behind the bar. */
  body { padding-bottom: calc(78px + env(safe-area-inset-bottom)); }
  .theme-btn {
    width: 38px;
    height: 38px;
  }

  /* A wrapped flex line still applies border-left to the item that starts
     it, so the stats showed a stray divider hanging at the beginning of the
     second row. A grid has no such edge case, and two columns is a better
     fit for four stats on a phone than an uneven wrap. */
  .statbar {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 8px 12px;
    margin: 14px 0 14px;
  }
  .stat-item {
    font-size: 12px;
    white-space: normal;
  }
  .stat-item b {
    font-size: 15px;
    max-width: none;
  }
  .stat-item + .stat-item {
    margin-left: 0;
    padding-left: 0;
    border-left: 0;
  }
  .offline-pill { margin: 0 0 12px; }

  /* Three tracks, three rows. The sort select was sharing one row with
     Filters, the view switcher AND Share, which squeezed it until "A–Z"
     clipped to "A-". Share moves down beside Sync, which also stops Sync
     spanning a row on its own. */
  .controls {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    gap: 8px;
    margin-bottom: 12px;
  }
  .field.search {
    grid-column: 1 / -1;
    grid-row: 1;
    min-width: 0;
  }
  input[type="search"] {
    min-height: 46px;
    font-size: 16px;
  }
  .filters-btn,
  .field select,
  .share-btn,
  .sync-btn {
    min-height: 42px;
  }
  .filters-btn {
    grid-column: 1;
    grid-row: 2;
    padding-inline: 14px;
  }
  .controls > .field:not(.search) {
    grid-column: 2;
    grid-row: 2;
    min-width: 0;
  }
  .views {
    grid-column: 3;
    grid-row: 2;
    align-self: stretch;
  }
  .view-btn {
    width: 34px;
    min-height: 34px;
  }
  .share-btn {
    grid-column: 3;
    grid-row: 3;
    width: 42px;
    height: 42px;
    justify-self: end;
  }
  .sync-btn {
    grid-column: 1 / 3;
    grid-row: 3;
    justify-content: center;
    width: 100%;
  }

  .grid {
    grid-template-columns: 1fr;
    gap: 12px;
  }
  .court {
    border-radius: 14px;
    padding: 16px;
  }
  .court:hover {
    transform: none;
  }
  .court-foot {
    align-items: stretch;
  }
  .book,
  .log-btn {
    min-height: 40px;
    justify-content: center;
  }
  .log-btn {
    margin-left: 0;
    flex: 1 1 86px;
  }

  .sidebar.open {
    max-height: min(82vh, 680px);
    border-radius: 18px 18px 0 0;
    padding: 18px 16px calc(16px + env(safe-area-inset-bottom));
  }
  .facet-row,
  .facet-check {
    min-height: 42px;
  }

  .booking-layout {
    gap: 16px;
  }
  .form-card {
    position: static;
  }
  .field-grid {
    grid-template-columns: 1fr;
  }
  .bk {
    display: grid;
    grid-template-columns: 54px minmax(0, 1fr);
    align-items: start;
    gap: 10px 12px;
    padding: 14px;
  }
  .bk-cost {
    grid-column: 2;
  }
  .bk-acts {
    grid-column: 1 / -1;
    justify-content: flex-end;
  }
  .bk-acts .btn-ghost {
    min-height: 38px;
  }

  /* The map should end where the tab bar begins, so every pin is reachable
     without scrolling the map under the bar. The exact gap depends on how
     much chrome sits above it — stats, search, the controls grid, the result
     line — which wraps differently per device, so sizeMap() measures it and
     writes the height inline. This value is only the pre-measurement
     fallback; the old fixed 238px offset guessed ~95px short and pushed the
     bottom of the map off-screen. */
  #map {
    height: calc(100svh - 340px);
    min-height: 300px;
  }
}

/* ── date picker ────────────────────────────────────────
   A trigger button plus a popover reusing the bookings month grid.
   Native <input type="date"> renders as a different control in every
   browser and matched nothing else in the form; this reads as part of
   the app and shows the weekday, which is what you actually want to
   know when picking a court slot. */
.picker { position: relative; }

/* Matches the input styling exactly, so the row reads as one form. */
.picker-btn {
  width: 100%; text-align: left; cursor: pointer;
  font-family: var(--font-body); font-size: 14.5px; font-weight: 600; color: var(--ink);
  background: var(--surface); border: 1px solid var(--line-2);
  border-radius: var(--r-input); padding: 10px 14px;
  transition: border-color .15s ease;
}
.picker-btn:hover { border-color: var(--ink-3); }
.picker-btn[aria-expanded="true"] { border-color: var(--ink); }
.picker-btn.is-empty { font-weight: 500; color: var(--ink-3); }

.picker-pop {
  position: absolute; z-index: 25; top: calc(100% + 6px); left: 0;
  width: min(300px, 78vw);
  background: var(--surface); border: 1px solid var(--line-2);
  border-radius: var(--r-card); box-shadow: var(--shadow-lg);
  padding: 14px;
}
/* Keeps the popover on screen when the field sits near the right edge. */
.field-grid > .field-row:last-child .picker-pop { left: auto; right: 0; }

/* ── the same three, in the top layer ──────────────────
   [popover] is only present when the script found both the Popover API and
   anchor positioning (see NATIVE_POP), so everything here is scoped to the
   enhanced path and the rules above stay the floor.

   The win is that none of this depends any more on no ancestor happening to
   create a stacking context or clip overflow — the class of bug that put
   Leaflet's pins over the tab bar. */
.picker-pop[popover] {
  position: fixed;
  margin: 0;
  inset: auto;                      /* the UA sheet centres popovers */
  max-height: none;
  overflow: visible;

  /* position-area rather than a pair of anchor() insets: it sets both axes
     from one declaration and, unlike the inset form, it is what the built-in
     flip-* fallbacks below are defined against. "span-right" puts the box
     below the field and lets it run rightwards from the field's left edge,
     which is where these popovers already sit today. */
  position-area: bottom span-right;
  margin-top: 6px;

  /* The manual scroll nudge, done properly: flip above the field when there
     is no room below, or to the other side when there is none to the right.
     Where position-try isn't supported the popover just stays below, which
     is exactly the behaviour that ships today. */
  position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
}
/* Both edge nudges are the fallback path's job now. */
.field-grid > .field-row:last-child .picker-pop[popover] { right: auto; left: auto; }

#bDateBtn { anchor-name: --pk-date; }
#bTimeBtn { anchor-name: --pk-time; }
#bCourtBtn { anchor-name: --pk-court; }
#bDatePop[popover] { position-anchor: --pk-date; }
#bTimePop[popover] { position-anchor: --pk-time; }
#bCourtPop[popover] { position-anchor: --pk-court; }

.picker-pop .cal-cell { min-height: 34px; border-radius: 8px; }
.picker-pop .cal-head { margin: 0 0 10px; }
.picker-pop .cal-month { font-size: 14px; }
.picker-pop .cal-nav { width: 30px; height: 30px; }

/* Every day is selectable here — a booking log records the past as well
   as the future, so nothing is disabled. */
.cal--pick .cal-cell { cursor: pointer; color: var(--ink); }
.cal--pick .cal-cell:hover { background: var(--surface-2); }
.cal--pick .cal-cell.is-blank { cursor: default; }

/* ── time picker ────────────────────────────────────────
   One choice out of 76 became three small ones. Everything below is sized
   so the whole control — meridiem, twelve hours, four quarters — fits in a
   popover with no scrolling at any viewport. */
.picker-pop--time { width: min(268px, calc(100vw - 44px)); }

/* Two halves of a single track, the same shape as the status segmented
   control, so it reads as "pick one" rather than two buttons. */
.tp-ap {
  display: grid; grid-template-columns: 1fr 1fr; gap: 2px;
  background: var(--surface-2); border-radius: var(--r-pill);
  padding: 3px; margin-bottom: 12px;
}
.tp-ap-btn {
  padding: 7px 0; cursor: pointer;
  font-family: var(--font-body); font-size: 12px; font-weight: 700;
  letter-spacing: .06em; color: var(--ink-2);
  background: none; border: 0; border-radius: var(--r-pill);
  transition: background-color .15s ease, color .15s ease;
}
.tp-ap-btn:hover { color: var(--ink); }
.tp-ap-btn.is-on { background: var(--ink); color: var(--ground); }

/* Four across, twelve total: a clock face flattened into a keypad. */
.tp-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 5px; }
.tp-h {
  aspect-ratio: 1 / .82; min-height: 38px; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-body); font-size: 14.5px; font-weight: 600;
  font-variant-numeric: tabular-nums; color: var(--ink);
  background: var(--surface-2); border: 1px solid transparent; border-radius: 10px;
  transition: background-color .12s ease, border-color .12s ease;
}
.tp-h:hover:not(:disabled) { border-color: var(--ink-3); }
.tp-h.is-picked { background: var(--ink); border-color: var(--ink); color: var(--ground); }
/* 12am–4am are not slots anyone books here. Shown rather than removed, so
   the keypad keeps the shape of a clock face instead of growing holes. */
.tp-h:disabled { background: none; color: var(--ink-3); opacity: .38; cursor: not-allowed; }

.tp-legend {
  margin: 12px 0 6px;
  font-size: 11px; font-weight: 700; color: var(--ink-3);
  text-transform: uppercase; letter-spacing: .06em;
}
.tp-mins { display: grid; grid-template-columns: repeat(4, 1fr); gap: 5px; }
.tp-m {
  padding: 9px 0; cursor: pointer;
  font-family: var(--font-mono); font-size: 12.5px; font-weight: 600; color: var(--ink-2);
  background: var(--surface-2); border: 1px solid transparent; border-radius: 8px;
  transition: background-color .12s ease, border-color .12s ease, color .12s ease;
}
.tp-m:hover { color: var(--ink); border-color: var(--ink-3); }
.tp-m.is-picked { background: var(--ink); border-color: var(--ink); color: var(--ground); }

/* ── court picker ───────────────────────────────────────
   Wider than the other two popovers because these rows carry a name, a
   barangay and a rate. The list is the only part that scrolls, and the
   search box and city chips stay pinned above it. */
.picker-pop--court { width: min(400px, calc(100vw - 44px)); }
.cp-search { margin-bottom: 10px; }
.cp-search input[type="search"] { box-shadow: none; font-size: 14px; padding-block: 9px; }

.cp-cities {
  display: flex; gap: 6px; overflow-x: auto; padding-bottom: 2px;
  scrollbar-width: none;
}
.cp-cities::-webkit-scrollbar { display: none; }
.cp-city {
  display: inline-flex; align-items: center; gap: 5px; white-space: nowrap;
  cursor: pointer; padding: 6px 11px;
  font-family: var(--font-body); font-size: 12px; font-weight: 600; color: var(--ink-2);
  background: var(--surface-2); border: 1px solid transparent; border-radius: var(--r-pill);
  transition: color .15s ease, border-color .15s ease;
}
.cp-city:hover { color: var(--ink); border-color: var(--line-2); }
.cp-city span { font-family: var(--font-mono); font-size: 10.5px; color: var(--ink-3); }
.cp-city.is-on { background: var(--ink); color: var(--ground); }
.cp-city.is-on span { color: var(--ground); opacity: .7; }

.cp-list {
  max-height: min(44vh, 320px); overflow-y: auto; overscroll-behavior: contain;
  margin: 10px -4px 0; padding: 0 4px;
}
/* Sticky, so you always know which city you are scrolling through — the
   whole point of clustering rather than one A–Z run. */
.cp-group {
  position: sticky; top: 0; z-index: 1; margin: 0; padding: 7px 4px 5px;
  background: var(--surface);
  font-size: 10.5px; font-weight: 700; color: var(--ink-3);
  text-transform: uppercase; letter-spacing: .07em;
}
.cp-item {
  width: 100%; text-align: left; cursor: pointer;
  display: flex; flex-direction: column; gap: 2px;
  padding: 9px 10px; margin-bottom: 2px;
  background: none; border: 1px solid transparent; border-radius: 10px;
  transition: background-color .12s ease, border-color .12s ease;
}
.cp-item:hover { background: var(--surface-2); }
.cp-item.is-picked { background: var(--accent-wash); border-color: var(--accent); }
.cp-name {
  font-family: var(--font-body); font-size: 13.5px; font-weight: 600; color: var(--ink);
  line-height: 1.3;
}
.cp-star { color: var(--accent); font-size: 11px; margin-left: 5px; }
.cp-meta { font-family: var(--font-mono); font-size: 11px; color: var(--ink-3); }
.cp-none { margin: 16px 4px; font-size: 13px; color: var(--ink-3); }

.picker-quick { display: flex; gap: 6px; margin-top: 12px; flex-wrap: wrap; }
.chip {
  flex: 1; cursor: pointer; white-space: nowrap;
  font-family: var(--font-body); font-size: 12px; font-weight: 600; color: var(--ink-2);
  background: var(--surface-2); border: 1px solid transparent;
  border-radius: var(--r-pill); padding: 7px 10px;
  transition: color .15s ease, border-color .15s ease;
}
.chip:hover { color: var(--ink); border-color: var(--line-2); }

@media (max-width: 640px) {
  /* Full-width and centred under the field — 78vw next to a half-width
     field would hang off the screen. */
  .picker-pop {
    width: calc(100vw - 44px);
    left: 0; right: auto;
  }
  .field-grid > .field-row:last-child .picker-pop { left: auto; right: 0; }
  .picker-btn { min-height: 46px; }
  /* Thumb-sized targets: the hour keypad is the one control here you tap
     twelve different places on. */
  .tp-h { min-height: 44px; }
  .tp-m, .tp-ap-btn { min-height: 40px; }
  .cp-item { padding: 11px 10px; }
}

/* ── bookings calendar ──────────────────────────────────
   The same bookings arranged by date. A list answers "what is next";
   a month grid answers "how busy was this month, and where are the
   gaps" — which is the question you actually have when booking. */
.cal-head { display: flex; align-items: center; gap: 8px; margin: 4px 0 14px; }
.cal-month {
  margin: 0; flex: 1; text-align: center;
  font-size: 15px; font-weight: 700; letter-spacing: -0.01em;
}
.cal-nav {
  width: 36px; height: 36px; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  background: none; color: var(--ink-2); cursor: pointer;
  border: 1px solid var(--line-2); border-radius: 50%;
}
.cal-nav:hover { color: var(--ink); border-color: var(--ink-3); }
.cal-nav svg { width: 16px; height: 16px; }
.cal-today { flex: none; }

.cal-dow, .cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
.cal-dow {
  margin-bottom: 6px;
  font-size: 11px; font-weight: 700; color: var(--ink-3);
  text-transform: uppercase; letter-spacing: .06em; text-align: center;
}

.cal-cell {
  aspect-ratio: 1; min-height: 42px;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
  background: none; border: 1px solid transparent; border-radius: 10px;
  font-family: inherit; color: var(--ink-3); cursor: default;
  transition: background-color .12s ease, border-color .12s ease;
}
.cal-cell.is-blank { border: 0; }
.cal-num { font-size: 13px; font-variant-numeric: tabular-nums; }

/* Only days with something on them are interactive — an empty cell that
   highlights on hover promises something it cannot deliver. */
.cal-cell.has-bk { color: var(--ink); background: var(--surface-2); cursor: pointer; }
.cal-cell.has-bk:hover { border-color: var(--ink-3); }
.cal-cell.is-today { border-color: var(--ink-3); font-weight: 700; }
.cal-cell.is-picked { background: var(--ink); border-color: var(--ink); color: var(--ground); }
.cal-cell.is-picked .cal-dot { background: var(--ground); }

.cal-dots { display: flex; gap: 3px; min-height: 5px; }
.cal-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--accent); }
/* Cancelled and no-shows still happened, so they show — hollow, because
   they cost nothing and count toward nothing. */
.cal-dot.is-off { background: none; box-shadow: inset 0 0 0 1px var(--ink-3); }

.cal-total {
  margin: 14px 0 0; text-align: center;
  font-size: 12px; font-weight: 600; color: var(--ink-3);
  text-transform: uppercase; letter-spacing: .06em;
}
.cal-day-head {
  margin: 18px 0 10px; font-size: 14px; font-weight: 700; color: var(--ink);
}

.list-tools { display: flex; align-items: center; gap: 8px; }

@media (max-width: 640px) {
  .cal-cell { min-height: 38px; border-radius: 8px; }
  .cal-grid, .cal-dow { gap: 3px; }
  .list-tools .btn-ghost { padding-inline: 10px; }
}

/* ═══════════════════════════════════════════════════════════
   MATCH SCORING
   Two surfaces with opposite jobs. The SCORER is held at arm's
   length and tapped between rallies, so it is sized for a
   thumb and nothing else competes with the two rally buttons.
   The BOARD is read from the far baseline of a court — call it
   10 metres — so its type scales with the viewport rather than
   sitting at a fixed px size that only suits one iPad.
   ═══════════════════════════════════════════════════════════ */

/* A quiet dot, not a count: "a match is running" is the whole
   message, and a number here would invite reading it as a score. */
.tab-live { color: var(--accent); font-size: 9px; line-height: 1; }

.match-setup {
  position: static;            /* not sticky: it is the whole panel here */
  max-width: 620px;
  margin: 0 auto 8px;
}
.card-sub { font-size: 13px; color: var(--ink-2); margin: -8px 0 4px; }
.field-note { font-size: 12px; color: var(--ink-3); margin: -4px 0 2px; }

/* ── the set-up court ───────────────────────────────────
   Names are typed on a court instead of into four unlabelled
   fields. It costs no more space and carries information the
   list could not: who starts on which side, and therefore who
   serves first — the two right-hand boxes being diagonal is a
   thing you learn by seeing it once.

   The inputs are placed by grid-area, so DOM order stays
   A1 → A2 → B1 → B2 for tabbing while the picture matches the
   court and the scoreboard's own left/right convention. */
.setup-court {
  position: relative;
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-areas:
    "aLeft  bRight"
    "aRight bLeft";
  gap: 6px;
  padding: 10px 10px 26px;
  border: 1.5px solid var(--line-2);
  border-radius: var(--r-input);
  background: var(--surface-2);
}
.sc-a-right { grid-area: aRight; }
.sc-a-left  { grid-area: aLeft; }
.sc-b-right { grid-area: bRight; }
.sc-b-left  { grid-area: bLeft; }

.sc-cell { display: flex; }
.sc-cell input {
  width: 100%; min-height: 52px;
  padding: 8px 10px; text-align: center;
  font-family: inherit; font-size: 14px; font-weight: 600; color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line-2); border-radius: 10px;
}
.sc-cell input::placeholder { font-weight: 500; color: var(--ink-3); }
.sc-cell input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Right-hand boxes serve first for their side, so they carry a mark. The
   dot is on the box, not in a caption, because the whole point is that the
   position is what tells you. */
.sc-a-right input,
.sc-b-right input { border-color: var(--ink-3); }
.sc-a-right::after,
.sc-b-right::after {
  content: ""; position: absolute; width: 6px; height: 6px; border-radius: 50%;
  background: var(--ink-3); margin: 7px 0 0 7px;
}

/* The net, drawn between the two halves. */
.sc-net {
  position: absolute; top: 6px; bottom: 22px; left: 50%;
  width: 0; border-left: 2px dashed var(--ink-3); opacity: .5;
}
.sc-side-a, .sc-side-b {
  position: absolute; bottom: 7px;
  font-size: 10.5px; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink-3);
}
.sc-side-a { left: 12px; }
.sc-side-b { right: 12px; }

/* Singles: one box a side, and the partner rows collapse rather than
   leaving two empty holes in the court. */
.setup-court.is-singles {
  grid-template-areas: "aRight bRight";
}

/* First serve, placed under the half it refers to so the choice is
   spatial. Same hidden-radio pattern as .seg. */
.sc-serve { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.sc-serve input { position: absolute; opacity: 0; width: 0; height: 0; }
.sc-serve label {
  text-align: center; cursor: pointer;
  font-size: 12.5px; font-weight: 600; color: var(--ink-3);
  padding: 9px 6px; border-radius: var(--r-pill);
  border: 1px solid var(--line-2); background: var(--surface);
  transition: color .15s ease, background-color .15s ease, border-color .15s ease;
}
.sc-serve label:hover { color: var(--ink); }
.sc-serve input:checked + label {
  background: var(--ink); border-color: var(--ink); color: var(--ground);
}
.sc-serve input:focus-visible + label { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Buttons carrying an icon alongside their label. */
.with-ico { display: inline-flex; align-items: center; gap: 7px; }
.with-ico svg { width: 14px; height: 14px; flex: none; }
#mCopyBoard.is-done { color: var(--good); border-color: var(--good); }

.form-actions.stack { flex-direction: column; align-items: stretch; }
.form-actions.stack .btn,
.form-actions.stack .btn-ghost { justify-content: center; min-height: 44px; }
.dlg-hint { font-size: 12px; color: var(--ink-3); margin: 4px 0 6px; }

/* ── the scorer ────────────────────────────────────────── */

.scorer { max-width: 620px; margin: 0 auto; }

/* Clears the sticky masthead. The other two tabs get this from their
   stat strip; the scorer leads with the call score and would otherwise
   tuck the one number that matters under the header. */
#view-match { padding-top: 22px; }

.scorer-head { text-align: center; margin-bottom: 18px; }

/* The called score is the artefact players actually speak aloud
   ("nine, eight, two"), so it leads — the per-side numbers below
   are the reference, not the headline. */
.scorer-call {
  font-family: var(--font-mono);
  font-size: clamp(40px, 12vw, 58px); font-weight: 700;
  letter-spacing: -0.02em; line-height: 1; margin: 0;
  font-variant-numeric: tabular-nums;
}
.scorer-note { font-size: 13px; color: var(--ink-2); margin: 6px 0 0; }
.scorer-serve { font-size: 13px; color: var(--ink-3); margin: 2px 0 0; }
.scorer-switch {
  display: inline-block; margin: 10px 0 0;
  font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em;
  /* Accent ON the wash needs the darker variant: --accent is tuned for white
     text on a filled button, and only reaches 4.26:1 against the tint. */
  background: var(--accent-wash); color: var(--accent-2);
  padding: 5px 12px; border-radius: var(--r-pill);
}

/* Sync state. Scoring is local-first, so the score on this phone can be
   ahead of the board courtside — that gap is the only thing this feature
   has to be candid about, and it gets a line rather than a silent icon. */
.scorer-sync {
  display: inline-block; margin: 8px 0 0;
  font-size: 11.5px; font-weight: 600; letter-spacing: .04em;
  color: var(--ink-3);
}
.scorer-sync.ok { color: var(--good); }
.scorer-sync.warn {
  color: var(--accent-2);
  background: var(--accent-wash); padding: 3px 10px; border-radius: var(--r-pill);
}

.scorer-prompt {
  text-align: center; font-size: 12.5px; font-weight: 600;
  color: var(--ink-3); text-transform: uppercase; letter-spacing: .07em;
  margin: 0 0 10px;
}

.scorer-teams { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }

/* Deliberately huge. This is pressed with one thumb, often mid-
   conversation, sometimes in sunlight — a 44px target would be
   the wrong answer even though it passes the guideline. */
.rally-btn {
  position: relative;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2px; min-height: 168px; padding: 20px 12px;
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line-2); border-radius: var(--r-card);
  cursor: pointer; font-family: inherit;
  transition: border-color .15s ease, background-color .15s ease, transform .06s ease;
  -webkit-tap-highlight-color: transparent;
}
.rally-btn:hover { border-color: var(--ink-3); }
.rally-btn:active { transform: scale(.985); }
.rally-btn:disabled { opacity: .55; cursor: default; }

/* Serve is shown as a filled side rather than a small icon: which
   side is serving has to be legible at a glance from a pocket. */
.rally-btn.is-serving {
  border-color: var(--ink);
  background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--ink);
}

/* The tap is optimistic-feeling but the server is authoritative, so
   the pending state dims rather than pre-drawing a score that might
   come back refused. */
.rally-btn.is-pending { opacity: .6; }

.rally-name {
  font-size: 14px; font-weight: 600; color: var(--ink-2);
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rally-score {
  font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  font-size: clamp(52px, 16vw, 72px); font-weight: 700; line-height: 1.05;
  letter-spacing: -0.03em;
}
.rally-games { font-size: 11.5px; color: var(--ink-3); min-height: 16px; }
.rally-tag {
  position: absolute; top: 10px; left: 50%; transform: translateX(-50%);
  font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em;
  background: var(--accent); color: var(--accent-ink);
  padding: 3px 9px; border-radius: var(--r-pill); white-space: nowrap;
}

.scorer-games { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; margin-top: 14px; }
.game-pill {
  font-size: 11.5px; color: var(--ink-2);
  background: var(--surface-2); padding: 4px 10px; border-radius: var(--r-pill);
}
.game-pill b { font-family: var(--font-mono); color: var(--ink); }

.scorer-acts {
  display: flex; flex-wrap: wrap; gap: 8px; justify-content: center;
  margin-top: 18px;
}
.scorer-acts .btn-ghost,
.scorer-acts .btn { min-height: 42px; }
.btn-ghost.danger { color: var(--ink-3); }
.btn-ghost.danger:hover { color: var(--accent); }

#mBoardUrl {
  font-family: var(--font-mono); font-size: 11.5px;
  width: 100%; padding: 10px 12px;
  border: 1px solid var(--line-2); border-radius: var(--r-input);
  background: var(--surface-2); color: var(--ink-2);
}

@media (max-width: 700px) {
  .rally-btn { min-height: 200px; }   /* more thumb, less page */
  .scorer-acts .btn,
  .scorer-acts .btn-ghost { flex: 1 1 45%; }
}

/* ═══════════════ the courtside board ═══════════════════

   Read from the far baseline of a court — call it 10 metres. Every
   size here is viewport-relative for that reason: a fixed px scale
   suits exactly one iPad and no other screen.

   The two point totals get the whole middle of the screen. Anything
   that is not a point total lives in the header or the footer, so
   nothing competes with the number people are squinting at.
   ═══════════════════════════════════════════════════════ */

/* Its own page, so it owns the viewport outright: no masthead, no
   scrolling, nothing to tap by accident when someone picks the
   iPad up to move it. */
.board-body {
  margin: 0; height: 100svh; overflow: hidden;
  background: var(--ground); color: var(--ink);
}
.board {
  height: 100svh; position: relative;
  display: flex; flex-direction: column;
  padding: max(16px, env(safe-area-inset-top)) max(20px, env(safe-area-inset-right))
           max(14px, env(safe-area-inset-bottom)) max(20px, env(safe-area-inset-left));
}

.board-idle { margin: auto; text-align: center; }
.board-idle-title { font-size: clamp(24px, 5vw, 40px); font-weight: 700; margin: 0; }
.board-idle-sub { font-size: clamp(14px, 2vw, 19px); color: var(--ink-3); margin: 10px 0 0; }

.board-live { flex: 1; display: flex; flex-direction: column; min-height: 0; gap: clamp(6px, 1.4vh, 18px); }

.board-head { display: flex; justify-content: space-between; align-items: center; gap: 16px; }
.board-meta {
  font-size: clamp(11px, 1.5vw, 17px); font-weight: 600;
  color: var(--ink-3); text-transform: uppercase; letter-spacing: .09em;
}
.board-call {
  font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  font-size: clamp(16px, 2.6vw, 34px); font-weight: 700;
  color: var(--ink-2);
}
.board-call.is-final-text {
  font-family: var(--font-body); font-weight: 800;
  letter-spacing: .12em; color: var(--good);
}

.board-sides {
  flex: 1; min-height: 0;
  display: grid; grid-template-columns: 1fr 1fr;
  align-items: center;
  gap: clamp(8px, 2vw, 36px);
}

.board-side {
  min-width: 0; align-self: center; justify-self: center;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: clamp(2px, .6vh, 10px);
  padding: clamp(10px, 2vh, 26px) clamp(10px, 2vw, 28px);
  border-radius: clamp(12px, 2vw, 26px);
  border: 2px solid transparent;
  transition: border-color .2s ease, background-color .2s ease;
}
/* Serving side is drawn with a border and a live dot rather than a
   colour fill: the score has to stay maximum-contrast, and a filled
   panel would cost the digits their legibility from the baseline. */
.board-side.is-serving { border-color: var(--ink); background: var(--surface-2); }
.board-side.is-winner  { border-color: var(--good); background: var(--good-wash); }

.board-team {
  display: flex; align-items: center; gap: .4em; margin: 0;
  font-size: clamp(13px, 2.2vw, 28px); font-weight: 700; letter-spacing: -0.01em;
  max-width: 100%; padding: 0 10px;
}
.board-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.board-serve {
  width: .42em; height: .42em; border-radius: 50%;
  background: var(--accent); opacity: 0; flex: none;
}
.board-side.is-serving .board-serve { opacity: 1; }

/* ── the score dial ─────────────────────────────────────
   The ring is progress toward the target, so a glance answers "how
   far along is this game" without reading either number. Sizing is
   driven by the dial's own font-size, which lets the digits inside
   be expressed in em and stay centred at every viewport. */
.score-dial {
  position: relative;
  font-size: clamp(56px, min(22vw, 28vh), 280px);
  width: 2.05em; max-width: 100%;
  aspect-ratio: 1;
}
.ring { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.ring circle { fill: none; stroke-width: 3.4; }
.ring-track { stroke: var(--line); }
.ring-fill {
  stroke: var(--ink-3);   /* linecap is set per-render: see scoreboard.js */
  transform: rotate(-90deg); transform-origin: 50% 50%;   /* start at 12 o'clock */
  transition: stroke-dasharray .45s cubic-bezier(.2, .8, .3, 1);
}
.board-side.is-serving .ring-fill { stroke: var(--accent); }
.board-side.is-winner  .ring-fill { stroke: var(--good); }

/* Monospaced with tabular figures on purpose: every digit occupies
   the same width, so 9 and 11 sit on the same optical centre and the
   number does not shift sideways as the score climbs. That stability
   is the whole reason a scoreboard uses a readout face. */
.board-points {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 1.32em; font-weight: 700;
  letter-spacing: -0.04em; line-height: 1;
}

.board-towin {
  margin: 0; min-height: 1.3em;
  font-size: clamp(10px, 1.4vw, 16px); font-weight: 600;
  text-transform: uppercase; letter-spacing: .12em; color: var(--ink-3);
}
.board-games { font-size: clamp(12px, 1.9vw, 24px); color: var(--ink-3); letter-spacing: .2em; margin: 0; min-height: 1.2em; }
.board-tag {
  font-size: clamp(10px, 1.4vw, 17px); font-weight: 700;
  letter-spacing: .1em; color: var(--accent); margin: 0; min-height: 1.3em;
}
.board-side.is-winner .board-tag,
.board-side.is-winner .board-games { color: var(--good); }

/* ── the serve cue ──────────────────────────────────────
   A whole court diagram outsized the score it was meant to
   annotate. This is the same information at a glyph's scale: four
   service boxes with the one being served from lit up.

   Only one box lights. An earlier version also washed the
   receiving box to imply the diagonal, but two fills at different
   opacities are indistinguishable at ten metres — it just read as
   a checkerboard. One lit box is unambiguous, which is the only
   thing that matters at this size. */
.serve-strip { display: flex; align-items: center; gap: .7em; justify-self: center; }
.court-mini { width: clamp(48px, 6vw, 88px); height: auto; flex: none; display: block; }

/* Lines sit above the fill and have to stay legible against it, so
   they take an ink tone rather than the usual hairline grey. */
.cm-line { stroke: var(--ink-3); stroke-width: 1.6; fill: none; opacity: .55; }
.cm-out  { stroke-width: 2.2; opacity: .75; }
.cm-net  { stroke: var(--ink-2); stroke-width: 2.4; }

.cm-quad { rx: 1.5; }
.cm-serve { fill: var(--accent); filter: drop-shadow(0 0 3px var(--accent)); }

.board-server {
  font-size: clamp(10px, 1.35vw, 16px); font-weight: 600;
  color: var(--ink-2); text-transform: uppercase; letter-spacing: .08em;
  white-space: nowrap;
}

/* Three tracks so the serve cue is centred on the board, not on
   whatever happens to be left of it. */
.board-foot {
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 16px;
  font-size: clamp(11px, 1.5vw, 18px); color: var(--ink-2); min-height: 2em;
}
.board-games-log { display: flex; gap: 8px; flex-wrap: wrap; }
.board-game-pill {
  font-family: var(--font-mono);
  background: var(--surface-2); color: var(--ink-2);
  padding: .2em .7em; border-radius: var(--r-pill);
}
.board-switch {
  justify-self: end;
  font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
  background: var(--accent); color: var(--accent-ink);
  padding: .3em 1em; border-radius: var(--r-pill);
}

.board-conn {
  position: absolute; bottom: 4px; left: 50%; transform: translateX(-50%);
  margin: 0; font-size: 11px; color: var(--ink-3);
}

/* The only motion on the board. A point landing is the one event
   worth animating, and a short pulse reads as "that counted" from
   across the court where a number changing by one does not. */
@keyframes board-bump {
  0%   { transform: scale(1); }
  38%  { transform: scale(1.045); }
  100% { transform: scale(1); }
}
.board-side.is-bumped .score-dial { animation: board-bump .42s cubic-bezier(.3, 1.4, .5, 1); }

@media (prefers-reduced-motion: reduce) {
  .rally-btn, .board-side, .ring-fill { transition: none; }
  .board-side.is-bumped .score-dial { animation: none; }
}

/* ═══════════════ board controls, mirror, winner overlay ═══════════════ */

/* ── controls ───────────────────────────────────────────
   Invisible until the board is tapped, then gone again after a few
   seconds. visibility (not just opacity) so a hidden button can
   neither be tabbed to nor catch a stray tap meant for the board. */
.board-ctl {
  position: fixed; z-index: 30;
  top: max(10px, env(safe-area-inset-top)); right: max(12px, env(safe-area-inset-right));
  display: flex; gap: 8px;
  opacity: 0; visibility: hidden;
  transition: opacity .25s ease, visibility 0s linear .25s;
}
.board-ctl.is-shown { opacity: 1; visibility: visible; transition-delay: 0s; }
.ctl-btn {
  display: inline-flex; align-items: center; gap: .5em;
  min-height: 44px; padding: 0 14px;
  font: inherit; font-size: 14px; font-weight: 700; color: var(--ink);
  background: var(--surface); border: 1px solid var(--line-2); border-radius: var(--r-pill);
  box-shadow: var(--shadow-md); cursor: pointer;
}
.ctl-btn svg { width: 20px; height: 20px; flex: none; }
.ctl-btn[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }
.ctl-btn:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }

/* ── mirror ─────────────────────────────────────────────
   Swapping sides is a ROTATION of the serve glyph, not a scaleX flip.
   Each team's "left"/"right" service box is relative to its own facing
   (see QUAD in scoreboard.js). A team that walks to the other half
   now faces the opposite way, so its left box lands on the opposite
   screen edge AND the opposite top/bottom half. A horizontal flip
   would move the half but keep top/bottom, lighting the wrong box;
   a 180deg turn moves both, and needs no change to the geometry. */
.is-mirrored #boardSide0 { order: 2; }
.is-mirrored #boardSide1 { order: 1; }
.is-mirrored .court-mini { transform: rotate(180deg); }

/* ── winner overlay ─────────────────────────────────────
   Opaque enough to be the only thing read, but not a solid fill:
   the score behind it stays faintly visible so the moment is
   still "this match". */
.board-win {
  position: fixed; inset: 0; z-index: 40;
  display: flex; align-items: center; justify-content: center;
  padding: max(20px, env(safe-area-inset-top)) max(20px, env(safe-area-inset-right))
           max(20px, env(safe-area-inset-bottom)) max(20px, env(safe-area-inset-left));
  background: color-mix(in srgb, var(--ground) 90%, transparent);
  cursor: pointer; text-align: center;
}
.win-confetti { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.win-card { position: relative; max-width: 94vw; }
.win-title {
  margin: 0; font-weight: 800; letter-spacing: .22em; color: var(--good);
  font-size: clamp(20px, 4vw, 54px);
}
.win-names {
  margin: .25em 0 0; font-weight: 800; letter-spacing: -0.02em; line-height: 1.05;
  font-size: clamp(34px, min(9vw, 14vh), 150px); overflow-wrap: anywhere;
}
.win-line {
  margin: .5em 0 0; font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  font-weight: 700; color: var(--ink-2); font-size: clamp(18px, 3.4vw, 46px);
}
@keyframes win-in {
  from { opacity: 0; transform: scale(.94); }
  to   { opacity: 1; transform: scale(1); }
}
.board-win.is-in .win-card { animation: win-in .45s cubic-bezier(.2, .9, .3, 1.2); }
@media (prefers-reduced-motion: reduce) {
  .board-ctl { transition: none; }
  .board-win.is-in .win-card { animation: none; }
}

/* ═══════════════ open play (board) ═══════════════ */

/* Same rule as the match board: sizes follow the viewport so the
   courts read from the baseline. Everything is clipped, never
   scrolled: a board that scrolls is a board nobody can touch. */
.board-open {
  flex: 1; min-height: 0; display: flex; flex-direction: column;
  gap: clamp(8px, 1.6vh, 20px);
  font-size: clamp(12px, min(1.7vw, 2.6vh), 26px);
}
.bo-head { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; }
.bo-title {
  font-size: 1.3em; font-weight: 800; letter-spacing: -0.01em;
}
.bo-stats {
  font-size: .85em; font-weight: 600; color: var(--ink-3);
  text-transform: uppercase; letter-spacing: .09em;
}
.bo-main {
  flex: 1; min-height: 0; display: grid;
  grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
  gap: clamp(10px, 2vw, 28px);
}
.bo-courts {
  --bo-cols: 2;
  min-height: 0; overflow: hidden; align-content: stretch;
  display: grid; grid-template-columns: repeat(var(--bo-cols), minmax(0, 1fr));
  grid-auto-rows: minmax(0, 1fr);
  gap: clamp(8px, 1.4vw, 18px);
}
.bo-court {
  min-width: 0; min-height: 0; overflow: hidden;
  display: flex; flex-direction: column; gap: .35em; justify-content: space-between;
  padding: .7em .9em; border-radius: clamp(12px, 1.6vw, 22px);
  background: var(--surface-2); border: 2px solid var(--line);
}
.bo-court-top { display: flex; justify-content: space-between; align-items: center; gap: .5em; }
.bo-court-no { font-weight: 800; font-size: 1.15em; }
.bo-chip {
  padding: .15em .75em; border-radius: var(--r-pill);
  font-size: .8em; font-weight: 800; letter-spacing: .08em; text-transform: uppercase;
  background: var(--line); color: var(--ink-2);
}
.bo-court.is-playing { border-color: var(--good); }
.bo-court.is-playing .bo-chip { background: var(--good); color: var(--accent-ink); }
.bo-court.is-called  { border-color: var(--accent); background: var(--accent-wash); }
.bo-court.is-called  .bo-chip { background: var(--accent); color: var(--accent-ink); }
.bo-court.is-free { opacity: .8; }

.bo-teams { display: flex; flex-direction: column; align-items: center; text-align: center; gap: .05em; min-height: 0; }
/* Names grow as courts get fewer: with two courts each card is half the
   board, and 20px names in a half-screen card are unreadable from the far
   baseline. --bo-cols is the grid's own column count. */
.bo-team { margin: 0; max-width: 100%; font-weight: 700; line-height: 1.15; overflow-wrap: anywhere;
  font-size: clamp(1.2em, calc(4.2em / var(--bo-cols, 2)), 2.2em); }
.bo-vs { font-size: .75em; font-weight: 700; color: var(--ink-3); text-transform: uppercase; letter-spacing: .14em; }
.bo-timer {
  margin: 0; text-align: center; font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  font-weight: 700; font-size: 1.5em; color: var(--ink-2);
}
.bo-timer.is-call { font-family: var(--font-body); font-size: 1em; letter-spacing: .06em; text-transform: uppercase; color: var(--accent); }
.bo-court-free { margin: auto 0; text-align: center; font-weight: 600; color: var(--ink-3); }

/* Called is the one moment worth motion: players are being summoned.
   A two-beat ring pulse is visible across a hall without shouting. */
@keyframes bo-pulse {
  0%   { box-shadow: 0 0 0 0 var(--accent); transform: scale(1); }
  35%  { box-shadow: 0 0 0 .5em transparent; transform: scale(1.03); }
  70%  { box-shadow: 0 0 0 0 var(--accent); transform: scale(1); }
  100% { box-shadow: 0 0 0 .5em transparent; transform: scale(1); }
}
.bo-court.is-called-now { animation: bo-pulse 1.6s ease-out 2; }
@media (prefers-reduced-motion: reduce) { .bo-court.is-called-now { animation: none; } }

.bo-side { min-height: 0; display: flex; flex-direction: column; gap: clamp(8px, 1.4vw, 18px); }
.bo-panel {
  min-height: 0; overflow: hidden; padding: .7em .9em;
  border-radius: clamp(12px, 1.6vw, 22px); border: 1px solid var(--line-2); background: var(--surface);
}
.bo-next { flex: 1.5; }
.bo-lb   { flex: 1; }
.bo-h {
  margin: 0 0 .4em; font-size: .8em; font-weight: 800;
  text-transform: uppercase; letter-spacing: .14em; color: var(--ink-3);
}
.bo-list, .bo-lbl { list-style: none; margin: 0; padding: 0; }
.bo-next-row, .bo-lb-row {
  display: grid; align-items: baseline; gap: .6em;
  padding: .22em 0; border-bottom: 1px solid var(--line);
}
.bo-next-row:last-child, .bo-lb-row:last-child { border-bottom: 0; }
.bo-next-row { grid-template-columns: 1.4em minmax(0, 1fr) auto; font-size: 1.3em; font-weight: 700; }
.bo-lb-row   { grid-template-columns: 1.4em minmax(0, 1fr) auto auto; font-size: 1em; font-weight: 600; }
.bo-rank { color: var(--ink-3); font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.bo-nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bo-eta, .bo-wl, .bo-rating {
  font-family: var(--font-mono); font-variant-numeric: tabular-nums; color: var(--ink-2);
}
.bo-eta { font-size: .75em; font-weight: 600; }
.bo-delta { margin-left: .4em; font-size: .8em; font-weight: 700; }
.bo-delta.is-up { color: var(--good); }
.bo-delta.is-down { color: var(--accent); }
.bo-empty { color: var(--ink-3); font-weight: 600; list-style: none; }
.bo-break {
  margin: 0; font-size: .95em; font-weight: 600; color: var(--ink-2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* Portrait or a narrow window: one column, courts above the lists. */
@media (max-aspect-ratio: 1/1) {
  .bo-main { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1.3fr) minmax(0, 1fr); }
  .bo-courts { --bo-cols: 2 !important; }
  .bo-side { flex-direction: row; }
  .bo-side .bo-panel { flex: 1; }
}

/* ═══════════════ open play (phone) ═══════════════ */

.op-live { display: flex; flex-direction: column; gap: 12px; max-width: 620px; margin: 0 auto; }
.op-top { display: flex; flex-direction: column; gap: 2px; }
.op-summary { margin: 0; font-size: 13px; font-weight: 600; color: var(--ink-2); }
.op-top .scorer-sync { margin: 0; align-self: flex-start; }
.op-card { padding: 16px 14px; }
.op-card .card-title { margin-bottom: 10px; }
.op-head { display: flex; justify-content: space-between; align-items: center; }
.op-head .card-title { margin-bottom: 10px; }
.op-sub { font-size: 12.5px; font-weight: 700; color: var(--ink-2); margin: 16px 0 8px; }

/* steppers */
.op-step { display: inline-flex; align-items: center; gap: 4px; background: var(--surface-2); padding: 4px; border-radius: var(--r-pill); align-self: flex-start; }
.op-step-btn {
  width: 38px; height: 38px; border-radius: 50%; border: 0; cursor: pointer;
  background: var(--surface); color: var(--ink); font-size: 20px; line-height: 1; font-weight: 600;
  box-shadow: var(--shadow-sm);
}
.op-step-btn:active { transform: scale(.94); }
.op-step-btn:focus-visible, .op-chip:focus-visible, .op-side:focus-visible, .op-lb-row:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.op-step-val { min-width: 44px; text-align: center; font-size: 17px; font-weight: 700; font-variant-numeric: tabular-nums; }
.op-seg-4 label { font-size: 12px; padding: 7px 2px; }
.op-seg-4, .match-setup .seg { min-width: 0; }
#opAwayKeep + label, #opAwayBack + label, #opCfgAwayKeep + label, #opCfgAwayBack + label { font-size: 12px; }

/* courts */
.op-courts { display: grid; gap: 10px; }
.op-court { border: 1px solid var(--line); border-radius: var(--r-input); padding: 12px; background: var(--surface); }
.op-court.is-called { border-color: var(--accent); background: var(--accent-wash); }
.op-court.is-playing { border-color: var(--good); background: var(--good-wash); }
.op-court-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.op-court-head h3 { margin: 0; font-size: 14.5px; font-weight: 700; flex: 1; }
.op-state { font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--ink-3); background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r-pill); padding: 1px 8px; }
.op-state.is-called { color: var(--accent); border-color: var(--accent); background: var(--surface); }
.op-state.is-playing { color: var(--good); border-color: var(--good); background: var(--surface); }
.op-time { font-family: var(--font-mono); font-size: 13px; font-variant-numeric: tabular-nums; color: var(--ink-2); }
.op-hint { margin: 0; font-size: 13px; color: var(--ink-3); }
.op-teams { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 8px; margin-bottom: 10px; }
.op-team { margin: 0; font-size: 14px; font-weight: 600; overflow-wrap: anywhere; }
.op-team:last-child { text-align: right; }
.op-vs { font-size: 11px; color: var(--ink-3); text-transform: uppercase; }
.op-btns { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.op-btns .btn { padding: 9px 16px; }
.op-btns .btn-ghost { padding: 9px 10px; }

/* queue rows */
.op-q { display: flex; align-items: center; gap: 10px; padding: 9px 0; border-top: 1px solid var(--line); }
.op-q:first-child { border-top: 0; }
.op-q-no { flex: none; width: 26px; height: 26px; border-radius: 50%; background: var(--surface-2); color: var(--ink-2); font-size: 12px; font-weight: 700; display: grid; place-items: center; }
.op-q-body { flex: 1; min-width: 0; }
.op-q-name { margin: 0; font-size: 14.5px; font-weight: 600; overflow-wrap: anywhere; }
.op-more { font-size: 20px; line-height: 1; min-width: 44px; min-height: 40px; text-decoration: none; }

/* check-in */
.op-add { display: flex; gap: 8px; }
.op-add input { flex: 1; min-width: 0; }
.op-levels { display: flex; flex-direction: column; gap: 6px; margin-top: 10px; }
.op-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.op-chip {
  font: inherit; font-size: 13.5px; font-weight: 600; cursor: pointer;
  background: var(--surface-2); color: var(--ink); border: 1px solid var(--line-2);
  border-radius: var(--r-pill); padding: 8px 14px; min-height: 38px;
}
.op-chip:hover { border-color: var(--ink-3); }
.op-chip[aria-pressed="true"] { background: var(--ink); color: var(--ground); border-color: var(--ink); }
.op-letters { margin-bottom: 8px; }
.op-letter { padding: 6px 0; min-width: 34px; min-height: 34px; font-size: 12.5px; }
#opPickChips .op-chips + .op-chips { margin-top: 4px; }

/* leaderboard */
.op-lb { display: flex; flex-direction: column; }
.op-lb-row {
  display: grid; grid-template-columns: 22px minmax(0, 1fr) 40px 40px 34px 62px; gap: 6px; align-items: center;
  font: inherit; font-size: 12.5px; color: var(--ink); text-align: left;
  background: none; border: 0; border-top: 1px solid var(--line); padding: 9px 0; width: 100%;
  font-variant-numeric: tabular-nums;
}
button.op-lb-row { cursor: pointer; }
button.op-lb-row:hover { background: var(--surface-2); }
.op-lb-head { border-top: 0; font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .03em; color: var(--ink-3); padding-top: 0; }
.op-lb-name { font-size: 13.5px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.op-delta { margin-left: 3px; font-size: 10.5px; font-weight: 700; }
.op-delta.up { color: var(--good); }
.op-delta.down { color: var(--accent); }

/* session actions */
.op-acts-grid { display: flex; flex-wrap: wrap; gap: 4px 6px; margin-bottom: 10px; }

/* dialogs */
.op-sides { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.op-side {
  font: inherit; font-size: 14px; font-weight: 600; cursor: pointer; color: var(--ink);
  background: var(--surface-2); border: 2px solid var(--line-2); border-radius: var(--r-input);
  padding: 14px 8px; min-height: 64px; overflow-wrap: anywhere;
}
.op-side[aria-pressed="true"] { border-color: var(--good); background: var(--good-wash); }
.op-score { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
#opResultDlg .form-actions { flex-wrap: wrap; }
.op-menu-btn { text-align: left; padding: 12px 10px; font-size: 14px; }
.op-stats { display: grid; grid-template-columns: auto 1fr; gap: 6px 14px; margin: 0; font-size: 13.5px; }
.op-stats dt { color: var(--ink-3); }
.op-stats dd { margin: 0; font-weight: 600; text-align: right; overflow-wrap: anywhere; }
.op-prov { font-size: 12px; font-weight: 500 !important; color: var(--ink-3); }

@media (prefers-reduced-motion: reduce) { .op-step-btn:active { transform: none; } }
