/* =========================================================================
   Kicksrv — Admin Command Center (prototype)
   Same broadcast/acid-green system as the player app, but desktop-dense.
   ========================================================================= */

/* Tokens live in static/tokens.css, which every page loads before this file.
   This copy was missing --chal, --tile, --divider-2, --t-dim-3 and --maxw
   outright, so any component moved here from climb.css lost them silently. */

* { box-sizing: border-box; margin: 0; padding: 0; }
/* The `hidden` attribute must always win, even over class rules that set
   display (e.g. .field{display:flex}) — used for show/hide in the demo screens. */
[hidden] { display: none !important; }
html, body { background: var(--screen); }
body.admin { font-family: var(--sans); color: var(--t-hi); -webkit-font-smoothing: antialiased; min-height: 100vh; }

/* ---- top chrome ---- */
.ah {
  position: sticky; top: 0; z-index: 20;
  display: flex; align-items: center; justify-content: space-between;
  padding: 14px 16px; background: var(--screen); border-bottom: 1px solid var(--divider);
}
.ah-brand { display: flex; align-items: center; gap: 10px; }
.ah-dot { width: 26px; height: 26px; border-radius: 50%; background: var(--acc); box-shadow: inset -5px -5px 0 -3px rgba(0,0,0,.18); }
.ah-word { font: 800 20px var(--cond); letter-spacing: .16em; text-transform: uppercase; }
.ah-tag { font: 700 9px var(--mono); letter-spacing: .14em; text-transform: uppercase; color: var(--on-acc); background: var(--acc); padding: 3px 7px; border-radius: var(--r-xs); }
.ah-right { display: flex; align-items: center; gap: 14px; }
.ah-asof { font: 500 10px var(--mono); letter-spacing: .04em; color: var(--t-muted); }
.ah-link {
  font: 700 11px var(--mono); letter-spacing: .08em; text-transform: uppercase;
  color: var(--acc); white-space: nowrap;
  border: 1px solid var(--acc-line);
  border-radius: var(--r-full); padding: 5px 11px; transition: background 150ms ease;
}
.ah-link:hover { background: var(--acc-container); }
.ah-back { cursor: pointer; }
.ah-back:hover { color: var(--acc); }

.admin-main { max-width: 1200px; margin: 0 auto; padding: 18px 16px 50px; }
/* A single-column page — sign in, sign up, join a club, revise settings, and the
   mid-onboarding token pages. Twelve templates wanted this and expressed it five
   ways: `.narrow`, which was defined NOWHERE so those four rendered at the full
   1200px with a lone form floating in the middle, plus inline max-widths of 420,
   460, 520 and 560px. One measure, comfortable for a form and for short prose. */
.admin-main.narrow { max-width: 480px; }

/* =========================================================================
   THE 2026-08 ADMIN ARTWORK — everything below `.ashell`

   ⚠ EVERY RULE IN THIS SECTION IS SCOPED, and the scope is the point.
   `admin.css` is not this shell's alone: seven pitch-demo templates on port
   5004 hand-write their own <head>, <link> this file and render
   `<body class="admin">`. Repainting `body.admin` would have moved the demo's
   screens onto navy under a `.season-band` whose radial-gradient ends on
   --screen, a `.health-ring-inner` on --surface and `.need`/`.ins` tuned for
   charcoal — and PYTEST NEVER RENDERS THAT HOST, so nothing would have failed.
   `.ashell` is set by templates/_base.html and by nothing else.

   ⚠ AND NOT BY REPOINTING A SYS ROLE. The three-line version of this section is
       .ashell { --surface: var(--card-ground); --surface-2: var(--surface-row) }
   and it PASSES test_only_tokens_css_declares_the_palette, whose regex is
   `:root\s*\{...\}` and does not see a class selector. That is exactly why not:
   it is the seven-:root drift wearing a costume the guard cannot catch, it
   repaints every component nobody reviewed, and it leaves the next person
   debugging a colour needing to know a body class re-points a role. The
   component rules are written out.
   ========================================================================= */

/* The canvas, not just the box. `html` carries its own background four rules
   up, and a background on `body` does not propagate to the canvas once it
   does — so without this the overscroll gutter stays charcoal while the page
   is navy. `:has()` is already relied on elsewhere in this file. */
html:has(body.ashell) { background: var(--screen-player); }
.ashell { background: var(--screen-player); }

/* TWO ROWS ON A PHONE, ONE FROM 880px, and the SAME three children in the same
   markup order both times — `order` and a 100% flex-basis do the splitting, so
   there is no second header to keep in sync. Lifted from `.pnav-bar`, which
   solved this first.

   The rule under the bar is 3px of --surface-panel, which is what the artwork
   draws. */
.ashell .ah {
  flex-wrap: wrap; gap: 0 14px;
  padding: 8px 16px 0;
  background: var(--screen-player);
  border-bottom: 3px solid var(--surface-panel);
}
.ashell .ah-brand { order: 1; flex: none; display: flex; align-items: center; height: 46px; }
.ashell .ah-brand .klogo { height: 30px; }
.ashell .ah-right { order: 2; margin-left: auto; height: 46px; }
/* The strip drops to its own row below 880px — see .atabs's own note for the
   width arithmetic that forces it. */
.atabs { order: 3; flex: 1 0 100%; display: flex; align-items: center; gap: 4px; padding-bottom: 6px; }

/* ONLY THE ACTIVE TAB IS A PILL. The others are bare labels carrying almost no
   horizontal padding — the 4px gap separates them, and the 44px target keeps
   them tappable.

   13px ON A PHONE, 17px FROM 880px, and this is not a free choice. At the
   player strip's 17px the four labels "ADMIN HOME PLAYERS LADDERS EVENTS"
   measure ~360px of text alone, which overflows 375 before any padding; at
   13px they come to ~275 and the row fits with room. player.css:246 records
   the same failure reached from the other direction — a nav that scrolls
   sideways is what the four-section design exists to prevent, and it does not
   care whether the cause was tab count or type size.

   admin.css is desk-shaped, so 13px here is the file's own base size rather
   than a shrunk version of the player view's courtside 17px. */
.atab {
  position: relative;
  display: flex; align-items: center;
  min-height: 30px; padding: 0 4px;
  font: 600 13px var(--sans); letter-spacing: .04em; text-transform: uppercase;
  color: var(--t-hi); border-radius: var(--r-row); white-space: nowrap;
}
/* The visible pill is 30px and the TARGET is 44 — one box would have drawn the
   pill 44px tall and eaten more of the bar than the artwork's ~36. Split with
   a ::before, the same arrangement .tab uses. */
.atab::before {
  content: ""; position: absolute; left: 0; right: 0;
  top: 50%; height: 44px; transform: translateY(-50%);
}
.atab:hover { background: var(--surface-row); }
/* Quieter than --acc on purpose: "the tab you are already on" is not the one
   thing per screen the eye should hit, and a strip in full accent would
   compete with the orange CTA below it. The measured pair — see the
   --nav-active note in tokens.css, and the contrast test that composites it
   over exactly this navy. */
.atab--active, .atab--active:hover {
  background: var(--nav-active); color: var(--on-nav-active);
  border-radius: var(--r-row); padding: 0 12px;
}

/* The right-hand group, quieter than it was. "Player view" and "Your clubs"
   stay — a multi-club admin has no other switcher, and /me has no other door —
   but they are navigation, not action, so they read as the artwork's grey
   rather than competing with the strip beside them. */
/* 11px AND A 10px GAP ON A PHONE, measured rather than chosen. At 12px with a
   12px gap the three of them came to ~260px, the brand another 69, and the two
   together landed exactly on 375's 343px of usable bar — so the group wrapped
   and the header became THREE rows before the strip had even been reached.
   Nothing overflowed; it just ate 150px of an 812px screen. The 12px values
   are in the min-width block, where there is room for them. */
.ashell .ah-asof { font: 500 11px var(--sans); letter-spacing: 0; color: var(--t-court); }
.ashell .ah-right { gap: 10px; }
.ashell .ah-back:hover { color: var(--t-hi); }
/* A MODIFIER, NOT A NEW DEFAULT. `.admin-main` stays at 1200px because the
   draw editor, the bracket and the import previews need it; the four screens
   the artwork covers opt into its 725px column. */
.admin-main.column { max-width: 725px; }

/* --- the surfaces every other admin page inherits ------------------------
   These are what stop the ~30 pages the artwork does not cover from becoming
   warm-charcoal islands on a cool navy page. One declaration each, and each is
   the same move: the component keeps its class, its layout and its tests, and
   changes only which surface it sits on. */
.ashell .panel { background: var(--card-ground); border-color: var(--card-line); }
.ashell .panel .prow,
.ashell .lcard { background: var(--surface-row); border-color: var(--card-line); }
.ashell .prow { background: var(--surface-row); border-color: var(--card-line); }
/* Everything that was on --surface-2, the inset surface, and now wants the
   card's row. Grouped rather than repeated so the next inset joins a list. */
.ashell .field-input,
.ashell .field-textarea,
.ashell .icon-btn,
.ashell .menu-pop,
.ashell .suggest,
.ashell .seg,
.ashell .rule-card,
.ashell .uploader { background: var(--surface-row); border-color: var(--card-line); }
/* The artwork's grey for a subtitle, and a step brighter on navy than
   --t-muted was on charcoal. */
.ashell .page-sub { color: var(--t-court); }

/* A section label with a rule running off to the right — the artwork's
   "Players ————" head. `.sech` itself lives in shared.css and the pitch demo's
   CRM uses it, so BOTH the line and the type below are added HERE, under the
   scope, rather than by editing the shared rule (CLAUDE.md §1: the demo's own
   screens do not move for an admin change).

   SENTENCE CASE IN THE BODY FACE, per the 2026-08c board — the shared rule is
   11px mono, tracked out and uppercased, which is the loudest a label gets in
   this system and was competing with the pill now sitting on the same line. */
.ashell .sech {
  display: flex; align-items: center; gap: 14px;
  font: 400 var(--fs-title) var(--sans); letter-spacing: 0;
  text-transform: none; color: var(--t-court);
}
.ashell .sech::after {
  content: ""; flex: 1; height: 1px; background: var(--card-line);
}
/* THE VARIANT THAT CARRIES A CONTROL — "Current ladders ———— [Create a new
   ladder]". The rule has to sit BETWEEN the label and the pill, and a
   pseudo-element cannot: `::after` is always the last child and `order` on it
   would need every sibling ordered too. So the rule becomes a real <span> and
   the pseudo-element is switched off, rather than a second head component
   existing alongside this one. */
.ashell .sech--act::after { content: none; }
.ashell .sech-line { flex: 1; height: 1px; background: var(--card-line); }

/* --- the grouped card ----------------------------------------------------
   templates/_admin_card.html. A --card-ground box with a hairline, a header
   strip, and rows separated by 2px of that ground showing through. The gaps
   ARE the dividers — no border between rows, no wash behind them.

   `overflow: hidden` rather than per-corner radii on the first and last rows.
   The player card cannot do that (its rows collapse, so a hidden first row
   still matches :first-child and the radius lands on an invisible element);
   nothing here collapses, so the cheap version is also the correct one. */
.acard {
  background: var(--card-ground);
  border: 1px solid var(--card-line);
  border-radius: var(--r-l);
  overflow: hidden;
  margin-bottom: var(--sp-6);
}
/* THE HEAD RECESSES UNDER THE ROWS. --surface-card-head is darker than
   --surface-row, where the player card's --surface-panel is lighter than it.
   See tokens.css: that inversion is the one place the two cards differ, and it
   is the artwork's, not an accident. */
