/* Every colour in this stylesheet comes from a token defined here, and
   nowhere else, so the dark block below is the only other place a colour
   is written down. A literal anywhere else is a bug: it will look right in
   one theme and wrong in the other.

   Dark is a designed palette, not an inversion. Two things needed real
   choices rather than arithmetic: the status tints, which carry meaning
   and simply vanish when a 15%-alpha wash is laid over a dark row, so
   dark gets its own opaque shades; and the stripe that fades other
   leagues' rows, which recedes toward white on a light page and so has to
   recede toward black on a dark one. */
:root {
  color-scheme: light dark;

  /* surfaces and type */
  --bg: #ffffff;
  --surface: #ffffff;
  --text: #1a1a1a;
  --muted: #666;
  --border: #d8d8d8;

  /* accents and buttons */
  --accent: #2f6f4f;        /* links and other accents */
  --btn: #3d8a63;           /* buttons at rest -- lighter than the accent */
  --btn-hover: #275c42;     /* and darkening on hover */
  --btn-text: #fff;
  --focus-ring: rgba(47, 111, 79, 0.25);

  /* destructive */
  --danger: #d9534f;
  --danger-hover: #a02622;
  --danger-text: #fff;
  --error-text: #b3261e;

  /* match status. Colour is never the only cue -- the marker in the first
     cell says the same thing -- so this reads fine without colour vision. */
  --tint-proposed: rgba(233, 146, 40, 0.16);
  --tint-accepted: rgba(45, 140, 90, 0.15);
  --flash-proposed: rgba(233, 146, 40, 0.60);
  --flash-accepted: rgba(45, 140, 90, 0.55);

  /* rows belonging to another league, striped to recede */
  --stripe-wash: rgba(252, 252, 252, 0.92);
  --legend-base: rgba(120, 120, 120, 0.16);
  --legend-stripe: rgba(253, 253, 253, 0.95);

  /* seats in a lineup */
  --seat-line-idle: #cfcfcf;
  --seat-text: #333;
  --seat-release-fill: #fae8e7;
  --seat-release-fill-hover: #f6dbda;
  --seat-release-text-hover: #a02622;
  --seat-claim-hover: rgba(47, 111, 79, 0.12);
  --caret: #999;

  /* fields you can edit in place */
  --field-bg: #ffffff;

  /* contact buttons */
  --icon-email: #d7e9fb;
  --icon-email-hover: #b9d8f6;
  --icon-msg: #d9f0d9;
  --icon-msg-hover: #bce5bc;

  /* dialogs */
  --dialog-shadow: 0 10px 40px rgba(0, 0, 0, 0.22);
  --backdrop: rgba(0, 0, 0, 0.38);

  /* standings, the one table you read rather than act on */
  --standings-bg: #eef2f9;
  --standings-border: #cfdaec;
  --standings-head: #4a5a75;
  --standings-stripe: rgba(255, 255, 255, 0.55);
  --standings-rule: #dde4f0;

  /* score preview in the report dialog */
  --preview-ok: rgba(45, 140, 90, 0.13);
  --preview-bad: rgba(217, 83, 79, 0.14);
  --preview-bad-text: #8c2019;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #15171a;
    /* Cards lift off the page rather than matching it, which is what keeps
       the sections legible as sections without drawing heavier borders. */
    --surface: #1e2126;
    --text: #e6e8ea;
    --muted: #9ba1a8;
    --border: #3a4047;

    /* The green has to come up to stay legible as a link on a dark ground;
       the button keeps its weight because it carries white text either way. */
    --accent: #7ec89b;
    --btn: #3d8a63;
    --btn-hover: #4fa77c;
    --focus-ring: rgba(126, 200, 155, 0.38);

    --danger: #e0605c;
    --danger-hover: #c2413d;
    --error-text: #ff9d97;

    /* Opaque, not a wash: alpha over a dark row reads as almost nothing,
       which is how the proposed/scheduled distinction disappeared. */
    --tint-proposed: #3a2c1b;
    --tint-accepted: #1e3329;
    --flash-proposed: #6b4a1e;
    --flash-accepted: #2c5a43;

    --stripe-wash: rgba(8, 9, 11, 0.55);
    --legend-base: rgba(150, 150, 150, 0.18);
    --legend-stripe: rgba(8, 9, 11, 0.6);

    --seat-line-idle: #4a5157;
    --seat-text: #d3d7db;
    --seat-release-fill: #43211f;
    --seat-release-fill-hover: #5a2b28;
    --seat-release-text-hover: #ffb3ae;
    --seat-claim-hover: rgba(126, 200, 155, 0.16);
    --caret: #8b9299;

    --field-bg: #282c31;

    --icon-email: #22384f;
    --icon-email-hover: #2d4a68;
    --icon-msg: #20402a;
    --icon-msg-hover: #2b5638;

    --dialog-shadow: 0 10px 40px rgba(0, 0, 0, 0.6);
    --backdrop: rgba(0, 0, 0, 0.62);

    --standings-bg: #1b2430;
    --standings-border: #2f3d4f;
    --standings-head: #9fb2cc;
    --standings-stripe: rgba(255, 255, 255, 0.04);
    --standings-rule: #2b3646;

    --preview-ok: rgba(45, 140, 90, 0.26);
    --preview-bad: rgba(217, 83, 79, 0.26);
    --preview-bad-text: #ffb3ae;
  }
}