.acard-head {
  display: flex; align-items: center; gap: var(--sp-3);
  min-height: 48px; padding: var(--sp-2) var(--sp-4);
  background: var(--surface-card-head);
}
/* Orange, and SENTENCE CASE IN THE BODY FACE as of the 2026-08c board. It was
   Saira Condensed uppercase, which is this system's label voice — right when
   every one of these read "PLAYER DIRECTORY", i.e. was a category name over a
   list. It stopped being true the moment the Ladders and Events tabs made the
   title the ladder's or the event's OWN NAME: "2026 A LADDER" set in condensed
   caps is a label about a thing, and what the board draws is the thing itself.
   A proper noun gets the body face.

   --action reads at this size on this surface; --action-inline is the value
   for orange text at 11px on a row, and this is neither.

   IT IS A LINK ONLY WHERE IT HAS SOMEWHERE TO GO (`title_href` in
   _admin_card.html). Orange with no underline is the permitted link form —
   DESIGN.md §2 gives two, white-with-an-underline-or-icon and orange-with-
   neither — so the anchor needs no treatment of its own and takes the h2's. */
.acard-title {
  flex: 1; min-width: 0;
  font: 700 var(--fs-lead) var(--sans); letter-spacing: 0;
  color: var(--action);
}
.acard-title a:hover { filter: brightness(1.15); }
/* The head's right-aligned figure — "1063 members", the number bright and the
   word grey, which is the one place a count is allowed to be the loud thing. */
.acard-fig { flex: none; display: flex; align-items: baseline; gap: 6px; }
.acard-fig-n { font: 400 22px var(--sans); letter-spacing: -.01em; color: var(--t-hi); }
/* A figure slot holding a WORD rather than a count — "Quarterfinals", or a
   champion's name. 22px is the size of a number; a phrase at it shouts, and on
   a phone it wraps the head. See the note in _admin_card.html for why the
   macro decides this rather than the call site. */
.acard-fig-n--text { font-size: var(--fs-lead); }
.acard-fig-w { font: 400 var(--fs-body) var(--sans); color: var(--t-court); }

.acard-rows { display: flex; flex-direction: column; gap: var(--gutter-hair); }
/* 2px of card ground above the first row, so the head is separated from it by
   the same hairline that separates the rows from each other. */
.acard-head + .acard-rows { margin-top: var(--gutter-hair); }

.arow {
  display: flex; align-items: center; gap: var(--sp-3);
  min-height: 56px; padding: var(--sp-2) var(--sp-4);
  background: var(--surface-row);
}
.arow-main { flex: 1; min-width: 0; display: flex; align-items: baseline; flex-wrap: wrap; gap: 0 8px; }
/* The count shape: a large figure and a grey word on one line. */
.arow-fig { font: 400 21px var(--sans); letter-spacing: -.01em; color: var(--t-hi); }
.arow-word { font: 400 var(--fs-body) var(--sans); color: var(--t-court); }
/* The thing shape: a name over a sub-line. `flex-basis: 100%` on the sub is
   what makes it a second line inside a row that is otherwise baseline-aligned,
   without a second wrapper. */
.arow-name {
  font: 700 var(--fs-body) var(--sans); letter-spacing: .02em;
  text-transform: uppercase; color: var(--t-hi);
}
.arow-sub { flex: 1 0 100%; font: 400 12px var(--sans); color: var(--t-court); }

/* TERTIARY, AND IT NAMES WHERE IT GOES — white with a chevron, never orange.
   DESIGN.md §2: a link is white with an underline or an icon, or orange with
   neither. Orange is the pill beside it. */
.arow-go {
  flex: none; display: inline-flex; align-items: center; gap: 6px;
  font: 500 var(--fs-body) var(--sans); color: var(--t-hi); white-space: nowrap;
}
.arow-go:hover { filter: brightness(1.12); }
.arow-chev { color: var(--t-court); }

/* THE PILL. A real <a> or <button>, never an inert <span> — the row is a <div>
   so that this can be. Filled for the primary action, outlined white for the
   secondary, and one primary per card: if two rows both want one, the second is
   the outline. */
.apill {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  min-height: 28px; padding: 0 16px;
  font: 600 var(--fs-body) var(--sans); border-radius: var(--r-full);
  white-space: nowrap; cursor: pointer;
  transition: filter 150ms ease, background 150ms ease;
}
.apill--primary { background: var(--action); color: var(--on-action); border: 1px solid var(--action); }
.apill--primary:hover { filter: brightness(1.1); }
.apill--ghost { background: none; color: var(--t-hi); border: 1px solid var(--t-hi); }
.apill--ghost:hover { background: var(--surface-card-head); }
/* The outlined-red third, for Deny on the join queue. `.abtn--danger` in
   shared.css is the same idea on the square button; this is the pill form of
   it, and it is an OUTLINE rather than a fill for the reason --action is: a
   filled red beside a filled orange makes two things shout on a row where the
   orange is the one you usually mean. */
.apill--danger { background: none; color: var(--danger); border: 1px solid var(--danger-line); }
.apill--danger:hover { background: var(--danger-wash); }

/* --- the Players tab ------------------------------------------------------
   The toolbar, the filter row, and the roster's rows inside a card. Scoped to
   `.ashell` like everything above, so the demo's own players screen (which is
   a different page on a different host, see CLAUDE.md §1) does not move. */

/* Wraps on a phone, one row from 880px. The club link and the search box both
   grow; the History pill and Upload do not. */
.ashell .ptools {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--sp-2); margin: 0 0 var(--sp-3);
}
.ashell .clink { position: relative; display: flex; flex: 1 1 240px; min-width: 0; }
.ashell .clink-in {
  flex: 1; min-width: 0; padding-right: 44px;
  font: 400 var(--fs-body) var(--sans); color: var(--t-court-hi);
  text-overflow: ellipsis;
}
.ashell .clink-copy {
  position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; border: 0; border-radius: var(--r-xs);
  background: none; color: var(--t-hi); cursor: pointer;
}
.ashell .clink-copy:hover { background: var(--surface-card-head); }
/* The one piece of feedback a copy button can give. No toast: the control is
   its own confirmation, and a toast for "the clipboard did what you asked" is
   a notification about nothing. */
.ashell .clink-copy.is-done { color: var(--acc); }

/* The way to the PUBLIC sign-up page, beside the club link because they are
   the two things an admin hands out. `flex: 0 0 auto` so it never competes
   with the link box or the search for the row's width — those two grow, this
   states a destination. 44px tall for the same reason every other tap target
   in this toolbar is. */
.ashell .ptools-link {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; min-height: 44px;
  font: 600 var(--fs-body) var(--sans); color: var(--action-inline);
  text-decoration: underline; text-underline-offset: 3px;
}

/* ITS OWN ROW ON A PHONE. Sharing one with Upload left ~64px of visible text
   between a 34px magnifier gutter and the 84px the Search button reserves, so
   the placeholder was clipped mid-word at 375. The link and the button both
   read fine at half a row; this does not. */
.ashell .search { flex: 1 1 100%; }
/* The magnifier sits in the padding the input reserves on its LEFT, the same
   arrangement .search-go has on the right. */
.ashell .search-ico {
  position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
  color: var(--t-court); pointer-events: none;
}
.ashell .search-input { padding-left: 34px; }

/* The filter row: the dropdown on the left, the count on the right, and the
   count drops to its own line before the two collide.

   `align-items: center` rather than `baseline` since the left side became a
   control with a border rather than a label and a row of chips — a baseline
   here would hang the button off the count's text line. */
.ashell .pfilters {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-3); margin: 0 0 var(--sp-3);
}
.ashell .pcount { margin-left: auto; font: 400 var(--fs-body) var(--sans); color: var(--t-court); }
.ashell .pcount b { font-weight: 400; font-size: 17px; color: var(--t-hi); }
.ashell .pcount a { color: var(--action-inline); }

/* THE FILTER DROPDOWN — templates/people_list.html, 2026-08c.

   It replaces the row of one-at-a-time chips. `.menu` and `.menu-pop` do the
   opening and the panel, exactly as the Upload control above it does; what is
   added here is the closed control's look and the ticked options inside.

   A QUIET CONTROL, NOT A CTA. It sits on the same line as nothing else and
   under a toolbar whose one orange thing is Upload; a filter is a way of
   looking, not a thing to press. So it takes the row surface and a hairline,
   the same as the search box beside it — `.abtn`'s default in this shell.

   The M3 greys in the drawing (#CAC4D0, #49454F, #1C1B1F, #938F99) are a Figma
   component's defaults, not this product's palette, and are deliberately not
   reproduced. */
.ashell .pfilter { flex: none; }
.ashell .pf-btn {
  display: inline-flex; align-items: center; gap: 6px;
  max-width: 260px;
  font: 500 var(--fs-body) var(--sans);
  background: var(--surface-row); border: 1px solid var(--card-line);
  color: var(--t-court-hi);
}
.ashell .pf-btn:hover { border-color: var(--t-court-hi); color: var(--t-hi); }
/* THE SUMMARY IS ALSO THE READOUT — with tags ticked it names them, and a club
   with five ladders can tick all five. Truncated in CSS rather than by counting
   characters in the script: the board draws "2026 Ladder A, 2026…", and where
   the ellipsis falls is a question about the rendered width, which only the
   browser knows. `min-width: 0` because a flex child will not shrink below its
   content without it, and the ellipsis would never appear. */