* { box-sizing: border-box; }

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  max-width: 900px;
  margin: 0 auto;
  padding: 0 1rem 3rem;
  line-height: 1.5;
  background: var(--bg);
  color: var(--text);
}

/* The league page's match tables carry user-supplied locations and up to
   four player names per row, so 900px clips the actions column once real
   data is in. Form pages stay narrow. */
body.wide { max-width: 1100px; }

nav {
  display: flex;
  gap: 1rem;
  align-items: center;
  padding: 1rem 0;
  border-bottom: 1px solid var(--border);
  margin-bottom: 1.5rem;
  flex-wrap: wrap;
}

nav a, nav button.link {
  text-decoration: none;
  color: var(--accent);
  font-weight: 600;
  background: none;
  border: none;
  cursor: pointer;
  font-size: 1rem;
  padding: 0;
}

nav .spacer { flex: 1; }

nav .whoami { color: var(--muted); font-weight: normal; }

h1 { margin-top: 0; }

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 1rem;
  margin-bottom: 1rem;
}

table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 0.5rem; border-bottom: 1px solid var(--border); }

form .field { margin-bottom: 1rem; }
label { display: block; font-weight: 600; margin-bottom: 0.25rem; }
input, select { padding: 0.4rem; width: 100%; max-width: 320px; }

button, .button {
  background: var(--btn);
  color: white;
  border: none;
  padding: 0.5rem 1rem;
  border-radius: 6px;
  cursor: pointer;
  font-size: 1rem;
  text-decoration: none;
  display: inline-block;
  transition: background-color 0.15s ease, border-color 0.15s ease;
}

/* `display: inline-block` above outranks the browser's own
   `[hidden] { display: none }`, so anything hidden from script -- the
   inline-edit Save buttons -- stays stubbornly on screen without this. */
button[hidden], .button[hidden] { display: none; }

button.danger, .button.danger {
  background: var(--danger);
  color: var(--danger-text);
  border: 1px solid var(--danger);
  font-weight: 600;
}

/* Hover states are gated on the device actually having hover: on touch,
   :hover latches after a tap and leaves buttons looking stuck in their
   pressed shade until you tap elsewhere. */
@media (hover: hover) {
  button:hover, .button:hover { background: var(--btn-hover); }
  button.danger:hover, .button.danger:hover {
    background: var(--danger-hover);
    border-color: var(--danger-hover);
  }
  .icon-btn.email:hover { background: var(--icon-email-hover); }
  .icon-btn.msg:hover { background: var(--icon-msg-hover); }
}

.error { color: var(--error-text); font-weight: 600; margin-bottom: 1rem; }
.muted { color: var(--muted); }
.tabs { display: flex; gap: 1rem; margin-bottom: 1rem; }
.tabs a.active { text-decoration: underline; }
.set-row { display: flex; gap: 0.5rem; align-items: center; margin-bottom: 0.5rem; }
.set-row input { max-width: 80px; }

.icon-btn {
  background: none;
  border: none;
  font-size: 1rem;
  cursor: pointer;
  padding: 0.22rem 0.4rem;
  line-height: 1;
  border-radius: 6px;
}
/* Tinted so the contact actions read as buttons rather than loose emoji.
   Emoji supply their own color, so these only need to carry the tint. */
.icon-btn.email { background: var(--icon-email); }
.icon-btn.msg { background: var(--icon-msg); }

/* Compact league header: name + type on the left, commissioner pushed
   right, description in small type underneath. */
.league-header {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.3rem 1.5rem;
  margin-bottom: 0.2rem;
}
.league-header h1 { margin: 0; font-size: 1.55rem; }
/* Type sits in the same line as the league name, dialed back so the name
   stays the thing you read first. */
.league-header .league-type {
  font-size: 0.95rem;
  font-weight: 500;
  color: var(--muted);
  letter-spacing: 0.01em;
}
.league-header .commissioner {
  margin-left: auto;
  color: var(--muted);
  font-size: 0.9rem;
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
}
.league-header .commissioner strong { color: inherit; }
.league-description {
  margin: 0 0 1.25rem;
  font-size: 0.85rem;
  color: var(--muted);
}

/* Section heading with its action buttons inline on the right. */
.section-header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.6rem;
}
.section-header h2 { margin: 0; font-size: 1.1rem; }
.section-header .actions { margin-left: auto; display: flex; gap: 0.4rem; }

.button.small, button.small { padding: 0.3rem 0.6rem; font-size: 0.85rem; }

.table-scroll { overflow-x: auto; }

/* Tabular match/proposal rows so columns line up down the list. */
table.rows { width: 100%; border-collapse: collapse; font-size: 0.92rem; }
table.rows th {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  font-weight: 600;
  padding: 0.2rem 0.35rem;
  white-space: nowrap;
}
table.rows td {
  padding: 0.35rem 0.35rem;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
  vertical-align: middle;
}
table.rows tr:last-child td { border-bottom: none; }
table.rows td.wrap { white-space: normal; }
table.rows td.marker {
  width: 1.5rem;
  text-align: center;
  padding-right: 0;
}
/* The actions stay on one line even when a scheduled match carries four
   of them: wrapped, Report score and Cancel sit on top of each other and
   read as two separate rows. The width for that comes from the short
   dates and the tight column padding above. */
/* Each kind of action gets a column of its own, so a row that's missing
   one doesn't shuffle the rest along and leave the table looking ragged
   down its right-hand side. They sit shoulder to shoulder, so they don't
   each need a full gutter. */
table.rows td.actions {
  text-align: right;
  white-space: nowrap;
  padding-left: 0.15rem;
  padding-right: 0.15rem;
}
table.rows td.actions:last-child { padding-right: 0.35rem; }
table.rows td.actions form {
  display: inline-flex;
  gap: 0.3rem;
  align-items: center;
  margin: 0;
}

/* Accepting used to mean a button plus a partner picker in this column,
   which needed fixed grid tracks to stop the button drifting with the
   width of a native <select>. Partners are now chosen by claiming a spot
   in the lineup, so the column is buttons again and can size itself. */
table.rows td.actions form.row-action button { min-width: 5rem; }
table.rows td.actions button,
table.rows td.actions .button { padding: 0.3rem 0.6rem; font-size: 0.85rem; }

.card h2 { margin-top: 0; }

/* Phone layout: the match tables are far wider than a phone screen, and
   the actions column is what falls off the right edge. Below this width
   each row becomes a stacked card instead, so nothing needs sideways
   scrolling to reach. Column headers are replaced by per-cell labels
   drawn from each cell's data-label. */