.ashell .pf-btn-l {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ashell .pfilter[open] .pf-btn { border-color: var(--action); color: var(--t-hi); }

.ashell .pf-pop { left: 0; right: auto; min-width: 240px; }
/* The whole row is the label, so the hit area is the row rather than an 18px
   box — the same reasoning `.prow--pick` gives for the import preview. */
.ashell .pf-opt {
  display: flex; align-items: center; gap: 10px; cursor: pointer;
  padding: 8px 11px; border-radius: var(--r-xs);
  font: 500 var(--fs-body) var(--sans); color: var(--t-hi);
}
.ashell .pf-opt:hover { background: var(--surface-2); }
.ashell .pf-box { flex: none; width: 15px; height: 15px; accent-color: var(--action); cursor: pointer; }
.ashell .pf-opt-l { flex: 1; min-width: 0; }
/* What ticking this would ADD, which is a different number from the readout on
   the right. Monospaced so the column of them lines up down the menu. */
.ashell .pf-opt-n { flex: none; font: 400 11px var(--mono); color: var(--t-court); }
.ashell .pf-clear {
  display: block; width: 100%; text-align: left; cursor: pointer;
  margin-top: 4px; padding: 8px 11px; border-radius: var(--r-xs);
  background: none; border: 0; border-top: 1px solid var(--card-line);
  font: 600 var(--fs-body) var(--sans); color: var(--action-inline);
}
.ashell .pf-clear:hover { background: var(--surface-2); }

/* --- the roster, inside the card ---
   THE ROW KEEPS ITS CLASS. `players_search.js` selects `#playerList .prow` and
   reads `data-tags` off it, `test_player_tags.py` matches `class="ptag
   ptag--ok"`, and the 560px block below already drops `.p-tags` under the
   name. The row is restyled where it sits; renaming it would have moved a
   filter, four tests and a breakpoint to buy nothing.

   The card supplies the outer radius through its own `overflow: hidden`, so
   the rows are square and the gaps are the dividers. */
.ashell .acard > .plist { gap: var(--gutter-hair); }
.ashell .acard .prow {
  background: var(--surface-row); border: 0; border-radius: 0;
  padding: var(--sp-3) var(--sp-4);
}
.ashell .acard .prow:hover { background: var(--surface-card-head); }
.ashell .acard .p-name { font-size: var(--fs-lead); font-weight: 600; letter-spacing: 0; }
.ashell .acard .p-meta { color: var(--t-court); }
/* The artwork's amber tag is --amb at 40% with white ink, which IS --chip —
   the pair the contrast test composites and measures. --amb-container (14%) was
   too faint to read as a pill on the new row surface. */
.ashell .acard .ptag--event { background: var(--chip); color: var(--on-chip); }
.ashell .acard .hl-empty { padding: var(--sp-4); background: var(--surface-row); }

/* ---- season header band ---- */
.season-band {
  display: flex; flex-direction: column; align-items: flex-start; justify-content: space-between; gap: 18px;
  background: radial-gradient(120% 120% at 0% 0%, var(--surface) 0%, var(--screen) 72%);
  border: 1px solid var(--divider); border-radius: var(--r-l); padding: 20px 22px;
}
.season-name { font: 800 30px var(--cond); letter-spacing: .02em; text-transform: uppercase; color: var(--t-hi); }
.season-dates { font: 500 12px var(--mono); color: var(--t-muted-2); margin-top: 4px; }
.season-stats { font: 600 13px var(--sans); color: var(--t-body); margin-top: 10px; display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
.health-dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; }
.health-dot--good { background: var(--acc); }
.health-dot--warn { background: var(--amb); }
.health-dot--bad { background: var(--danger); }
.health-dot--neutral { background: var(--t-dim); }
.season-actions { display: flex; gap: 10px; flex: none; }

/* .abtn and the .abtn--* modifiers moved to shared.css — a button in the score
   dialog is the same button on either shell. */

/* ---- consolidated action bar ---- */
.actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 14px 0 4px; }
.act-btn {
  font: 700 12px var(--sans); letter-spacing: .02em; cursor: pointer;
  color: var(--t-hi); background: var(--surface); border: 1px solid var(--border-2);
  border-radius: var(--r-s); padding: 9px 15px; transition: border-color 150ms ease, background 150ms ease;
}
.act-btn:hover { border-color: var(--t-dim); background: var(--surface-2); }
.act-more { position: relative; }
.act-more-btn { color: var(--t-muted); }
.act-menu {
  position: absolute; top: calc(100% + 6px); right: 0; z-index: 30;
  min-width: 180px; background: var(--surface); border: 1px solid var(--border-2);
  border-radius: var(--r-m); padding: 6px; box-shadow: 0 14px 30px -12px rgba(0,0,0,.7);
  display: flex; flex-direction: column;
}
.act-menu[hidden] { display: none; }
.act-menu-item { font: 600 12.5px var(--sans); color: var(--t-body); padding: 9px 11px; border-radius: var(--r-s); }
.act-menu-item:hover { background: var(--surface-2); color: var(--t-hi); }

/* ---- body grid ---- */
.admin-grid { display: grid; grid-template-columns: 1fr; gap: 18px; margin-top: 18px; align-items: start; }

.panel { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-l); padding: 18px 18px 20px; margin-bottom: 18px; }
.panel--alert { border-color: var(--amb-line); }
/* .panel-title moved to shared.css — the score dialog's heading uses it. */

/* ---- needs attention ---- */
.needs { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 11px; }
.need {
  position: relative; cursor: pointer;
  background: var(--surface-2); border: 1px solid var(--border); border-left: 3px solid var(--t-dim-2);
  border-radius: var(--r-m); padding: 14px 14px 13px;
  display: flex; flex-direction: column; gap: 3px;
  transition: border-color 150ms ease, transform 120ms ease;
}
.need:hover { transform: translateY(-1px); border-color: var(--t-dim); }
.need--dispute, .need--overdue { border-left-color: var(--amb); }
.need--dispute:hover, .need--overdue:hover { border-color: var(--amb-line); }
.need-count { font: 800 30px/1 var(--cond); color: var(--t-hi); }
.need--dispute .need-count, .need--overdue .need-count { color: var(--amb); }
.need-label { font: 600 11.5px var(--sans); color: var(--t-muted); line-height: 1.3; }
.need-btn {
  align-self: flex-start; margin-top: 10px; cursor: pointer;
  font: 700 10px var(--sans); letter-spacing: .04em; text-transform: uppercase;
  padding: 7px 13px; border-radius: var(--r-s); background: var(--acc); color: var(--on-acc);
  transition: filter 150ms ease;
}
.need-btn:hover { filter: brightness(1.08); }
.need--dispute .need-btn, .need--overdue .need-btn { background: var(--amb); color: var(--on-amb); }
.allclear { font: 600 14px var(--sans); color: var(--acc); padding: 8px 0; }

/* ---- ladder health ---- */
.health { display: flex; align-items: center; gap: 22px; }
.health-ring {
  --pct: 0;
  flex: none; width: 116px; height: 116px; border-radius: 50%;
  background: conic-gradient(var(--acc) calc(var(--pct) * 1%), var(--border) 0);
  display: flex; align-items: center; justify-content: center;
}
.health-ring-inner {
  width: 88px; height: 88px; border-radius: 50%; background: var(--surface);
  display: flex; align-items: center; justify-content: center;
}
.health-ring-num { font: 800 34px/1 var(--cond); color: var(--t-hi); }
.health-ring-pct { font-size: 16px; color: var(--t-dim); }
.health-meta { flex: 1; min-width: 0; }
.health-band { font: 800 14px var(--cond); letter-spacing: .06em; text-transform: uppercase; margin-bottom: 10px; }
.health-band--good { color: var(--acc); }
.health-band--warn { color: var(--amb); }
.health-band--bad { color: var(--danger); }
.health-band--neutral { color: var(--t-muted-2); }
.health-metrics { display: flex; flex-direction: column; }
.hm { display: flex; align-items: center; gap: 9px; padding: 7px 0; border-top: 1px solid var(--divider); }
.hm:first-child { border-top: none; }
.hm-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; }
.hm-dot--good { background: var(--acc); }
.hm-dot--warn { background: var(--amb); }
.hm-dot--bad { background: var(--danger); }
.hm-dot--neutral { background: var(--t-dim); }
.hm-label { flex: 1; font: 500 12.5px var(--sans); color: var(--t-body); }
.hm-val { font: 700 12.5px var(--mono); color: var(--t-hi); }

/* ---- insights ---- */
.ins-group { margin-top: 14px; }
.ins-group:first-of-type { margin-top: 0; }
.ins-group-title { font: 700 10px var(--mono); letter-spacing: .16em; text-transform: uppercase;
  color: var(--t-dim); margin-bottom: 9px; padding-bottom: 7px; border-bottom: 1px solid var(--divider); }
.insights { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 11px; }
.ins { background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--r-m); padding: 13px 14px; }
.ins-label { font: 700 9px var(--mono); letter-spacing: .12em; text-transform: uppercase; color: var(--t-muted); }
.ins-unit { color: var(--t-dim); font-weight: 500; letter-spacing: .06em; }
.ins-big { font: 800 26px/1 var(--cond); color: var(--t-hi); margin-top: 8px; display: flex; align-items: baseline; gap: 8px; }
.ins-delta { font: 700 12px var(--mono); }
.ins-delta--up { color: var(--acc); }
.ins-delta--down { color: var(--neg); }
.ins-delta--flat { color: var(--t-dim); }
.ins-sub { font: 500 11px var(--sans); color: var(--t-muted-2); margin-top: 6px; line-height: 1.4; }
.ins-list { list-style: none; margin-top: 8px; display: flex; flex-direction: column; }
.ins-list li { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 5px 0; border-top: 1px solid var(--divider); font: 500 12.5px var(--sans); color: var(--t-body); }
.ins-list li:first-child { border-top: none; }
.ins-list b { font: 700 12.5px var(--mono); color: var(--acc); }
.ins-list b.dim { color: var(--t-muted); }

/* ---- highlights ---- */
.panel--feed { padding-bottom: 14px; }
.highlights { display: flex; flex-direction: column; }
.hl { display: flex; align-items: flex-start; gap: 11px; padding: 11px 0; border-top: 1px solid var(--divider); }
.hl:first-child { border-top: none; }
.hl-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; margin-top: 5px; background: var(--t-dim); }
.hl--urgent .hl-dot { background: var(--danger); }
.hl--warn .hl-dot { background: var(--amb); }
.hl--good .hl-dot { background: var(--acc); }
.hl-text { font: 600 13px/1.4 var(--sans); color: var(--t-body); }
.hl--urgent .hl-text, .hl--warn .hl-text { color: var(--t-hi); }
.hl-empty { font: 500 13px/1.5 var(--sans); color: var(--t-dim); padding: 11px 0; }

/* shareable invite link banner */
.invite-banner { background: linear-gradient(180deg, var(--acc-wash) 0%, var(--surface) 100%);
  border: 1px solid var(--acc-line); border-radius: var(--r-m); padding: 16px 18px; margin-bottom: 18px; }
.invite-banner-eyebrow { font: 700 10px var(--mono); letter-spacing: .16em; color: var(--acc); }
.invite-banner-sub { display: block; font: 500 13px/1.5 var(--sans); color: var(--t-muted); margin-top: 6px; }
.invite-banner-sub b { color: var(--t-hi); }
.invite-banner-row { display: flex; gap: 10px; margin-top: 13px; }
.invite-link { flex: 1; min-width: 0; background: var(--surface-2); border: 1px solid var(--border-2); border-radius: var(--r-s);
  color: var(--t-body); font: 500 13px var(--mono); padding: 11px 12px; outline: none; text-overflow: ellipsis; }
.invite-link:focus { border-color: var(--acc); }
.invite-copy { flex: none; background: var(--acc); color: var(--on-acc); border: none; border-radius: var(--r-s); padding: 0 18px;
  font: 800 12px var(--sans); letter-spacing: .04em; text-transform: uppercase; cursor: pointer; transition: filter 120ms ease, background 120ms ease; }
.invite-copy:hover { filter: brightness(1.05); }
.invite-copy.copied { background: var(--acc); }
/* Demo ladder's invite link sits at the bottom of the Command Center */
.invite-banner--bottom { margin-bottom: 0; margin-top: 20px; }
.invite-open { flex: none; display: inline-flex; align-items: center; background: var(--surface-2);
  border: 1px solid var(--border-2); border-radius: var(--r-s); padding: 0 16px; color: var(--t-hi);
  font: 800 12px var(--sans); letter-spacing: .04em; text-transform: uppercase; text-decoration: none;
  transition: border-color 120ms ease, color 120ms ease; }
.invite-open:hover { border-color: var(--acc); color: var(--acc); }

/* ---- stub pages ---- */
.stub { max-width: 560px; margin: 40px auto; text-align: center; background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-l); padding: 36px 28px; }
.stub-eyebrow { font: 700 10px var(--mono); letter-spacing: .18em; color: var(--t-muted); }
.stub-title { font: 800 30px var(--cond); letter-spacing: .03em; text-transform: uppercase; color: var(--t-hi); margin-top: 8px; }
.stub-filter { font: 600 12px var(--mono); color: var(--acc); margin-top: 8px; }
.stub-note { font: 400 13.5px/1.6 var(--sans); color: var(--t-muted); margin: 14px 0 20px; }

/* ---- workflow screens (pitch demo: registrations / challenges / disputes) ---- */
.wf { max-width: 780px; margin: 6px auto 0; }
.wf-head { margin-bottom: 16px; }
.wf-eyebrow { font: 700 10px var(--mono); letter-spacing: .18em; color: var(--t-muted); }
.wf-title { font: 800 30px var(--cond); letter-spacing: .03em; text-transform: uppercase; color: var(--t-hi); margin-top: 6px; }
.wf-sub { font: 600 13px var(--sans); color: var(--t-muted); margin-top: 6px; }
.wf-sub b { color: var(--acc); font: 800 13px var(--mono); }