@media (max-width: 600px) {
  table.cards thead { display: none; }
  table.cards,
  table.cards tbody,
  table.cards tr,
  table.cards td { display: block; width: 100%; }

  table.cards tr {
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 0.6rem 0.75rem;
    margin-bottom: 0.6rem;
  }
  table.cards tr:last-child { margin-bottom: 0; }

  table.cards td {
    border-bottom: none;
    white-space: normal;
    padding: 0.12rem 0;
    display: flex;
    gap: 0.5rem;
  }
  table.cards td::before {
    content: attr(data-label);
    flex: 0 0 5.5rem;
    color: var(--muted);
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    padding-top: 0.2rem;
  }

  /* Marker emoji and match type form the card's title line. */
  table.cards td.marker,
  table.cards td.title {
    display: inline-block;
    width: auto;
    padding: 0 0.35rem 0.35rem 0;
    vertical-align: middle;
  }
  table.cards td.title { font-weight: 700; font-size: 1rem; }
  table.cards td.marker::before,
  table.cards td.title::before { content: none; }

  /* Actions go full width at the bottom of the card, thumb-sized. */
  table.cards td.actions {
    flex-wrap: wrap;
    gap: 0.4rem;
    padding-top: 0.6rem;
    text-align: left;
  }
  table.cards td.actions::before { content: none; }
  table.cards td.actions.empty { display: none; }
  /* Stacked as separate blocks on a card, they'd each open with the same
     gap the first one needs. */
  table.cards td.actions + td.actions { padding-top: 0; }
  table.cards td.actions > form,
  table.cards td.actions > .button { flex: 1 1 auto; }
  /* Cards are narrower than the desktop tracks, so drop the fixed grid
     and let the button and picker share the card's full width instead. */
  table.cards td.actions > form,
  table.cards td.actions > form.row-action {
    display: flex;
    min-width: 0;
    grid-template-columns: none;
  }
  table.cards td.actions form button,
  table.cards td.actions > .button {
    width: 100%;
    text-align: center;
    padding: 0.5rem 0.6rem;
  }
  table.cards td.actions .icon-btn { flex: 0 0 auto; padding: 0.45rem 0.6rem; }
}

/* Modal forms. Sized so a tall form scrolls inside the panel rather than
   pushing off-screen -- matters most on phones, where the on-screen
   keyboard eats much of the viewport. */
dialog {
  border: none;
  border-radius: 10px;
  padding: 0;
  width: calc(100% - 2rem);
  max-width: 420px;
  max-height: calc(100vh - 2rem);
  max-height: calc(100dvh - 2rem);
  overflow: auto;
  box-shadow: var(--dialog-shadow);
  color: inherit;
  background: var(--surface);
}
dialog::backdrop { background: var(--backdrop); }
.dialog-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.85rem 1rem;
  border-bottom: 1px solid var(--border);
}
.dialog-head h2 { margin: 0; font-size: 1.05rem; }
/* A dialog gives focus to its first control when it opens, and unless the
   dialog names one with autofocus that's this button -- so it wears a
   focus ring from the moment the dialog appears. WebKit's `outline: auto`
   traces a shape of its own, which sat lopsidedly around a small button
   with uneven padding. Square it off and draw our own ring. */
.dialog-close {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.9rem;
  height: 1.9rem;
  padding: 0;
  background: none;
  border: none;
  border-radius: 6px;
  color: var(--muted);
  font-size: 1.4rem;
  line-height: 1;
  cursor: pointer;
}
.dialog-close:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}
@media (hover: hover) {
  .dialog-close:hover { color: var(--text); }
}
/* 22px was a small thing to hit with a thumb. */
@media (pointer: coarse) {
  .dialog-close { width: 2.6rem; height: 2.6rem; }
}
.dialog-body { padding: 1rem; }
.dialog-body .field:last-of-type { margin-bottom: 1rem; }
.dialog-body input,
.dialog-body select { max-width: 100%; }

/* iOS Safari zooms the whole page in whenever a focused form control has
   a font-size below 16px, and does not zoom back out -- the user is left
   pinching to read a form they were part-way through. The UA default for
   text inputs is ~13px, so every field here tripped it, including the
   ones inside modals.

   Scoped to coarse pointers so desktop control sizing is untouched; the
   later, equally-specific selectors below re-assert the size over the
   smaller values set for table rows. */
@media (pointer: coarse) {
  input,
  select,
  textarea,
  .dialog-body input,
  .dialog-body select,
  table.rows td .inline-edit {
    font-size: 16px;
  }
}

/* Empty date/time inputs render as blank pills on iOS with nothing to
   suggest they're tappable. Desktop draws its own mm/dd/yyyy affordance,
   so the hint is only shown where it's actually missing. */
.field-hint { display: none; }
@media (pointer: coarse) {
  .field-hint {
    display: block;
    margin: 0.3rem 0 0;
    font-size: 0.8rem;
    color: var(--muted);
  }
}

/* Row colour carries the match's status: orange while it's still only a
   proposal, green once someone has accepted. Each status also names a
   stronger shade of itself for the striped variant below, so the two
   signals compose instead of fighting.

   Colour is never the only cue -- the 🙋/🎾 marker in the first cell says
   the same thing -- so this reads fine without colour vision. */
tr.status-proposed { --status-tint: var(--tint-proposed); }
tr.status-accepted { --status-tint: var(--tint-accepted); }
tr.status-proposed,
tr.status-accepted { background-color: var(--status-tint); }

/* "Your Matches and Proposals" spans every league the player belongs to,
   so rows from elsewhere are striped. The stripe washes toward near-white
   rather than adding a darker colour: these rows are context for spotting
   clashes, not things to act on here, so they should recede next to the
   league you're actually looking at. Striping a darker shade over them had
   the opposite effect and pulled the eye straight to them. */
tr.other-league {
  background-image: repeating-linear-gradient(
    45deg,
    transparent 0 7px,
    var(--stripe-wash) 7px 14px
  );
}

/* The key only has to say "striped means elsewhere", so it stays neutral
   rather than picking one of the two status colours. Slightly stronger
   than the rows themselves so a 2rem swatch still reads on a white card. */
.other-league-fill {
  background-color: var(--legend-base);
  background-image: repeating-linear-gradient(
    45deg,
    transparent 0 7px,
    var(--legend-stripe) 7px 14px
  );
}

.section-note { margin: -0.3rem 0 0.7rem; font-size: 0.82rem; }

.legend {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.8rem;
  color: var(--muted);
}
.legend-swatch {
  width: 2.2rem;
  height: 1rem;
  border: 1px solid var(--border);
  border-radius: 3px;
  display: inline-block;
}

/* Name plus its contact buttons, kept on one line and vertically centred. */
.with-contact {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
}

/* Flash a row that was just added or changed, then settle to its normal
   status colour, so an action taken in a dialog is visible in the list you
   land back on. */
tr.status-proposed { --status-flash: var(--flash-proposed); }
tr.status-accepted { --status-flash: var(--flash-accepted); }

@keyframes settle-to-status {
  from { background-color: var(--status-flash); }
  to   { background-color: var(--status-tint); }
}
tr.just-changed { animation: settle-to-status 3s ease-out 1; }

@media (prefers-reduced-motion: reduce) {
  tr.just-changed { animation: none; }
}

/* Score entry laid out as a grid, with each player's name over their own
   column so there's no guessing which box is whose. */
.score-grid { width: auto; border-collapse: collapse; }
.score-grid th,
.score-grid td { border: none; padding: 0.2rem 0.35rem; text-align: left; }
.score-grid thead th {
  font-size: 0.78rem;
  color: var(--muted);
  font-weight: 600;
  max-width: 8rem;
}
.score-grid tbody th { font-weight: 600; font-size: 0.9rem; white-space: nowrap; }
.score-grid input { width: 3.5rem; max-width: 3.5rem; text-align: center; }
.score-grid .dash { color: var(--muted); }

.score-preview {
  display: block;
  margin: 0.75rem 0;
  padding: 0.6rem 0.75rem;
  border-radius: 6px;
  font-size: 0.9rem;
  min-height: 1.2rem;
}
.score-preview:empty { display: none; }
.score-preview.ok { background: var(--preview-ok); }
.score-preview.invalid { background: var(--preview-bad); color: var(--preview-bad-text); }
.score-preview.muted { background: none; color: var(--muted); padding-left: 0; }