.wf-group-title { font: 800 12px var(--cond); letter-spacing: .1em; text-transform: uppercase; color: var(--t-muted-2); margin: 22px 0 10px; }

.wf-list { display: flex; flex-direction: column; gap: 10px; }
.wf-row {
  display: flex; align-items: center; flex-wrap: wrap; gap: 14px;
  background: var(--surface); border: 1px solid var(--border); border-left: 3px solid var(--t-dim-2);
  border-radius: var(--r-m); padding: 14px 16px;
  transition: opacity 200ms ease, border-color 150ms ease;
}
.wf-row--warn { border-left-color: var(--amb); }
.wf-row--dispute { border-left-color: var(--amb); align-items: flex-start; }
.wf-row--static { border-left-color: var(--border-2); }
.wf-main { flex: 1; min-width: 0; }
.wf-name { font: 700 15px var(--sans); color: var(--t-hi); }
.wf-vs { font: 500 12px var(--sans); color: var(--t-dim); }
.wf-meta { font: 500 12px var(--sans); color: var(--t-muted-2); margin-top: 4px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

.wf-actions { display: flex; gap: 8px; flex: none; }
.wf-actions--col { flex-direction: column; align-items: stretch; }
.wf-actions[hidden] { display: none; }

.wbtn {
  cursor: pointer; white-space: nowrap;
  font: 700 11px var(--sans); letter-spacing: .03em;
  padding: 9px 14px; border-radius: var(--r-s); border: 1px solid transparent;
  transition: filter 150ms ease, background 150ms ease, border-color 150ms ease;
}
.wbtn--ok { background: var(--acc); color: var(--on-acc); }
.wbtn--ok:hover { filter: brightness(1.08); }
.wbtn--warn { background: var(--amb); color: var(--on-amb); }
.wbtn--warn:hover { filter: brightness(1.06); }
.wbtn--ghost { background: var(--surface-2); color: var(--t-muted); border-color: var(--border-2); }
.wbtn--ghost:hover { color: var(--t-hi); border-color: var(--t-dim); }

.wf-done[hidden] { display: none; }
.done-pill {
  font: 700 11px var(--sans); letter-spacing: .03em; white-space: nowrap;
  padding: 7px 12px; border-radius: var(--r-full);
  background: var(--acc-container); color: var(--acc);
  border: 1px solid var(--acc-line);
}
.wf-row.is-done { opacity: .55; }
.wf-row.neg .done-pill { background: var(--danger-container); color: var(--danger); border-color: var(--danger-line); }
.wf-row.warn .done-pill { background: var(--amb-container); color: var(--amb); border-color: var(--amb-line); }

/* status pills */
.pill { font: 700 9px var(--mono); letter-spacing: .1em; text-transform: uppercase; padding: 3px 8px; border-radius: var(--r-full); flex: none; }
.pill--overdue { background: var(--danger-container); color: var(--danger); }
.pill--pending { background: var(--amb-container); color: var(--amb); }
.pill--scheduled { background: var(--acc-container); color: var(--acc); }
/* Import-preview verdicts: what this row will do to the database. Same
   container roles as the pills above, no new hex. */
.pill--new { background: var(--acc-container); color: var(--acc); }
.pill--upd { background: var(--amb-container); color: var(--amb); }
.pill--arch { background: var(--neutral-container); color: var(--t-muted); }
/* Matched, and nothing to write. Deliberately the quietest of the four — it is
   the verdict that means "this row will do nothing", so it must not compete
   with the ones that will. Dimmer than --arch, which at least signals a
   change. */
.pill--dupe { background: var(--neutral-container); color: var(--t-dim); }
/* Join-request evidence: how much backs linking an account to this record.
   Same container roles again, no new hex. --warn is the "name only" case and
   is meant to be read as a caution rather than a label, because a name is the
   one thing about a player that is public. */
.pill--ok { background: var(--acc-container); color: var(--acc); }
.pill--warn { background: var(--amb-container); color: var(--amb); }

/* dispute claims */
.dispute-claims { display: flex; gap: 26px; margin-top: 11px; flex-wrap: wrap; }
.claim { display: flex; flex-direction: column; gap: 3px; }
.claim-who { font: 600 10px var(--mono); letter-spacing: .06em; text-transform: uppercase; color: var(--t-muted-2); }
.claim-score { font: 700 16px var(--mono); color: var(--t-hi); }

/* reset control */
.demo-reset {
  cursor: pointer; font: 700 11px var(--mono); letter-spacing: .06em; text-transform: uppercase;
  color: var(--t-muted); background: var(--surface); border: 1px solid var(--border-2);
  border-radius: var(--r-full); padding: 5px 12px; transition: color 150ms ease, border-color 150ms ease;
}
.demo-reset:hover { color: var(--t-hi); border-color: var(--t-dim); }

/* toast */
.demo-toast {
  position: fixed; left: 50%; bottom: 28px; z-index: 60;
  transform: translateX(-50%) translateY(12px);
  background: var(--acc); color: var(--on-acc); font: 700 13px var(--sans);
  padding: 11px 18px; border-radius: var(--r-s); box-shadow: 0 14px 30px -10px rgba(0,0,0,.6);
  opacity: 0; pointer-events: none; transition: opacity 200ms ease, transform 200ms ease;
}
.demo-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }

/* ---- send announcement (compose) ---- */
.compose { margin-top: 4px; }
.chips { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; margin-bottom: 9px; }
.chips-label { font: 700 10px var(--mono); letter-spacing: .08em; text-transform: uppercase; color: var(--t-muted-2); }
.chip {
  cursor: pointer; font: 600 11.5px var(--sans); color: var(--t-body);
  background: var(--surface-2); border: 1px solid var(--border-2); border-radius: var(--r-full);
  padding: 6px 12px; transition: color 140ms ease, border-color 140ms ease;
}
.chip:hover { color: var(--acc); border-color: var(--acc-line); }
.char-count { font: 500 11px var(--mono); color: var(--t-muted-2); margin-top: 7px; text-align: right; }
.sent-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
.sent-actions .wbtn { text-decoration: none; padding: 11px 18px; }

/* ---- player info (roster management) ---- */
.plist { display: flex; flex-direction: column; gap: 6px; }
.prow {
  display: flex; align-items: center; gap: 14px;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-s);
  padding: 10px 14px; transition: border-color 150ms ease;
}
.prow:hover { border-color: var(--border-2); }
/* A .prow nested in a .panel would be --surface on --surface. Recede it to
   --surface-2, exactly as .need and .ins tiles do inside a panel. */
.panel .prow { background: var(--surface-2); }
.p-rank { font: 800 15px var(--mono); color: var(--acc); width: 46px; flex: none; }
.p-id { flex: 1; min-width: 0; }
.p-name { font: 700 14px var(--sans); color: var(--t-hi); }
/* Emails, member ids and phone numbers land here and have no spaces to break
   at, so a long one would otherwise widen the row past the viewport. */
.p-meta { font: 500 11.5px var(--sans); color: var(--t-muted); margin-top: 2px; overflow-wrap: anywhere; }
.p-controls { display: flex; align-items: center; gap: 6px; flex: none; }

/* Tags under a name on the Players list: who this person is, and what they are
   currently in. Built by services/people.tags_for.

   TWO COLOURS, NOT ONE PER KIND OF THING. Cyan is --acc, which in this system
   means IDENTITY (tokens.css), so "Verified" — an account holds this record —
   is exactly what it is for. Amber is the running-event colour the ladder and
   event cards already use for something live. A ladder and a tournament share
   the amber because the colour says what SORT of fact a tag is; the label says
   which one, and "2026 A Ladder" needs no help telling itself from "Club Cup".

   Wraps rather than scrolls: a player in three events on a phone gets two
   lines, and a row that scrolls sideways hides the tag on the end. No new hex —
   the same --X-container roles the .pill set uses. */
/* BESIDE THE NAME, NOT UNDER IT. Under it they read as another line of the
   same grey contact block; on the right they line up down the list, so "who is
   verified" is one vertical scan. Capped at half the row so a player in four
   events cannot squeeze the name to nothing — .p-id is flex:1 and would give
   up its width first. */
.p-tags {
  display: flex; flex-wrap: wrap; gap: 5px;
  justify-content: flex-end; flex: none; max-width: 52%;
}
.ptag {
  font: 700 9.5px var(--mono); letter-spacing: .06em; text-transform: uppercase;
  padding: 3px 7px; border-radius: var(--r-full); white-space: nowrap;
}
.ptag--ok { background: var(--acc-container); color: var(--acc); }
.ptag--event { background: var(--amb-container); color: var(--amb); }
/* FILLED, where the others are tinted. Administering the club is a different
   ORDER of fact from being verified or being on a ladder, and the palette has
   no third hue to spend on it — --action means "the thing to press" and a tag
   is not pressable. So it separates by WEIGHT instead: same cyan, full
   strength. No new hex. */
.ptag--admin { background: var(--acc); color: var(--on-acc); }

/* Belt-and-braces, and worth knowing WHY it is only that: line 13 of this file
   is a blanket `[hidden] { display: none !important }`, so the tag filter would
   work without this rule. It is `player.css` that has no such line, which is
   where `.lrow { display: flex }` really did beat `hidden` and leave a filter
   reporting itself applied over rows that never moved (CLAUDE.md). Stated
   explicitly because the reverse assumption — that this sheet is at risk too —
   is the easy one to make. */
.prow[hidden] { display: none; }

/* ---- the search box, with its button inside it ---- */
.search { position: relative; display: flex; flex: 1; min-width: 240px; }
/* Room for the button, so a long query never slides under it. */
.search-input { flex: 1; padding-right: 92px; }
.search-go {
  position: absolute; top: 50%; right: 5px; transform: translateY(-50%);
  border: 0; border-radius: var(--r-xs); cursor: pointer;
  padding: 7px 13px; background: var(--action); color: var(--on-action);
  font: 700 11px var(--sans); letter-spacing: .04em;
}
.search-go:hover:not(:disabled) { filter: brightness(1.06); }
/* Inert until something is typed — and it LOOKS inert, because a control that
   only stops working when pressed is worse than one that says so. */
.search-go:disabled {
  background: var(--neutral-container); color: var(--t-dim);
  cursor: default;
}

.suggest {
  position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 30;
  margin: 0; padding: 4px; list-style: none;
  background: var(--surface); border: 1px solid var(--border-2);
  border-radius: var(--r-s); box-shadow: 0 10px 28px rgb(0 0 0 / .38);
  max-height: 320px; overflow-y: auto;
}
.suggest[hidden] { display: none; }
.suggest-item {
  padding: 9px 11px; border-radius: var(--r-xs); cursor: pointer;
  font: 600 13px var(--sans); color: var(--t-hi);
}
.suggest-item:hover, .suggest-item.is-active { background: var(--surface-2); }

/* ---- one CTA, two destinations ---- */
.menu { position: relative; }
.menu-btn { list-style: none; cursor: pointer; user-select: none; }
.menu-btn::-webkit-details-marker { display: none; }
.menu-btn::after { content: ' ▾'; font-size: 10px; }
.menu-pop {
  position: absolute; top: calc(100% + 6px); right: 0; z-index: 30;
  min-width: 232px; padding: 5px;
  background: var(--surface); border: 1px solid var(--border-2);
  border-radius: var(--r-s); box-shadow: 0 10px 28px rgb(0 0 0 / .38);
}
/* Links AND buttons: the publish menu's items are form submits, because
   publishing is a POST and a menu of <a>s could not carry one. The button reset
   is what makes the two indistinguishable in the list. */