.result-winner { font-size: 1.15rem; font-weight: 700; margin: 0 0 0.25rem; }

/* Format hints that should show on desktop too, unlike the touch-only
   "tap to pick" ones. */
.field-hint.always { display: block; margin: 0.3rem 0 0; font-size: 0.8rem; color: var(--muted); }

/* A submit that can't do anything yet should look like it. */
button:disabled, .button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* ---- lineups ----
   Who's in a match and which spots are still going. Both singles and
   doubles lay their seats on the same three-column grid -- seat,
   separator, seat -- at a fixed width, so every "/" and "vs" lines up
   down the table instead of wandering with the length of the names
   either side of it. Doubles fills two rows of it, singles one.

   Every seat is boxed, and the box says what you can do with it: dashed
   green for a spot going begging, filled red for one you're able to free
   up, faint grey for everyone else's. The grey is the important half --
   the two live states stand out because the inert ones step back, not
   because they shout, which is what a boxed-and-dark seat everywhere was
   forcing them to do. */
.lineup {
  display: grid;
  grid-template-columns: 7rem 1.2rem 7rem;
  align-items: center;
  row-gap: 0.25rem;
  column-gap: 0.15rem;
}
.seat-join {
  color: var(--muted);
  font-size: 0.8rem;
  text-align: center;
}
.seat-form { display: block; margin: 0; width: 100%; }

.seat {
  display: block;
  width: 100%;
  padding: 0.15rem 0.35rem;
  border: 1px solid var(--seat-line, var(--seat-line-idle));
  border-radius: 5px;
  font-size: 0.9rem;
  line-height: 1.3;
  text-align: center;
  white-space: normal;
  background: transparent;
  color: inherit;
}
.seat.taken { color: var(--seat-text); }
.seat.open { color: var(--muted); font-style: italic; }

/* An empty spot you can take. Dashed rather than solid so it reads as a
   gap to fill instead of a finished thing. */
button.seat.claim {
  --seat-line: var(--accent);
  border-style: dashed;
  color: var(--accent);
  font-weight: 600;
  cursor: pointer;
}
/* An open spot you can fill on someone's behalf. Drawn as one of our own
   seat boxes rather than as a form control: native select chrome matches
   nothing else on the page and differs between browsers, so the appearance
   is stripped and the caret painted on. It reads as the "Open" box it
   replaces until you go near it. */
select.seat.pick {
  -webkit-appearance: none;
  appearance: none;
  cursor: pointer;
  padding-right: 1.1rem;
  background-image: linear-gradient(45deg, transparent 50%, var(--caret) 50%),
                    linear-gradient(135deg, var(--caret) 50%, transparent 50%);
  background-position: calc(100% - 12px) 0.62em, calc(100% - 8px) 0.62em;
  background-size: 4px 4px, 4px 4px;
  background-repeat: no-repeat;
}
@media (hover: hover) {
  select.seat.pick:hover { --seat-line: var(--muted); }
}
/* Only ever seen with JavaScript off, where it's the way to commit a pick. */
.seat-go { display: block; width: 100%; margin-top: 2px; }

/* A name you can vacate -- yours, or anyone the proposer penciled in.
   Red, and filled rather than merely outlined: fill is what the eye finds
   first, and an outline alone left these indistinguishable from the names
   around them. Clicking one takes a player out of the match, which the
   hover strikethrough spells out.

   The fill is opaque on purpose. As a translucent red it picked up
   whatever row it sat on -- pink over an orange proposal, but muddied to
   a warm grey over a green scheduled match, which is where it matters
   most. Opaque, the control looks the same wherever it lands. */