.menu-pop a,
.menu-pop button {
  display: block; width: 100%; text-align: left; cursor: pointer;
  padding: 9px 11px; border-radius: var(--r-xs);
  background: none; border: 0;
  text-decoration: none; color: var(--t-hi);
  font: 700 13px var(--sans);
}
.menu-pop form { margin: 0; }
.menu-pop a:hover,
.menu-pop button:hover { background: var(--surface-2); }
/* What each one actually imports. "Players" and "History" are short enough to
   be guessed wrongly, and this is the screen where guessing wrong means
   uploading a season of match logs into the roster importer. Also covers the
   publish menu's own sub-labels ("Only the players in the field", "· current")
   under each button — a POST can't be an <a>, so it needs the same treatment. */
.menu-pop a span,
.menu-pop button span {
  display: block; margin-top: 2px;
  font: 500 11px var(--sans); color: var(--t-muted);
}
/* The publish menu's own "Live now · ..." header line — not a control, so it
   gets none of .menu-pop button's interactive styling. */
.menu-note {
  padding: 9px 11px 8px; margin-bottom: 4px;
  border-bottom: 1px solid var(--divider);
  font: 600 11px var(--sans); color: var(--t-muted);
}

/* ---- filtering by tag ----
   `.tagbar` / `.tagchip` are GONE with the chips they painted: 2026-08c makes
   the roster filter a multi-select dropdown, and this sheet was their only
   declaration and people_list.html their only caller. The rules that replaced
   them are `.pf-*`, up in the Players-tab section under the `.ashell` scope. */

/* A <button> that has to read as a link — the "show everyone" way back out of
   a filter that emptied the list. */
.linkish {
  background: none; border: 0; padding: 0; cursor: pointer;
  color: var(--action-inline); font: inherit; text-decoration: underline;
}

/* ---- people waiting to join ----
   A faded orange bar, above the list they are asking to be on. Orange is this
   system's "the thing you are meant to act on"; at --action-wash (10%) it says
   so without competing with the Upload button, which is the same hue at full
   strength a few pixels above. */
.jr-banner {
  display: flex; align-items: center; justify-content: space-between;
  gap: 14px; flex-wrap: wrap; margin: 0 0 12px;
  padding: 11px 14px; border-radius: var(--r-s);
  background: var(--action-wash); border: 1px solid var(--action-line);
  font: 500 13px var(--sans); color: var(--t-hi);
}
.jr-link {
  color: var(--action-inline); font-weight: 700; white-space: nowrap;
}

/* ⚠ NO `display` ON THE DIALOG ITSELF. The UA hides a closed <dialog> with
   `dialog:not([open]) { display: none }`, and any class rule setting `display`
   outranks it — which is how `.profile-dlg { display: flex }` painted an empty
   shell on every player page and was found on a phone two merges later
   (CLAUDE.md). `.report-dlg` was never affected because it sets none, and this
   follows it: the layout lives on the children. */
.jr-dlg {
  border: 1px solid var(--border-2); border-radius: var(--r-l);
  background: var(--surface); color: var(--t-body);
  padding: 18px; width: min(760px, calc(100vw - 32px));
  max-height: calc(100vh - 64px);
  /* ⚠ RE-STATING THE UA'S OWN RULE, because line 10 of this file removed it.
     `* { margin: 0 }` beats `dialog { margin: auto }`, so a modal that the
     browser would have centred was pinned to the TOP-LEFT corner at every
     width where it did not fill the screen. Invisible below ~790px, which is
     the only width this was ever walked at, and invisible to the suite, which
     renders no geometry. `.pick-dlg` and `.report-dlg` have the same shape and
     are NOT fixed here: the first is the draw editor's and the second is in
     shared.css and renders on the player shell too, so each is its own change
     with its own walk. */
  margin: auto;
}
.jr-dlg::backdrop { background: rgba(0, 0, 0, .6); }
.jr-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin-bottom: 4px;
}
/* Sentence case, as the board draws it, and SCOPED TO THIS DIALOG. The shared
   `.panel-title` is 17px Saira Condensed uppercase and is worn by every panel
   and dialog in the app; retuning it here would restyle all of them for one
   drawing. The queue's own head follows the same rule the admin card's title
   now does — a title in the body face — because it names a thing rather than
   labelling a section. */
.jr-dlg .panel-title {
  font: 700 var(--fs-title) var(--sans); letter-spacing: 0;
  text-transform: none;
}
/* The cards scroll, the header does not — a queue of fifteen must not push
   its own close button off the screen. */
.jr-body { overflow-y: auto; max-height: calc(100vh - 150px); }

/* ---- one request, as one row (2026-08c) ----
   templates/_join_request.html. The card is a <section> when there is nothing
   on file and a <details> when there is; `.panel jr` is on both, and the row
   below is the same either way. */
.jr { padding: 0; margin-top: 10px; overflow: hidden; }
.jr-row {
  display: flex; align-items: center; flex-wrap: wrap; gap: 10px 14px;
  padding: 12px 14px; background: var(--surface-row);
}
/* THE MARKER IS THE PILL, not a triangle beside one. `list-style: none` plus
   the WebKit pseudo-element is the same pair `.menu-btn` needs; without both,
   Safari draws its own disclosure triangle in the middle of the row. */
details.jr > summary.jr-row { cursor: pointer; list-style: none; }
details.jr > summary.jr-row::-webkit-details-marker { display: none; }
details.jr > summary.jr-row:hover { background: var(--surface-card-head); }
details.jr[open] > summary.jr-row { background: var(--surface-card-head); }

.jr-who { flex: 1 1 240px; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.jr-name { font: 500 var(--fs-lead) var(--sans); color: var(--t-hi); }
/* SMALL CAPS, AS THE VERDICT. Grey when the roster has somebody who might be
   them, cyan when it plainly does not — cyan is this product's "good", and a
   first-time player is the clean case with nothing to weigh, not a warning. */
.jr-status {
  font: 700 10.5px var(--sans); letter-spacing: .08em;
  text-transform: uppercase; color: var(--t-court);
}
.jr-status--new { color: var(--acc); }
/* The evidence disagreeing with itself. NEVER behind the disclosure — see the
   note in the template. */
.jr-conflict {
  margin-top: 4px; padding: 6px 9px; border-radius: var(--r-xs);
  background: var(--amb-container); border: 1px solid var(--amb-line);
  font: 500 12px var(--sans); color: var(--amb);
}
/* ⚠ `flex: none` HOLDS ONLY WHILE THE CONTENT IS TWO SHORT PILLS, which it was
   until the notify checkbox joined the approve form. A non-shrinking box is
   sized by its content, so a 200px label made this 428px wide inside a 359px
   card and pushed the primary button off the right edge — no horizontal
   scrollbar, because the overflow is inside a rounded card. Measured on a
   375px viewport, not guessed.

   It still refuses to shrink where there is room. Below 560px it takes its own
   line under the name instead, which is the shape the toolbar on the Players
   tab already uses for the same reason. */
.jr-acts { flex: none; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
@media (max-width: 560px) {
  .jr-acts { flex: 1 0 100%; justify-content: flex-start; }
}
.jr-acts form { margin: 0; }

/* The panel behind "Link to existing profile". --card-ground rather than the
   row's surface, so an open card reads as one object with a lid rather than as
   two stacked rows. */
.jr-more { padding: 14px; background: var(--card-ground); }
.jr-more .page-sub { margin-bottom: 10px; }
.jr-more .plist { gap: var(--gutter-hair); }
.jr-asked { margin: 10px 0 0; }
.jr-decide {
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
  margin-top: 12px;
}
.jr-decide form { display: flex; align-items: center; gap: 8px; margin: 0; }

/* The opt-in notice, inside each approve form. Quiet — it is a choice ABOUT
   the act, not the act — and it wraps to its own line on a phone rather than
   squeezing the button, because both approve forms are already flex rows with
   a select or a pill in them. 44px so the label is a real tap target: a
   checkbox nobody can hit on a phone is a control that only works at a desk. */
.jr-notify {
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 44px; flex: 0 1 auto;
  font: 400 var(--fs-meta) var(--sans); color: var(--t-court);
  cursor: pointer;
}
.jr-notify input { width: 16px; height: 16px; accent-color: var(--action); }

/* A checkbox field on an admin form — the share page's "Show match times to
   guests". A real rule and not just a class name in the markup: a label that
   declares nothing renders the box and its words as bare inline text, which is
   the `.sec-card` trap CLAUDE.md §2 records. Same box and accent as the join
   queue's notify control, so the two opt-ins on the admin side read alike. */
.field-check {
  display: flex; align-items: center; gap: 8px;
  font: 500 var(--fs-body) var(--sans); color: var(--t-hi);
  cursor: pointer;
}
.field-check input { width: 16px; height: 16px; accent-color: var(--action); }

/* The two outcomes on the signup confirmation, as a list rather than a
   paragraph — they are alternatives, and a reader should be able to find the
   one that applies to them without reading both.

   ⚠ IT CARRIES `.page-sub` FOR ITS COLOUR AND SIZE, so this rule only has to
   undo the browser's list indent and set the rhythm. Declaring nothing at all
   would have made `confirm-next` the kind of class this repo has been bitten
   by twice — a name in the markup that paints nothing (see `.sec-card` in
   CLAUDE.md §2). */
.confirm-next {
  margin: 10px 0 0;
  padding-left: 1.1em;
  display: flex; flex-direction: column; gap: 6px;
}
/* The picker takes the room; Deny beside it does not. TWO forms here, not the
   three this once laid out — linking and creating are one question and share
   one picker, so what wraps to a second line is only ever Deny. Which is a
   different kind of thing and reads as one. See _join_request.html. */
.jr-decide form:first-child { flex: 1 1 320px; }
.jr-decide select { flex: 1; min-width: 0; }
/* The group labels inside the picker. Left to the UA they render near-black on
   the dark field and the two halves of the choice stop being separable. */
.jr-decide optgroup { color: var(--t-hi); background: var(--surface-card-head); }
.jr-decide option { color: var(--t-hi); background: var(--surface-card-head); }

/* Import preview: a row you can untick. The whole row is the label, so the hit
   area is the row rather than an 18px box — the same reasoning as .prow being
   the touch target elsewhere. An unticked row dims rather than disappearing, so
   the admin can see what they are excluding and change their mind. */
.prow--pick { cursor: pointer; }
.p-pick { flex: none; width: 18px; height: 18px; accent-color: var(--acc); cursor: pointer; }
.prow--pick:has(.p-pick:not(:checked)) { opacity: .45; }
.prow--pick:has(.p-pick:not(:checked)) .p-name { text-decoration: line-through; }
.icon-btn {
  cursor: pointer; width: 30px; height: 30px; border-radius: var(--r-xs);
  background: var(--surface-2); border: 1px solid var(--border-2); color: var(--t-muted);
  font-size: 11px; line-height: 1; transition: color 140ms ease, border-color 140ms ease;
}
.icon-btn:hover { color: var(--t-hi); border-color: var(--t-dim); }
.p-remove { padding: 7px 12px; }
.p-remove:hover { color: var(--danger); border-color: var(--danger-line); }

/* ---- new-season wizard ---- */
.wiz { max-width: 640px; margin: 6px auto 0; }
.wiz-steps { display: flex; align-items: center; gap: 10px; margin: 20px 0 22px; }
.wiz-step { display: flex; align-items: center; gap: 9px; flex: none; }
.wiz-num {
  width: 26px; height: 26px; border-radius: 50%; flex: none;
  display: flex; align-items: center; justify-content: center;
  font: 800 12px var(--mono); color: var(--t-muted);
  background: var(--surface); border: 1px solid var(--border-2);
}
.wiz-slabel { display: none; font: 700 11px var(--sans); letter-spacing: .03em; color: var(--t-muted); white-space: nowrap; }
.wiz-step.is-active .wiz-num { background: var(--acc); color: var(--on-acc); border-color: var(--acc); }
.wiz-step.is-active .wiz-slabel { color: var(--t-hi); }
.wiz-step.is-done .wiz-num { background: var(--acc-container); color: var(--acc); border-color: var(--acc-line); }
.wiz-bar { flex: 1; height: 1px; background: var(--border-2); }

.wiz-panel {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-l); padding: 22px;
}
.wiz-panel[hidden] { display: none; }

/* The form primitives — .field, .field-row, .field-label, .field-hint,
   .field-input, .field-textarea, .field-err — moved to shared.css. A form field
   looks the same wherever it is and had no business living in an admin
   stylesheet; the score dialog is shared by an admin page and a player page.
   The .field-as-a-row trap (CLAUDE.md, HANDOFF §7a) moves with them: .field is
   still a COLUMN. */

.invite-bar { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 10px; flex-wrap: wrap; }
.invite-count { font: 600 12.5px var(--sans); color: var(--t-muted); }
.invite-count b { color: var(--acc); font: 800 13px var(--mono); }

.wiz-nav { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 20px; }

/* skill range */
.range-row { display: flex; align-items: center; gap: 12px; }
.range-row .field-input { flex: 1; }
.range-dash { font: 600 12px var(--sans); color: var(--t-muted); flex: none; }

/* segmented control */
.seg { display: inline-flex; gap: 3px; padding: 3px; background: var(--surface-2); border: 1px solid var(--border-2); border-radius: var(--r-s); flex-wrap: wrap; }
.seg-opt {
  font: 700 12px var(--sans); letter-spacing: .01em; color: var(--t-muted);
  background: transparent; border: none; cursor: pointer;
  padding: 8px 14px; border-radius: var(--r-xs); transition: background 140ms ease, color 140ms ease;
}
.seg-opt:not(.is-on):hover { color: var(--t-hi); }
.seg-opt.is-on { background: var(--acc); color: var(--on-acc); }

/* source panels */
.src-panel[hidden] { display: none; }
.invite-bar--total { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--divider); }
.invite-breakdown { font: 500 11.5px var(--sans); color: var(--t-muted-2); margin-top: 6px; line-height: 1.5; }
.invite-breakdown[hidden] { display: none; }
.invite-breakdown b { color: var(--t-hi); font-weight: 700; }
.invite-breakdown .dup { color: var(--acc); font-weight: 700; }
.invite-preview { margin-top: 10px; max-height: 190px; overflow-y: auto; display: flex; flex-wrap: wrap;
  gap: 6px; padding: 11px; border: 1px solid var(--border); border-radius: var(--r-s); background: var(--surface-2); }
.invite-preview[hidden] { display: none; }
.inv-name { display: inline-flex; align-items: center; gap: 6px; font: 600 11.5px var(--sans);
  color: var(--t-body); background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-full); padding: 4px 6px 4px 10px; white-space: nowrap; }
.inv-x { border: none; background: none; color: var(--t-dim); cursor: pointer; font: 700 13px var(--sans);
  line-height: 1; padding: 0 2px; border-radius: 50%; }
.inv-x:hover { color: var(--danger); }
.invite-reset { margin-top: 8px; font: 500 11.5px var(--sans); color: var(--t-muted-2); }
.invite-reset[hidden] { display: none; }
.invite-reset a { color: var(--acc); cursor: pointer; text-decoration: none; font-weight: 700; }
.invite-reset a:hover { text-decoration: underline; }
/* the "remove whole group" affordance on the group-selected confirmation */
.src-clear { border: none; background: none; color: var(--t-dim); cursor: pointer;
  font: 700 12px var(--sans); margin-left: 8px; }
.src-clear:hover { color: var(--danger); }
/* registration fee input */
.fee-row { display: flex; align-items: center; gap: 8px; }
.fee-currency { font: 700 15px var(--mono); color: var(--t-muted); flex: none; }
.fee-row .field-input { flex: 1; }

/* added-source tags (one per ladder / group) above the invite list */
.src-tags { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 4px; }
.src-tags[hidden] { display: none; }
.src-tags-lbl { font: 700 10px var(--mono); letter-spacing: .1em; text-transform: uppercase;
  color: var(--t-dim); align-self: center; margin-right: 2px; }
.src-tag { display: inline-flex; align-items: center; gap: 8px; font: 700 12px var(--sans);
  border-radius: var(--r-full); padding: 6px 8px 6px 12px;
  color: var(--acc); border: 1px solid var(--acc-line);
  background: var(--acc-wash); }
.src-tag--group { color: var(--t-body); border-color: var(--border-2); background: var(--surface-2); }
.src-tag-n { font: 700 11px var(--mono); opacity: .8; }
.src-tag-x { border: none; background: none; color: currentColor; opacity: .7; cursor: pointer;
  font: 700 14px var(--sans); line-height: 1; padding: 0 2px; }
.src-tag-x:hover { opacity: 1; color: var(--danger); }

/* Excel uploader */
.uploader {
  display: flex; flex-direction: column; align-items: center; gap: 6px; text-align: center;
  border: 1.5px dashed var(--border-2); border-radius: var(--r-m); padding: 28px 20px;
  cursor: pointer; background: var(--surface-2); transition: border-color 150ms ease, background 150ms ease;
}
.uploader:hover { border-color: var(--t-dim); background: var(--surface-2); }
.uploader-ico { font-size: 26px; color: var(--t-muted); }
.uploader-main { font: 600 13.5px var(--sans); color: var(--t-body); }
.uploader-main .link { color: var(--acc); }
.uploader-sub { font: 500 11px var(--sans); color: var(--t-muted-2); }
.upload-result {
  margin-top: 12px; font: 600 12.5px var(--sans); color: var(--acc); text-align: center;
  background: var(--acc-wash);
  border: 1px solid var(--acc-line);
  border-radius: var(--r-s); padding: 10px 12px;
}
.upload-result[hidden] { display: none; }

.wiz-review { display: flex; flex-direction: column; }
.rev-row { display: flex; align-items: center; justify-content: space-between; gap: 14px; padding: 12px 0; border-top: 1px solid var(--divider); }
.rev-row:first-child { border-top: none; }
.rev-k { font: 600 12px var(--sans); color: var(--t-muted); }
.rev-v { font: 700 13.5px var(--sans); color: var(--t-hi); text-align: right; }

.wiz-success {
  text-align: center; background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-l); padding: 40px 28px;
}
.wiz-success[hidden] { display: none; }
.wiz-check {
  width: 64px; height: 64px; border-radius: 50%; margin: 0 auto 18px;
  display: flex; align-items: center; justify-content: center;
  font: 800 32px var(--sans); color: var(--on-acc); background: var(--acc);
}
.wiz-done-title { font: 800 26px var(--cond); letter-spacing: .02em; text-transform: uppercase; color: var(--t-hi); }
.wiz-done-sub { font: 400 13.5px/1.6 var(--sans); color: var(--t-muted); max-width: 420px; margin: 12px auto 22px; }
.wiz-done-btn { display: inline-block; text-decoration: none; padding: 11px 20px; }

/* ---- DESKTOP (>= 880px) -----------------------------------------------------
   This file used to be desktop-first with a max-width override block, which is
   how it ended up "desktop-dense" with three media queries covering seven
   declarations while 37 templates grew on top of it: every new component had to
   be written twice and the phone half was the half that got forgotten.

   Now the base rules ARE the phone rules and this block ADDS desktop, the same
   polarity as climb.css and bracket.css. Every declaration here is the value the
   base rule used to carry, so at >= 880px the cascade resolves to exactly what
   it did before the inversion.

   The admin UX is unchanged: the five-step wizard and the drag-to-edit draw are
   still desktop jobs (HANDOFF §7). They simply no longer overflow.
   -------------------------------------------------------------------------- */
@media (min-width: 880px) {
  .ah { padding-left: 30px; padding-right: 30px; }
  /* ONE ROW from here, with the same three children in the same markup order.
     `order` does the rearranging so the phone's two-row split needs no second
     header — the brand leads, the strip follows it, the right group is pushed
     out by its own auto margin. */
  .ashell .ah { flex-wrap: nowrap; gap: 26px; padding: 0 30px; }
  .ashell .ah-brand { order: 0; height: 78px; }
  .ashell .ah-brand .klogo { height: 40px; }
  .atabs { order: 1; flex: none; gap: 8px; padding-bottom: 0; }
  .ashell .ah-right { order: 2; height: 78px; gap: 14px; }
  .ashell .ah-asof { font-size: 12px; }
  /* The card's drawn proportions. Phone rows are shorter because a phone has
     less of everything; the artwork's 63px row is a 1440 value.

     THE TITLE NO LONGER GROWS HERE. It was 18px on the desk because condensed
     caps at 15 are small and quiet; in the body face at sentence case the same
     15px reads a size larger than it measures, and the 2026-08c board draws it
     at ~14 beside a 26px figure. A title that outgrew the section head above it
     — now 18px itself — would invert the two. */
  .acard-head { min-height: 48px; padding: var(--sp-2) 20px; }
  .acard-fig-n { font-size: 26px; }
  .acard-fig-n--text { font-size: var(--fs-title); }
  .arow { min-height: 63px; padding: var(--sp-2) 20px; }
  .arow-fig { font-size: 24px; }
  .arow-name { font-size: var(--fs-lead); }
  .ashell .acard .prow { padding: var(--sp-3) 20px; }
  /* The artwork's toolbar is one row: link, search, Upload. */
  .ashell .ptools { flex-wrap: nowrap; gap: var(--sp-3); }
  .ashell .search { flex: 1 1 240px; }
  .ashell .pfilters { flex-wrap: nowrap; }
  /* The artwork's size. See .atab for why the phone value is 13 and not this. */
  .atab { font-size: 17px; min-height: 36px; }
  .atab--active { padding: 0 14px; }
  .admin-main { padding: 22px 30px 60px; }
  .admin-grid { grid-template-columns: minmax(0, 760px); }
  .season-band { flex-direction: row; align-items: flex-start; }
  .wf-row { flex-wrap: nowrap; }
  /* .field-row's desktop direction moved to shared.css with the class. */
  .wiz-slabel { display: block; }
}

/* =========================================================================
   Tournaments
   Card grid mirrors the ladder cards on the site home (same .lgrid/.lcard
   recipe, lifted out of that page's inline styles) so a tournament card and
   a ladder card are visibly the same object.
   ========================================================================= */

/* THE PAGE-TITLE TREATMENT, and it is the player view's — DESIGN.md §2, 2026-08
   admin artwork. Uppercase Saira Condensed until then; the gradient belongs to
   a specific shape of headline (Archivo 700, title case, -.02em) and taking the
   colour without the letterforms would have left the product with two headline
   treatments sharing a paint job.

   Phone value in the base rule, desktop in the min-width block below — the
   artwork measures ~54px at 1440 and that is not a phone size.

   `background-clip: text` needs a `background`, so the ink comes from the
   gradient and `color` is transparent. Any page wanting flat ink must opt out
   the way .phead-h1--greet does in player.css, not by setting `color`. */