button.seat.taken {
  --seat-line: var(--danger);
  background: var(--seat-release-fill);
  cursor: pointer;
}
@media (hover: hover) {
  button.seat.claim:hover { background: var(--seat-claim-hover); }
  button.seat.taken:hover {
    background: var(--seat-release-fill-hover);
    color: var(--seat-release-text-hover);
    text-decoration: line-through;
  }
}
@media (pointer: coarse) {
  .seat { padding: 0.35rem 0.4rem; font-size: 16px; }
  .lineup { grid-template-columns: 8.5rem 1.4rem 8.5rem; }
}
/* On a phone the lineup shares its line with the row's label, and there
   isn't room for the fixed tracks -- they ran off the side of the card,
   taking the fourth player with them. Nothing needs to line up with the
   row above here anyway: each match is its own card. */
@media (max-width: 600px) {
  table.cards td .lineup {
    grid-template-columns: minmax(0, 1fr) 1.2rem minmax(0, 1fr);
    flex: 1 1 auto;
    min-width: 0;
  }
}

/* ---- editing a match in place ----
   Time and location stay editable while a match is live: someone books a
   court and writes the number in where the other three will see it. The
   fields are drawn as fields -- boxed, on a paler ground -- so it's
   obvious which cells in a table of otherwise fixed detail can be
   changed, and which can't. */
.inline-edit {
  font: inherit;
  color: inherit;
  background: var(--field-bg);
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 0.15rem 0.3rem;
  width: auto;
  max-width: 100%;
}
.inline-edit.wide { min-width: 8.5rem; }
/* A value you can't edit stands where an editable one's text stands, so
   the column reads as a column rather than as two ragged half-columns.
   Same box as .inline-edit, drawn in nothing. */
.inline-static {
  display: inline-block;
  border: 1px solid transparent;
  padding: 0.15rem 0.3rem;
}
@media (hover: hover) {
  .inline-edit:hover { border-color: var(--muted); }
}
.inline-edit:focus {
  border-color: var(--accent);
  outline: 2px solid var(--focus-ring);
}
.detail-edit { display: inline-flex; margin: 0; }

.commissioner-tools {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem 1.25rem;
}
.inline-form {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin: 0;
}
.inline-form label { margin: 0; font-size: 0.85rem; color: var(--muted); }

.ready-lineup { font-size: 1.05rem; margin-bottom: 0.2rem; }
.ready-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }

/* Standings is the one section you read rather than act on, so it gets a
   colour of its own. Cool blue-grey deliberately: orange and green are
   spoken for by match status and would read as a claim about the table. */
.card.standings {
  background: var(--standings-bg);
  border-color: var(--standings-border);
}
.card.standings table.rows th { color: var(--standings-head); }
.card.standings table.rows tr:nth-child(even) {
  background: var(--standings-stripe);
}
.card.standings table.rows td { border-bottom-color: var(--standings-rule); }

/* Nothing expires by itself, so a proposal nobody claimed or a match
   nobody reported stays in the list once its date has passed. Fading it
   says "this one has gone by" without hiding it -- a past scheduled match
   is precisely the one still owed a score.

   The actions keep their full strength: fading Report score would dim the
   button on exactly the rows that most need it. */
tr.past td { opacity: 0.5; }
tr.past td.actions { opacity: 1; }

/* ---- contact popover ----
   Who to text about a match, and their number. */
.contact-list {
  list-style: none;
  margin: 0 0 0.75rem;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}
.contact-list li {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 0.15rem 0.75rem;
  align-items: center;
  padding-bottom: 0.6rem;
  border-bottom: 1px solid var(--border);
}
.contact-list li:last-child { border-bottom: none; padding-bottom: 0; }
.contact-name { font-weight: 600; }
.copy-number { grid-row: span 2; }
.contact-number {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 1.05rem;
  letter-spacing: 0.01em;
  color: inherit;
  text-decoration: none;
  grid-column: 1;
}
/* The sms: link only leads anywhere on a device with a messaging app.
   Everywhere else the number is text to copy, so it shouldn't pretend to
   be a link. */
@media (hover: hover) and (pointer: fine) {
  .contact-number { pointer-events: none; }
}
@media (pointer: coarse) {
  .contact-number { text-decoration: underline; text-underline-offset: 3px; }
}
.contact-note { font-size: 0.82rem; margin: 0; }