/* ⚠ INLINE-BLOCK, AND THAT IS THE WHOLE OF THE "HEADER TEXT STYLE" NOTE ON THE
   2026-08c board. `background-clip: text` samples the gradient across the
   element's PADDING BOX, and an <h1> is a block — so the sweep was measured
   against the 1200px column and not against the word sitting in it. "Ladders"
   at 54px is ~200px wide, which is the first sixth of the ramp, so it rendered
   cream-to-pale-peach and never reached --action at all; only a title long
   enough to fill the column ever showed the orange end. Every drawn title runs
   the full cream → orange sweep, whatever its length.

   `max-width: 100%` so a long club name still wraps rather than pushing the
   column wide, and `width: fit-content` is deliberately NOT used: on a wrapped
   two-line title fit-content shrinks to the longest line, which is what we
   want anyway, and inline-block already does it without the intrinsic-sizing
   pass. */
.page-title {
  display: inline-block; max-width: 100%;
  font: 700 var(--fs-display)/1.12 var(--sans); letter-spacing: -.02em;
  margin: 8px 0 6px;
  background: var(--grad-name);
  -webkit-background-clip: text; background-clip: text;
  color: transparent;
}
.page-sub { font: 400 13.5px/1.6 var(--sans); color: var(--t-muted); margin: 0 0 22px; }
/* Its own block rather than a line in the one at the top of this section: that
   one sits ABOVE these rules, and an equal-specificity declaration up there
   loses to the base rule down here. Additive and min-width, so admin.css stays
   phone-first. */
@media (min-width: 880px) {
  .page-title { font-size: var(--fs-display-2); margin: 10px 0 10px; }
  .page-sub { font-size: 15px; margin-bottom: 30px; }
}

.panel-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
.panel-head .panel-title { margin-bottom: 0; }
/* space-between works on TWO children. A head with a title and more than one
   control needs them grouped, or the second control drifts into the middle. */
.panel-head-acts { display: flex; align-items: center; gap: 9px; }
.panel-head-acts form { margin: 0; }


.lgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 12px; }
.lcard {
  position: relative; display: flex; flex-direction: column; gap: 8px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-m); padding: 15px 16px 16px;
  text-decoration: none; color: var(--t-body); cursor: pointer;
}
.lcard:hover { border-color: var(--t-dim); }
.lcard-name { font: 800 17px/1.1 var(--cond); letter-spacing: .02em; text-transform: uppercase; color: var(--t-hi); padding-right: 28px; }
.lcard-stats { font: 500 12px var(--mono); color: var(--t-muted); }
.lcard-health { display: flex; align-items: center; gap: 7px; font: 600 12.5px var(--sans); color: var(--t-body); }
.lcard-notif {
  position: absolute; top: 11px; right: 11px; min-width: 23px; height: 23px; padding: 0 6px;
  display: grid; place-items: center; border-radius: var(--r-full);
  background: var(--amb); color: var(--on-amb); font: 800 11.5px var(--mono);
}
.lcard--new {
  border-style: dashed; border-color: var(--border-2); background: transparent;
  align-items: center; justify-content: center; text-align: center; color: var(--t-muted); min-height: 108px;
}
.lcard--new:hover { color: var(--acc); border-color: var(--acc-line); }
.lcard-plus { font: 300 30px/1 var(--sans); }

/* ---- field list ---- */
.fld-group { margin-bottom: 16px; }
.fld-group:last-child { margin-bottom: 0; }
.fld-list { list-style: none; margin: 8px 0 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 2px 14px; }
.fld-row { display: flex; align-items: center; gap: 9px; padding: 5px 0; border-bottom: 1px solid var(--divider); }
.fld-name { flex: 1; font: 600 13px var(--sans); color: var(--t-body); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fld-rank { font: 500 11px var(--mono); color: var(--t-muted); }
/* A player swapped out but not yet saved, or already withdrawn. */
.fld-row.is-out .fld-name { color: var(--t-dim); text-decoration: line-through; }
.fld-row.is-dirty { box-shadow: inset 2px 0 0 var(--amb); }
.fld-row.is-dirty .fld-name { color: var(--amb); }
/* ⚠ THE NAME IS NOT THE RED THING, and it used to be. A player who is IN the
   field but told this event they cannot play was flagged by painting their
   NAME --danger, on the argument that the admin has to be told at a glance
   which row needs a decision. The flag was right and it was on the wrong word:
   a red name says something is wrong with the PERSON and does not say what,
   so it needs the state word beside it read anyway before it means anything.
   The state word carries it now (.elig-resp--withdrawn below) and the name
   stays --t-body like every other name in the list.

   This is the standings row's lesson applied a second time — see CLAUDE.md on
   the beaten/lost tint, which went wash, then glyph, then a chip reading
   "Prev: You won", each pass saying more of the fact out loud. A colour needs
   a legend; the word IS the legend.

   `.fld-row.is-out` above is untouched and still dims and strikes through:
   that means "already taken out of the field", which this player has not
   been — they are occupying a seed right now. */

/* ---- eligibility field (_eligibility_field.html) ----
   ONE COLUMN, not `.fld-list`'s auto-fill grid — that grid packs short
   name/seed pairs at 210px each, and "No answer yet" does not fit one. Reuses
   `.fld-row`'s row shell (the border, the gap, `.fld-name`) so the two lists
   still read as the same kind of thing; only the layout and the third column
   are new. */
.elig-field-list { display: flex; flex-direction: column; gap: 0; margin: 8px 0 0; padding: 0; list-style: none; }
/* THE WORD IS THE SIGNAL — no chip, no wash, the same rule the player view's
   card states already follow (CLAUDE.md). Confirmed takes the product's one
   "good" cyan; no answer is plainly muted, not warned about — most rows sit
   here for most of the qualifying window and that is normal, not a problem
   to flag amber.

   WITHDRAWN IS NOT STRUCK THROUGH, by request (2026-08d). It used to be,
   borrowed from `.fld-row.is-out` below on the reasoning that the two look
   alike — but they mean opposite things, and that is exactly why the borrow
   was wrong. Striking a name says "this player is OUT of the field".
   Striking their ANSWER said the answer had been retracted, when what it
   records is a player who is still standing in the field and has said they
   cannot play — the one row an admin most needs to read, dressed up as the
   one they can skip. The colour carries it. */
/* RESERVE — ranked below the cut, asked the same question, not entered by
   the lock. One letter beside the name, muted: it says why the row is on a
   list that is longer than the field will be, and the ANSWER beside it is
   what the admin is actually scanning for. `flex: 0 0 auto` so it never
   competes with `.fld-name` for the row's width. */
.elig-r {
  flex: 0 0 auto; font: 700 10px var(--mono); letter-spacing: .04em;
  color: var(--t-muted-2); border: 1px solid var(--border-2);
  border-radius: var(--r-xs); padding: 1px 5px;
}
.elig-resp { font: 600 12px var(--sans); flex: 0 0 auto; }
.elig-resp--confirmed { color: var(--acc); }
/* ⚠ --danger, NOT --neg, AND THE TOKENS' OWN COMMENT IS WHY IT LOOKS WRONG.
   `--neg` is grey because "a lost match, a dropped rank, a downward trend" is
   an outcome and losses are not punished — but a withdrawal is not a defeat.
   It is a HOLE IN THE FIELD: the admin has to pull a reserve in, and
   `seed_and_draw` warns by name when a decliner is still in the draw. That is
   --danger's "something is wrong", the same tier as an overdue match.

   It was --t-dim, which is DIMMER THAN --none's --t-muted (neutral-38 against
   neutral-56) — so the one state needing action was the quietest thing in the
   row, and the two greys read as one. This panel is scanned down twenty rows
   for exactly that difference. */
.elig-resp--withdrawn { color: var(--danger); }
.elig-resp--none { color: var(--t-muted); font-weight: 500; }

/* THE ANSWER AS A CONTROL, on the one panel that may change it. The colour
   classes above are shared with the select deliberately: this list is scanned
   down twenty rows for who has not replied, and a control that dropped the
   colour would cost the panel the thing it is read for. Transparent ground and
   a hairline border so it still reads as the value first and a control second
   — the Field section below shows the same word with no box at all, which is
   what says "that one is a report, this one is the switch". */
.elig-set { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; margin: 0; }
.elig-sel {
  font: 600 12px var(--sans); padding: 3px 6px;
  background: transparent; border: 1px solid var(--border-2);
  border-radius: var(--r-xs); cursor: pointer;
}
.elig-sel:hover { border-color: var(--t-muted-2); }
.elig-sel:disabled { cursor: default; opacity: .5; }
/* ⚠ THE OPEN LIST IS NOT DRAWN BY THIS PAGE, and the state colour above is
   wrong the moment it leaves the card. A popup's <option>s INHERIT the
   select's `color` — cyan, red or grey, all picked to sit on a dark card —
   while the popup itself is painted by the UA on ITS ground, which is WHITE
   while nothing declares a scheme. So the closed control read correctly and
   the open list was pale-on-white: "No answer yet" all but invisible,
   "Withdrawn" a wash of pink.

   Two declarations, and both are needed. `color-scheme` is what tells the UA
   to paint its own furniture dark — the popup ground, the scrollbar, the
   highlight — and the system colours then put the option text back on the
   UA's terms instead of the card's, so the pair moves together whatever the
   engine decides to honour. Naming a hex here would fix the ground and not
   the highlight, and would be a second palette to keep in step.

   ⚠ NOT A HEX, ALSO BECAUSE IT CANNOT BE: tokens.css is the only file allowed
   to declare one (test_only_tokens_css_declares_the_palette), and a token
   would be the card's colour again — which is the bug.

   The COLOUR CUE STAYS ON THE CLOSED CONTROL, which is the thing being
   scanned down twenty rows. An open list shows one row at a time and is being
   read, not scanned. */
.elig-sel { color-scheme: dark; }
.elig-sel option { color: CanvasText; background-color: Canvas; }
.elig-go { padding: 3px 9px; font-size: 12px; }

/* ---- tournament wizard: the drag-and-drop picker ---- */
.draw-readout { font: 700 12px var(--mono); letter-spacing: .04em; color: var(--acc); margin-top: 9px; }
.draw-readout.is-warn { color: var(--amb); }

.pick { display: grid; grid-template-columns: 1fr; gap: 14px; }
.pick-col { min-width: 0; }
.pick-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 9px; min-height: 34px; }
.pick-search { max-width: 140px; padding: 7px 10px; font-size: 12.5px; }
.pick-hint { font: 500 11px var(--sans); color: var(--t-muted-2); }

.pick-list {
  min-height: 260px; max-height: 420px; overflow-y: auto;
  border: 1px solid var(--border); border-radius: var(--r-m);
  background: var(--surface-2); padding: 8px; display: flex;
  flex-direction: column; gap: 6px;
}
.pick-list--drop { border-style: dashed; border-color: var(--border-2); }
.pick-list.is-over { border-color: var(--acc); background: var(--acc-wash); }
.pick-empty { padding: 22px 12px; text-align: center; font: 500 12.5px/1.5 var(--sans); color: var(--t-dim); }

.pcard {
  display: flex; align-items: center; gap: 9px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-s); padding: 8px 9px; cursor: grab;
}
.pcard:hover { border-color: var(--t-dim); }
.pcard.is-dragging { opacity: .4; cursor: grabbing; }
.pcard-seed { font: 700 11px var(--mono); color: var(--acc); min-width: 15px; text-align: right; }
.pcard-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.pcard-name { font: 600 13px var(--sans); color: var(--t-body); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pcard-meta { font: 500 10.5px var(--mono); color: var(--t-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pcard-btn {
  flex: 0 0 auto; width: 24px; height: 24px; border-radius: var(--r-xs);
  border: 1px solid var(--border-2); background: transparent;
  color: var(--t-muted); font: 500 15px/1 var(--sans); cursor: pointer;
}
.pcard-btn:hover { color: var(--acc); border-color: var(--acc-line); }

.invite-note { border: 1px dashed var(--border-2); border-radius: var(--r-m); padding: 18px 20px; }

/* ---- action bar above a generated draw ---- */
.actbar { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; margin: 0 0 16px; }
.actbar form { margin: 0; }
.actbar-gap { flex: 1; }

/* ---- unsaved draw edits ---- */
.savebar {
  position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%);
  z-index: 60; display: flex; align-items: center; gap: 12px;
  background: var(--surface); border: 1px solid var(--amb);
  border-radius: var(--r-m); padding: 11px 14px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, .45);
  min-width: min(420px, calc(100vw - 32px));
}
.savebar-count { font: 600 13px var(--sans); color: var(--amb); }
.savebar-count b { font-family: var(--mono); }
.savebar-gap { flex: 1; }

/* ⚠ THE FIELD'S × WAS INVISIBLE, AND IT LOOKED LIKE A MISSING FEATURE.
   `.br-x` ships `opacity: 0` and is revealed by `.br-side:hover .br-x`
   (bracket.css) — but `.br-side` is a BRACKET class, and a field row is a
   `.fld-row`. So nothing on this list ever revealed it: the button was in the
   DOM, focusable, clickable if you found it, and painted at zero opacity.
   Reported as "I am still unable to edit players in the field", and I
   confirmed the markup rather than the paint, which is exactly how it
   survived a round of verification.

   ALWAYS VISIBLE HERE, not hover-revealed. The bracket hides it because that
   grid is dense and every cell carries one; the field is a short list an
   admin opens specifically to edit, where a control you have to discover by
   hovering is a control most people never find. */
.fld-row .br-x { opacity: 1; color: var(--t-muted-2); }
.fld-row .br-x:hover { color: var(--danger); background: var(--danger-container); }
/* And the seat that cannot be swapped — same reasoning as bracket.css's
   `.br-x--off`, restated because this list is styled here and does not load
   that sheet's hover rules. Dimmer than the live control and not a button. */
.fld-row .br-x--off { color: var(--t-dim-2); cursor: help; }
.fld-row .br-x--off:hover { color: var(--t-muted-2); background: transparent; }

/* Contact, on every row of the Eligibility panel. THE ADMIN SHELL'S OWN
   BUTTON, not the player card's `.crow-contact`: that one lives in player.css,
   which `_base.html` deliberately does not load, and reaching for it here
   would mean linking that sheet — the edit CLAUDE.md names as the one that
   quietly makes it shared. The SHEET the two open is the same file and its
   rules are in shared.css; only the trigger differs, which is right, because
   one is a pill on a card and this is a control at the end of a dense list.

   Quiet by default and only coloured on hover: the panel's job is reading who
   has answered, and 32 orange buttons down the right-hand side would compete
   with the answers they sit beside. `flex: 0 0 auto` because `.fld-name` is
   the only thing in that row meant to stretch. */
.fld-contact {
  flex: 0 0 auto;
  border: 1px solid var(--card-line); border-radius: var(--r-full);
  background: transparent; color: var(--t-muted-2);
  font: 600 11px var(--sans); letter-spacing: .02em;
  padding: 3px 10px; cursor: pointer;
}
.fld-contact:hover { color: var(--t-hi); border-color: var(--t-muted-2); }

/* Remove sits ABOVE the search box, because it is the other whole gesture
   this dialog offers rather than a footnote to replacing. Danger red for the
   same reason `--danger` is used on a withdrawn name: it takes somebody out
   of a field, and the pull-in behind it is described rather than implied. */
.pick-remove {
  display: flex; flex-direction: column; gap: 4px;
  padding-bottom: 12px; margin-bottom: 12px;
  border-bottom: 1px solid var(--divider);
}
.pick-remove .field-hint { margin: 0; }

/* The reserves get their own shorter list above the club's. Capped so the
   band never pushes the search box off a laptop screen — it holds at most
   RESERVE_BAND rows, but the cap keeps that promise local to the CSS. */
.pick-list--reserves { min-height: 0; max-height: 208px; }
/* Already in the field: shown rather than dropped, so this list keeps
   agreeing with the field below it, but not offerable. */
.pcard.is-taken { opacity: .55; cursor: default; }
.pcard.is-taken .pcard-btn { visibility: hidden; }

/* ---- replacement picker ---- */
.pick-dlg {
  border: 1px solid var(--border-2); border-radius: var(--r-l);
  background: var(--surface); color: var(--t-body);
  padding: 20px; width: min(460px, calc(100vw - 32px));
}
.pick-dlg::backdrop { background: rgba(0, 0, 0, .6); }
.pick-dlg .pick-list { min-height: 180px; max-height: 300px; margin-top: 10px; }

/* ---- per-round scoring ---- */
.rnd-list { display: flex; flex-direction: column; gap: 7px; }
.rnd-row {
  display: grid; grid-template-columns: 1fr; align-items: center; gap: 6px;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--r-s); padding: 9px 12px;
}
.rnd-name { font: 800 13px var(--cond); letter-spacing: .06em; text-transform: uppercase; color: var(--t-hi); }
.rnd-sub { font: 500 11px var(--mono); color: var(--t-dim); }
.rnd-sel { padding: 8px 10px; font-size: 12.5px; }
.rnd-row.is-locked { opacity: .55; }
.rnd-locked { font: 500 11px var(--mono); color: var(--amb); }

/* ---- message composer: picking people, and what was already sent ----
   ITS OWN ROW AND NOT `.rnd-row`, which is two columns' worth of content in a
   grid that becomes `60px 1fr 260px` at 720px — a shape the per-round scoring
   rows need and a name/address pair does not. */
.pick-people { max-height: 320px; overflow-y: auto; padding-right: 4px; }
.mrow {
  display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 10px;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--r-s); padding: 9px 12px;
}
.mrow--off { opacity: .55; }
.mrow-who { display: flex; align-items: center; gap: 9px; min-width: 0; }
.mrow-name { font: 800 13px var(--cond); letter-spacing: .06em; text-transform: uppercase; color: var(--t-hi); }
.mrow-sub { font: 500 11px var(--mono); color: var(--t-dim); overflow-wrap: anywhere; }
.mrow-why { font: 500 11px var(--mono); color: var(--amb); overflow-wrap: anywhere; }
label.mrow { cursor: pointer; }

.sendlog { display: flex; flex-direction: column; gap: 12px; }
.sendlog-item { padding-bottom: 12px; border-bottom: 1px solid var(--divider); }
.sendlog-item:last-child { padding-bottom: 0; border-bottom: 0; }
.sendlog-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.sendlog-meta { font: 500 11.5px/1.5 var(--sans); color: var(--t-muted-2); margin-top: 4px; }
.sendlog-bad { color: var(--amb); font-weight: 700; }

/* ---- a stated rule, not a control ---- */
.rule-card { border: 1px solid var(--border); border-radius: var(--r-m); background: var(--surface-2); padding: 15px 17px; }
.rule-title { font: 700 12px var(--sans); color: var(--t-hi); margin-bottom: 9px; }
.rule-list { margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 5px; }
.rule-list li { font: 500 12.5px/1.5 var(--sans); color: var(--t-body); }
.rule-list b { color: var(--acc); font-weight: 700; }
.rule-note { font: 500 11.5px/1.5 var(--sans); color: var(--t-muted-2); margin-top: 11px; padding-top: 10px; border-top: 1px solid var(--divider); }

/* The round-settings row is three columns once there is room for three. */
@media (min-width: 720px) {
  .rnd-row { grid-template-columns: 60px 1fr 260px; gap: 12px; }
}

/* ---- wizard review ---- */
.rev-row { display: flex; justify-content: space-between; gap: 14px; padding: 11px 2px; border-bottom: 1px solid var(--divider); }
.rev-k { font: 700 11px var(--mono); letter-spacing: .12em; text-transform: uppercase; color: var(--t-muted-2); }
.rev-v { font: 600 13.5px var(--sans); color: var(--t-hi); text-align: right; }

/* The field picker is two side-by-side columns only when they fit. */
@media (min-width: 720px) {
  .pick { grid-template-columns: 1fr 1fr; }
  .pick-search { max-width: 190px; }
}

/* ---- result dialog ----
   .report-dlg, .radio-row and .dlg-actions moved to shared.css, with the score
   control itself. The dialog is included by an admin page AND a player page,
   so its own styling cannot live in the admin sheet. */


/* =========================================================================
   Phone affordances

   None of this existed at any width before: the file had no rule below 720px,
   no touch-target sizing and no (hover: none) handling at all.
   ========================================================================= */

/* `.field-input { min-width: 0 }` and `.field-hint`'s overflow-wrap moved to
   shared.css WITH their classes. Both are §7d fixes, and a fix left behind here
   would be a phone bug that only appears on the player shell. */

/* Emails, member ids and raw CSV lines have no break opportunity, so without
   this they widen their container past the viewport instead of wrapping. */
.page-sub, .wf-meta, .rev-v, .ins-sub, .hl-text, .invite-breakdown { overflow-wrap: anywhere; }
code { overflow-wrap: anywhere; }

/* A row with controls beside it cannot keep both legible on a phone: .p-id was
   being squeezed to 78px of 303px on the administrators list. Below this width
   the controls wrap under the name and take the full row. */
@media (max-width: 560px) {
  .prow { flex-wrap: wrap; }
  .p-controls { width: 100%; justify-content: flex-end; }
  /* Beside the name is a DESKTOP idea. At 375px a 52% cap leaves the name
     about 150px, so the tags drop under it and go back to reading left-to-
     right with everything else. */
  .p-tags { max-width: 100%; width: 100%; justify-content: flex-start; }
  /* The button inside the box costs real estate the placeholder needs. */
  .search-input { padding-right: 84px; }
  .actbar { gap: 8px; }
  .actbar .abtn { flex: 1 1 auto; text-align: center; }
}

/* Touch. Reasoned exactly as bracket.css:157 already does for the bracket's own
   controls: there is no hover to reveal anything, and a 24-30px target is far
   below the ~44px a finger needs. Keyed on (hover: none) rather than on width,
   because the input device is the actual question — a small window on a desktop
   still has a mouse. */
@media (hover: none) {
  .icon-btn { width: 44px; height: 44px; font-size: 14px; }
  .inv-x, .src-tag-x, .src-clear { min-width: 44px; min-height: 44px; padding: 0 10px; }
  /* .abtn's 44px moved to shared.css with the class — the player shell does
     not load this file, so leaving it here loses the touch target there. */
  .demo-reset, .p-remove, .act-btn, .wbtn { min-height: 44px; }
  .ah-link, .ah-back { padding: 10px 12px; }
  /* The card's pill is drawn 28px and stays 28px — the TARGET grows, not the
     box, the same split .atab and player.css's row CTAs use. A ::before rather
     than min-height, because a 44px pill in a 56px row is a card that reads as
     buttons with text between them. */
  .apill { position: relative; }
  .apill::before {
    content: ""; position: absolute; left: 0; right: 0;
    top: 50%; height: 44px; transform: translateY(-50%);
  }
  .arow-go { min-height: 44px; }
  /* Hover styling is a lie on touch — it sticks after a tap until you tap
     elsewhere, so a "hovered" row stays highlighted and reads as selected. */
  .prow:hover, .need:hover, .wf-row:hover { border-color: var(--border); }
  .arow:hover, .apill:hover, .arow-go:hover { filter: none; }
  .apill--ghost:hover { background: none; }
  .need:hover { transform: none; }
}
