/* =====================================================================
   Pickleta — design system

   Tokens are role-named, never hue-named, so a page asks for "the thing
   you press" rather than for lime. Nothing declares a colour outside this
   block: a hex anywhere below is a bug, and inklint/contrast check it.

   COURT NIGHT.
   The ground is cool paper, the ink is a deep court green-black, and the
   energy is optic lime — the colour of the ball. Lime is both "press this"
   and "happening now", because on a board those are usually the same thing:
   the court that is live is the court you tap. Teal supports; warm orange
   is kept for a result, never for an action.

   Every pairing in here is measured by contrast.php, which reads its values
   out of this file rather than keeping its own copy — a gate with its own
   palette can pass while the stylesheet says something else.
   ===================================================================== */

:root {
  /* =====================================================================
     Navy + lime, sampled from the Pickleta logo: #013E83 from the wordmark
     and #BED31E from the ball. There is no third colour in the mark, so the
     accent is the blue rather than a colour invented to sit beside it.
     Same token names as before, retuned values — every stylesheet reads
     from here, so the whole app changes with this block rather than with
     a find-and-replace across five files.
     Contrast ratios are kept in the comments because they are the reason
     each value is what it is, not decoration.
     ===================================================================== */

  /* ---- ground ---- */
  /* The paper carries a trace of the logo's navy, so white cards sit on
     something related to the brand rather than on neutral grey. */
  --bg:            #F5F7FB;
  --bg-alt:        #E8EEF8;
  --surface:       #FFFFFF;
  --surface-alt:   #EFF3F9;
  --border:        #DFE6F0;
  /* 3.1:1 on white — a control edge has to be visible to be an edge. */
  --border-strong: #8494AB;

  /* ---- ink ---- */
  /* The logo's own dark, a shade deeper for running text. */
  --text:        #16243D;   /* 15.5:1 on white, 14.5:1 on the ground */
  --text-muted:  #4A5B76;   /* 6.9:1  on white */
  --text-faint:  #7787A0;   /* 3.7:1  on white */

  /* ---- the dark ground ---- */
  /* The sidebar, the hero overlay and every dark band. */
  /* The wordmark's own blue, straight off the logo. */
  --brand:       #013E83;
  --brand-deep:  #012C5E;
  --on-brand:    #E9F0FA;   /* 9.1:1 on the brand */

  /* ---- support: the logo's blue ---- */
  /* The mark is two colours, so the accent is the blue rather than a third
     hue invented to sit beside them. A step lighter than --brand, so a link
     on white is not the same value as the band behind the header. */
  --accent:      #0B4C95;   /* 8.5:1 on white  */
  --accent-soft: #E6EEFA;   /* accent on it: 7.2:1 */
  --on-accent:   #FFFFFF;   /* 8.5:1 on the accent */
  --accent-on-dark: #8FC2FF;/* 5.6:1 on the brand */
  /* The logo's blue at full strength, for fills and marks rather than text. */
  --accent-pure: #013E83;

  /* ---- energy: the action layer ---- */
  /* The lime from the ball. Four values
     because each does a different job: a colour bright enough to fill a
     button is never readable as text on one. */
  --action:         #BED31E;  /* the ball, sampled                         */
  --action-dark:    #7C8C12;  /* its edge and hover: 3.7:1 on white        */
  --action-ink:     #53600A;  /* lime as *text*: 6.9:1 on white            */
  --action-soft:    #F4F8DA;  /* the tint behind a badge: ink on it 6.3:1  */
  /* The logo puts navy dots on the lime ball, so the app writes on lime
     the way the mark does. 6.2:1. */
  --on-action:      #013E83;
  --action-on-dark: #BED31E;  /* lime needs no help on the brand: 6.2:1    */

  /* ---- live ---- */
  /* Named separately on purpose: if the action colour changes, "happening
     now" should not silently change with it. */
  --live:    #BED31E;
  --on-live: #013E83;

  /* ---- a result, never an action ---- */
  --win:      #C0532B;
  --win-ink:  #A5441E;
  --win-soft: #F7E7DF;

  /* ---- skill bands ---- */
  /* New: the mockup labels every session Beginner / Intermediate /
     Advanced. Each is a tint plus an ink that clears 4.5:1 on it, so the
     badge is legible rather than merely coloured. */
  --skill-all-ink:  #4A5C74;  --skill-all-soft:  #EFF3F8;
  --skill-beg-ink:  #1A5FA8;  --skill-beg-soft:  #E7F1FC;
  --skill-int-ink:  #2A6B2E;  --skill-int-soft:  #E8F4E9;
  --skill-adv-ink:  #16243D;  --skill-adv-soft:  #E5EAF2;

  /* ---- player discs ---- */
  /* Eight, so a court of four rarely repeats, and each carries white at
     5:1 or better. Assigned from the name so a player keeps their colour
     between sessions. No two neighbours closer than 21 degrees of hue. */
  --av-1: #2563C9;  /* blue    217deg  5.67:1 */
  --av-2: #3730A3;  /* indigo  244deg  9.93:1 */
  --av-3: #7A2E8E;  /* purple  288deg  8.05:1 */
  --av-4: #B8337A;  /* pink    328deg  5.54:1 */
  --av-5: #C0223F;  /* red     349deg  5.93:1 */
  --av-6: #B35309;  /* amber    26deg  5.05:1 */
  --av-7: #2F7D32;  /* green   122deg  5.12:1 */
  --av-8: #0F766E;  /* teal    175deg  5.47:1 */
  --av-ink: #FFFFFF;

  /* A real court is green, whatever the interface around it is doing. */
  --court-surface: #CCDAEE;
  --court-line:    #FFFFFF;

  /* ---- depth ---- */
  /* Tinted with the navy ink so a shadow belongs to this palette rather
     than being generic grey. */
  --shadow:      0 1px 2px rgba(27,42,51,.05), 0 8px 24px -14px rgba(27,42,51,.26);
  --shadow-lift: 0 2px 6px rgba(27,42,51,.09), 0 22px 46px -18px rgba(27,42,51,.38);
  --shadow-deep: 0 30px 60px -24px rgba(23,37,46,.45);
  --shadow-action: 0 10px 24px -10px rgba(110,144,24,.55);

  /* ---- shape ---- */
  --r-sm:   10px;
  --r-md:   14px;
  --r-lg:   20px;
  --r-xl:   26px;
  --r-full: 999px;

  /* ---- type ---- */
  /* The logo is set in a heavy rounded geometric -- perfectly circular
     counters, terminals cut round, the i-dot a full circle. Fredoka is the
     closest face of that description that can be loaded freely; it is a
     match for the logo's character rather than the logo's actual font,
     which is a commercial rounded geometric at a weight Fredoka does not
     reach. Headings and the brand wear it.
     Figtree stays for running text: a rounded display face is a pleasure
     in a wordmark and a chore in a paragraph, and this app has a lot of
     paragraphs. */
  --display: 'Fredoka', 'Figtree', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --body:    'Figtree', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --mono:    'DM Mono', ui-monospace, 'SFMono-Regular', Menlo, monospace;
  /* A dot-matrix face for numbers read at a glance across a hall: scores
     and queue positions. Never used for prose. */
  --dot:     'Doto', 'DM Mono', ui-monospace, monospace;

  /* ---- rhythm ---- */
  --s-1:  6px;
  --s-2: 10px;
  --s-3: 14px;
  --s-4: 20px;
  --s-5: 28px;
  --s-6: 40px;
  --s-7: 64px;

  --wrap: 1200px;
  --sec:  clamp(56px, 7.5vw, 104px);
  --pad:      20px;
  --pad-lg:   26px;

  /* ---- the shell ---- */
  --sidebar: 232px;
}

/* ---------- reset ---------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }

/* The browser's own [hidden] rule is the weakest there is, so any
   display we set — .btn is inline-flex — outranks it and the element
   stays visible while claiming to be hidden. Said once, with weight,
   in the one stylesheet every layout loads. */
[hidden] { display: none !important; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--body);
  font-size: 16px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
  overflow-x: hidden;
}
img, svg { max-width: 100%; }
button, input, select, textarea { font: inherit; color: inherit; }
button { background: none; border: none; cursor: pointer; touch-action: manipulation; }
a { color: var(--accent); text-decoration-thickness: 1px; text-underline-offset: 2px; }
:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
h1, h2, h3, h4 {
  margin: 0;
  font-family: var(--display);
  font-weight: 600;
  letter-spacing: -.02em;
  line-height: 1.05;
  text-wrap: balance;
}
p { margin: 0; }
.mono, .tnum { font-variant-numeric: tabular-nums; }
.mono { font-family: var(--mono); }
/* Scoreboard numerals. Large numbers only — a dot-matrix face at 12px
   is a puzzle rather than a style, so nothing small opts in. */
.dot {
  font-family: var(--dot);
  font-weight: 900;
  font-variant-numeric: tabular-nums;
  letter-spacing: .02em;
}

/* ---------- layout --------------------------------------------------- */
.wrap { width: 100%; max-width: var(--wrap); margin: 0 auto; padding: 0 20px; }
.wrap-narrow { max-width: 520px; }
.stack, .place-fields { display: flex; flex-direction: column; gap: 14px; }
.row { display: flex; align-items: center; gap: 10px; }
.spread { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.grid-2 { display: grid; gap: 14px; grid-template-columns: 1fr; }
@media (min-width: 720px) { .grid-2 { grid-template-columns: 1fr 1fr; } }

.eyebrow {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--text-faint);
}

/* ---------- site chrome ---------------------------------------------- */
.site-head {
  position: sticky; top: 0; z-index: 40;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: saturate(140%) blur(10px);
  border-bottom: 1px solid var(--border);
}
.site-head .inner { display: flex; align-items: center; justify-content: space-between; gap: 16px; height: 62px; }
.brand { display: inline-flex; align-items: center; gap: 9px; color: var(--text); text-decoration: none; font-family: var(--display); font-weight: 600; font-size: 18px; letter-spacing: -.02em; }
.brand svg { color: var(--accent); flex: none; }
.site-nav { display: flex; align-items: center; gap: 8px; }

/* ---- the header menu ------------------------------------------------- */
/* Hidden until a phone needs it. */
.navtoggle {
  display: none;
  align-items: center; justify-content: center;
  width: 42px; height: 42px; flex: none;
  padding: 0; border: 1px solid var(--border); border-radius: var(--r-md);
  background: var(--surface); color: var(--text); cursor: pointer;
}
.navtoggle:hover { border-color: var(--accent); color: var(--accent); }
/* Three bars drawn from one element: the middle is the box, the outer two
   are its pseudo-elements, so there is nothing extra in the markup. */
.navtoggle-bars,
.navtoggle-bars::before,
.navtoggle-bars::after {
  display: block; width: 18px; height: 2px;
  background: currentColor; border-radius: 2px;
}
.navtoggle-bars { position: relative; }
.navtoggle-bars::before,
.navtoggle-bars::after { content: ""; position: absolute; left: 0; }
.navtoggle-bars::before { top: -6px; }
.navtoggle-bars::after  { top: 6px; }
/* Open: the outer bars meet in the middle and cross. */
[aria-expanded="true"] .navtoggle-bars { background: transparent; }
[aria-expanded="true"] .navtoggle-bars::before { top: 0; transform: rotate(45deg); }
[aria-expanded="true"] .navtoggle-bars::after  { top: 0; transform: rotate(-45deg); }

/* The header on a phone.
   One fixed-height row with five things in it does not fit 390px. Wrapping
   them into a 2x2 grid fitted, but spent a third of the first screen on
   navigation. They go behind the button instead.

   .has-navtoggle is set by nav.js, so the closed state only exists once
   something can open it: with the script blocked the nav stays visible and
   the menu is merely tall, rather than unreachable. */
@media (max-width: 720px) {
  .navtoggle { display: inline-flex; }

  /* The panel is part of the bar, so it takes the bar's background --
     not the page's. Everything inside it (.site-link, .btn-ghost) is
     already coloured for a dark header by refresh.css; on a light panel
     those rules painted white text on near-white and the menu opened
     effectively blank. Inheriting the header's own colour keeps one
     source of truth for what the bar looks like. */
  .has-navtoggle .site-nav {
    display: none;
    position: absolute;
    top: 100%; left: 0; right: 0;
    flex-direction: column;
    align-items: stretch;
    gap: 8px;
    padding: 14px 20px 18px;
    background: var(--bg);
    border-bottom: 1px solid var(--border);
    box-shadow: var(--shadow-lift);
  }
  .has-navtoggle .site-nav.is-open { display: flex; }

  /* The header is the panel's anchor, so it needs a position of its own. */
  .site-head .inner { position: relative; }

  .site-nav .site-link {
    padding: 10px 2px;
    border-bottom: 1px solid var(--border);
  }
  .site-nav .btn { width: 100%; }
  .site-nav form { display: block !important; }
  .site-nav form .btn { width: 100%; }
}.site-foot { border-top: 1px solid var(--border); margin-top: var(--s-7); padding: var(--s-5) 0 34px; color: var(--text-faint); font-size: 13.5px; }

/* ---------- buttons -------------------------------------------------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  min-height: 46px; padding: 0 18px;
  border-radius: var(--r-md);
  font-weight: 700; font-size: 15px; letter-spacing: -.01em;
  background: var(--accent); color: var(--on-accent);
  text-decoration: none;
  transition: transform .08s ease, filter .15s ease;
}
.btn:hover { filter: brightness(1.07); }
.btn:active { transform: scale(.985); }
.btn:disabled { background: var(--surface-alt); color: var(--text-faint); cursor: not-allowed; filter: none; }
.btn-block { width: 100%; }
.btn-dark { background: var(--brand); color: var(--on-brand); }
.btn-ghost { background: transparent; color: var(--text); border: 1.5px solid var(--border-strong); }
.btn-ghost:hover { border-color: var(--accent); color: var(--accent); }
.btn-danger { background: transparent; color: var(--win-ink); border: 1.5px solid var(--win); }
.btn-danger:hover { background: var(--win); color: #fff; filter: none; }

/* ---- the scrim and the confirm dialog -------------------------------- */
/* Shared, because both layouts ask before doing something irreversible and
   only one of them used to. Moved out of board.css rather than copied into
   dashboard.css: two dialogs that are supposed to look identical will not
   stay that way. */
.scrim {
  position: fixed; inset: 0; z-index: 50; background: rgba(8,20,18,.6);
  display: flex; align-items: flex-end; justify-content: center;
}
/* ---- confirm dialog -------------------------------------------------- */
.confirm-box {
  width: 100%; max-width: 420px; margin: auto;
  background: var(--surface); border-radius: var(--r-lg);
  padding: 22px; box-shadow: var(--shadow-lift);
}
.confirm-box h3 { font-size: 18px; margin-bottom: 8px; }
.confirm-box p { color: var(--text-muted); font-size: 14.5px; margin-bottom: 18px; }
.confirm-actions { display: flex; gap: 10px; }
.confirm-actions .btn { flex: 1; }
.scrim-center { align-items: center; padding: 20px; }
.btn-sm { min-height: 36px; padding: 0 13px; font-size: 13.5px; border-radius: var(--r-sm); }
.btn-lg { min-height: 54px; padding: 0 26px; font-size: 16.5px; }

/* ---------- cards & forms -------------------------------------------- */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow);
}
.card-pad { padding: 20px; }

.field { display: flex; flex-direction: column; gap: 6px; }
.label { font-size: 13px; font-weight: 700; color: var(--text-muted); }
.input {
  width: 100%; min-height: 46px; padding: 11px 14px;
  background: var(--surface); color: var(--text);
  border: 1.5px solid var(--border); border-radius: var(--r-md);
  font-size: 16px; /* 16px keeps iOS from zooming on focus */
}
.input:focus { border-color: var(--accent); outline: none; }
.input[aria-invalid="true"] { border-color: var(--win); }
select.input { appearance: none; padding-right: 38px;
  background-image: linear-gradient(45deg, transparent 49%, currentColor 50%), linear-gradient(-45deg, transparent 49%, currentColor 50%);
  background-position: calc(100% - 19px) 20px, calc(100% - 13px) 20px;
  background-size: 6px 6px; background-repeat: no-repeat;
}
.err { font-size: 12.5px; color: var(--win-ink); font-weight: 600; }
.hint { font-size: 13px; color: var(--text-faint); line-height: 1.5; }

.check { display: flex; gap: 12px; align-items: flex-start; padding: 13px 14px; border: 1px solid var(--border); border-radius: var(--r-md); cursor: pointer; background: var(--surface); }
.check input { width: 20px; height: 20px; flex: none; margin-top: 2px; accent-color: var(--accent); }
.check strong { display: block; font-size: 14.5px; }
/* Stacked under the title, and stacked under each other when there are
   two — a switch that is off for a reason says both what it does and why
   it cannot, and inline they ran together into one sentence. The same
   treatment .settoggle already gave its own rows. */
.check .hint { display: block; margin-top: 2px; }

.segmented { display: flex; background: var(--surface-alt); border-radius: var(--r-md); padding: 3px; gap: 3px; }
.segmented label { flex: 1; }
.segmented input { position: absolute; opacity: 0; pointer-events: none; }
.segmented span {
  display: flex; align-items: center; justify-content: center;
  min-height: 40px; border-radius: var(--r-sm); font-size: 14px; font-weight: 700;
  color: var(--text-muted); cursor: pointer;
}
.segmented input:checked + span { background: var(--surface); color: var(--text); box-shadow: var(--shadow); }
.segmented input:focus-visible + span { outline: 3px solid var(--accent); outline-offset: 2px; }

/* ---------- player discs --------------------------------------------- */
/* Initials rather than a silhouette: a room of identical grey outlines
   tells you nothing, and the first letter of a name is the thing people
   actually scan a list for. */
.avatar {
  width: 34px; height: 34px; flex: none;
  border-radius: var(--r-full);
  display: grid; place-items: center;
  font-size: 12.5px; font-weight: 600; letter-spacing: .02em;
  color: var(--av-ink); background: var(--av-1);
  -webkit-user-select: none; user-select: none;
}
.avatar-1 { background: var(--av-1); }
.avatar-2 { background: var(--av-2); }
.avatar-3 { background: var(--av-3); }
.avatar-4 { background: var(--av-4); }
.avatar-5 { background: var(--av-5); }
.avatar-6 { background: var(--av-6); }
.avatar-7 { background: var(--av-7); }
.avatar-8 { background: var(--av-8); }
.avatar-sm { width: 27px; height: 27px; font-size: 10.5px; }
.avatar-lg { width: 54px; height: 54px; font-size: 19px; }
/* A ring in the surface colour, so discs can overlap and still read as
   separate. Only where they do overlap — a lone disc needs no cutout. */
.avatar-ring { box-shadow: 0 0 0 2px var(--surface); }

/* ---------- chips, flashes ------------------------------------------- */
.chip {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 11px; font-weight: 700; letter-spacing: .04em;
  padding: 3px 9px; border-radius: var(--r-full);
  background: var(--surface-alt); color: var(--text-muted);
}
.chip-live { background: var(--live); color: var(--on-live); }
.chip-you  { background: var(--accent); color: var(--on-accent); }
.chip-host { background: var(--brand); color: var(--on-brand); }
.chip-done { background: var(--surface-alt); color: var(--text-faint); }
/* Two players who always play together. Quieter than HOST or YOU — it is
   a fact about the draw rather than about the person, and on a night with
   several pairs it repeats down the list. accent on its own tint: 5.45:1. */
.chip-pair { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
/* Something the draw needs that nobody has answered yet. The same quiet
   weight, because it is a prompt and not an error: the night still runs,
   it just runs on rating for that one player. Same measured pair. */
.chip-ask { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
button.chip-ask { border: 0; font-family: inherit; cursor: pointer; }
button.chip-ask:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.flash { padding: 12px 16px; border-radius: var(--r-md); font-size: 14px; font-weight: 600; border: 1px solid; }
.flash-success { background: var(--accent-soft); color: var(--accent); border-color: color-mix(in srgb, var(--accent) 35%, transparent); }
.flash-error   { background: var(--win-soft); color: var(--win-ink); border-color: color-mix(in srgb, var(--win) 35%, transparent); }

.livedot { width: 8px; height: 8px; border-radius: 50%; background: var(--live); flex: none; animation: pulse 2.4s ease-out infinite; }
/* A live dot inside a live chip was --live on --live: the dot was there,
   it was simply the same lime as the pill behind it. Inside a chip it
   takes the chip's own ink, which is the one colour already proven to
   read on that fill -- and its pulse follows, because the default ring is
   a hard-coded lime that disappears there for the same reason. */
.chip-live .livedot { background: currentColor; animation-name: pulse-ink; }
@keyframes pulse-ink {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, currentColor 40%, transparent); }
  70%  { box-shadow: 0 0 0 7px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}

@keyframes pulse {
  0%   { box-shadow: 0 0 0 0 rgba(190,211,30,.55); }
  70%  { box-shadow: 0 0 0 8px rgba(190,211,30,0); }
  100% { box-shadow: 0 0 0 0 rgba(190,211,30,0); }
}

/* ---------- tables ---------------------------------------------------- */
.tablewrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
table { width: 100%; border-collapse: collapse; font-size: 14.5px; }
th {
  font-family: var(--mono); font-size: 10.5px; font-weight: 500;
  letter-spacing: .12em; text-transform: uppercase; color: var(--text-faint);
  text-align: right; padding: 10px 8px; border-bottom: 1px solid var(--border); white-space: nowrap;
}
td { padding: 12px 8px; text-align: right; border-bottom: 1px solid var(--border); font-variant-numeric: tabular-nums; }
th:first-child, td:first-child { text-align: left; padding-left: 18px; }
th:last-child, td:last-child { padding-right: 18px; }
tbody tr:last-child td { border-bottom: none; }
.rank-1 td { font-weight: 700; }
.medal { font-family: var(--mono); font-size: 12.5px; color: var(--text-faint); margin-right: 10px; }
.rank-1 .medal { color: var(--win-ink); }

/* ---------- theme toggle (site header) -------------------------------- */
.themebtn {
  width: 38px; height: 38px; flex: none;
  display: grid; place-items: center;
  border-radius: var(--r-full);
  color: var(--text-muted);
  border: 1.5px solid var(--border);
  background: var(--surface);
}
.themebtn:hover { color: var(--accent); border-color: var(--accent); }

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

/* ---------- empty state ---------------------------------------------- */
.empty { text-align: center; padding: 42px 22px; color: var(--text-faint); }
.empty h3 { font-size: 18px; color: var(--text-muted); margin-bottom: 6px; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
}

/* ---- show the password ------------------------------------------------ */
/* The button sits inside the field rather than beside it, so the field keeps
   its full width and the row keeps its height. Built by password.js, so it
   never appears as a control that does nothing when scripting is off. */
.pw-wrap { position: relative; display: block; }
.pw-input { padding-right: 46px; }
.pw-toggle {
  position: absolute; top: 50%; right: 6px; transform: translateY(-50%);
  width: 34px; height: 34px; display: grid; place-items: center;
  border: 0; border-radius: var(--r-sm); background: transparent;
  color: var(--text-muted); cursor: pointer;
}
.pw-toggle:hover { color: var(--text); background: var(--surface-alt); }
.pw-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ---- for screen readers only ------------------------------------------ */
/* Not display:none — that removes it from the accessibility tree too, which
   is the opposite of the point. Clipped to nothing, still announced. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ---- a row of choices instead of a dropdown --------------------------- */
/* A select is three interactions — open, find, choose — and on a phone it
   takes the whole screen to do it. Somebody adding six players in a row does
   that eighteen times. Three pills is one tap each, and the current answer
   is readable without opening anything.
   Radios underneath: it works with scripting off, a screen reader already
   knows what a radio group is, and arrow keys move between them for free. */
.seg {
  display: flex;
  gap: 3px;
  padding: 3px;
  background: var(--surface-alt);
  border-radius: var(--r-full);
}
.seg-opt { flex: 1 1 0; min-width: 0; margin: 0; cursor: pointer; }

/* Clipped rather than display:none, so it keeps its place in the focus
   order and a screen reader still finds it. */
.seg-opt input {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}
.seg-opt span {
  display: block;
  padding: 8px 6px;
  border-radius: var(--r-full);
  text-align: center;
  font-size: 13px; font-weight: 600; line-height: 1.2;
  color: var(--text-muted);            /* 6.06:1 on the track */
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  transition: background-color .12s ease, color .12s ease;
}
.seg-opt:hover span { color: var(--text); }
.seg-opt input:checked + span {
  background: var(--surface);
  color: var(--accent);                /* 5.45:1 */
  font-weight: 700;
  box-shadow: var(--shadow);
}
/* The ring goes on the pill, since the input it belongs to is clipped. */
.seg-opt input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 1px; }

/* =====================================================================
   GROUPED SETTINGS FORMS
   =====================================================================
   It was one card holding twenty-five controls in a single column across
   the full width of the board: a "Session name" field 1,370px wide, no
   grouping, and nothing to separate the three settings that change the
   draw from the three that change who may check in. An organiser looking
   for "win by two" had to read the whole form.

   Five groups, each saying what it is for, in a column narrow enough that
   a text field is a text field rather than a horizon.

   In app.css rather than board.css because two screens need it now: the
   session's own settings and the super admin's account editor. Both were
   the same long single column of unrelated inputs.
   ===================================================================== */
/* Two groups across once there is room for them. The fix for a 900px void
   beside a form is not to stretch the fields — stretching them is what made
   this form unreadable in the first place. The groups fill the width; the
   fields inside them stay the size a field should be.

   Measured against the form's own width, not the window's: the form lives
   in a boxed column, so the window is not what decides whether two groups
   fit. */
.setwrap { width: 100%; container-type: inline-size; }

.setwrap > form { display: grid; gap: 0 16px; }
@container (min-width: 1000px) {
  .setwrap > form { grid-template-columns: 1fr 1fr; align-items: start; }
  /* The save bar, and the group that is two textareas, take the full width. */
  .setwrap > form > .setsave { grid-column: 1 / -1; }
  .setwrap > form > .setgroup-wide { grid-column: 1 / -1; }

  /* A form whose groups should PACK rather than line up in rows.
     A grid aligns row by row, so one short group next to a tall one leaves
     a hole as deep as the difference — on the profile page, "How you play"
     is two dropdowns beside six fields, and the gap under it was the
     largest thing on the screen. Columns fill instead: each group is
     placed under the previous one in its column and kept whole by
     break-inside, so the two sides end up roughly level on their own.
     Only worth it where the groups are genuinely uneven; the settings
     forms are not, and stay on the grid. */
  .setwrap > form.setflow { display: block; columns: 2; column-gap: 16px; }
  .setwrap > form.setflow > .setgroup { break-inside: avoid; }
  .setwrap > form.setflow > .setsave { column-span: all; }
}

/* Each group sizes its own contents against its own width, not the
   window's: at two groups across, a group is ~625px and wants two fields
   in a row, while the same group alone on a phone wants one. A media
   query cannot see the difference. */
.setgroup { container-type: inline-size; }

.setgroup {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  box-shadow: var(--shadow);
  margin-bottom: 16px;
  overflow: hidden;
}
.setgroup > header {
  padding: 13px 18px;
  background: var(--surface-alt);
  border-bottom: 1px solid var(--border);
}
.setgroup > header h3 {
  margin: 0;
  font-family: var(--display);
  font-weight: 600;
  font-size: 14.5px;
  letter-spacing: -.01em;
}
.setgroup > header p {
  margin: 2px 0 0;
  font-size: 12.5px;
  color: var(--text-muted);
}

.setbody { padding: 16px 18px; display: grid; gap: 15px 18px; }
@container (min-width: 520px) {
  .setbody { grid-template-columns: 1fr 1fr; align-items: start; }
  /* Anything that is prose, or a list, gets the full width; the rest pair
     up. A street address and a set of house rules do not belong in half a
     column, and a dropdown with four options does not need a whole one. */
  .setbody > .wide,
  .setbody > .imgfield,
  .setbody > .settoggle { grid-column: 1 / -1; }
}

/* The switches, as one bordered list rather than four floating rows. */
.settoggle {
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  overflow: hidden;
}
/* The list draws the border; the rows inside it must not.
   .check carries its own 1px border and radius so it can stand alone as a
   single switch, and nothing here was unsetting them -- so every row drew
   a rounded box inside the list's box, and the two edges sat a pixel
   apart. Visible on the session settings pane as well as the profile. */
.settoggle > .check {
  margin: 0;
  padding: 12px 14px;
  align-items: flex-start;
  border: 0;
  border-radius: 0;
  background: transparent;
}
.settoggle > .check + .check { border-top: 1px solid var(--border); }
.settoggle > .check:hover { background: var(--surface-alt); }
.settoggle .hint { display: block; margin-top: 2px; }

/* ps_place_fields returns its three selects inside one wrapper, so as a
   single grid item they stacked in half a column while everything around
   them paired up. It spans, and divides its own width three ways — but
   only where three "Choose a province first" labels actually fit. */
.setbody > .place-fields { grid-column: 1 / -1; display: grid; gap: 15px 18px; }
@container (min-width: 760px) {
  .setbody > .place-fields { grid-template-columns: repeat(3, 1fr); }
}
@container (min-width: 520px) and (max-width: 759px) {
  .setbody > .place-fields { grid-template-columns: 1fr 1fr; }
}

.courtnames { display: grid; gap: 8px; }
@container (min-width: 520px) {
  .courtnames { grid-template-columns: 1fr 1fr; }
}

/* Sticky, because the form is five groups long and the one control you
   always want is at the bottom of it. */
.setsave {
  position: sticky;
  bottom: 0;
  z-index: 4;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  flex-wrap: wrap;
  /* Standing off the last group rather than butting against it. The
     groups carry their own 16px below, but this sits in a different grid
     row and inherited none of it. */
  margin-top: 8px;
  margin-bottom: 16px;
  padding: 12px 16px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-lift);
}
.setsave .btn { flex: none; min-width: 170px; }

/* Ending a night is not the same kind of act as renaming it. */
.setgroup-danger > header { background: var(--win-soft); border-bottom-color: #EBD2C6; }
.setgroup-danger > header h3 { color: var(--win-ink); }
.setbody-row { grid-template-columns: 1fr 1fr; }
@container (max-width: 519px) { .setbody-row { grid-template-columns: 1fr; } }

/* A field-level error, said in the same red the app uses for "this undoes
   something". It was an inline style pointing at --danger, a token that has
   never existed in this palette, so every validation message fell back to
   the hard-coded hex beside it. */
.field-error {
  margin: 6px 0 0;
  font-size: 12.5px;
  color: var(--win-ink);
}

/* A note above a grouped form — "this is your own account", and the like. */
.setnote {
  margin-bottom: 16px;
  padding: 12px 15px;
  background: var(--accent-soft);
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent);
  border-radius: var(--r-sm);
  font-size: 13px;
  line-height: 1.55;
  color: var(--text-muted);
}

/* A value that is shown but not editable. Deliberately not a disabled
   input: a greyed-out box that looks like a field invites people to click
   it and then wonder what they did wrong. */
.readonly-value {
  margin: 0;
  padding: 11px 2px;
  font-size: 15px;
  font-weight: 600;
  color: var(--text-muted);
  word-break: break-word;
}

/* The head of a page that is about one person: their face, then their
   name. The disc keeps its own size, so this is only the row. */
.profilehead { display: flex; align-items: center; gap: 15px; flex-wrap: wrap; }
.profilehead > div { min-width: 0; }

/* ---------- games waiting to be claimed ------------------------------ */
/* An offer, not a warning. Tinted with the action colour rather than the
   error red: nothing is wrong, there is simply something to gain. */
.claimbar {
  display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 3px solid var(--action);
  border-radius: var(--r-md);
  box-shadow: var(--shadow);
  padding: 14px 16px;
}
.claimbar > div { flex: 1 1 320px; min-width: 0; }
.claimbar strong { display: block; font-size: 14.5px; letter-spacing: -.01em; }
.claimbar .hint { display: block; margin-top: 3px; }
.claimbar .btn { flex: none; }

/* ---------- choosing a picture --------------------------------------- */
/* A file input styled by the browser is a grey box reading "No file
   chosen" that cannot be restyled and does not say what happens next. The
   input is hidden and its own <label> is the button, which is the one
   trick here — a label for a file input opens the picker on click and on
   Enter, so nothing is lost by not using the control itself. */

.imgfield .imgrow {
  display: flex;
  align-items: flex-start;
  gap: 16px;
  margin-top: 7px;
}

/* The preview is the real component at the real size — for an avatar that
   is the disc the board draws, initials and all. Showing "no photo" as
   the thing people will actually see beats showing them an empty frame. */
.imgpreview { flex: none; display: grid; place-items: center; }

.imgpreview-wide {
  width: 168px;
  height: 94px;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  overflow: hidden;
  background: var(--brand);
}
.imgpreview-wide img,
.imgpreview-wide .imgdrawn { width: 100%; height: 100%; display: block; }
.imgpreview-wide img { object-fit: cover; }

/* The same court-in-CSS the browse cards fall back to, so "no cover yet"
   previews as the banner a session will actually get. */
.imgdrawn {
  background:
    linear-gradient(to right, transparent calc(50% - 1px), rgba(255,255,255,.85) 50%, transparent calc(50% + 1px)),
    linear-gradient(to bottom, transparent calc(50% - 1px), rgba(255,255,255,.45) 50%, transparent calc(50% + 1px)),
    linear-gradient(160deg, rgba(255,255,255,.10), transparent 60%),
    var(--brand);
}

.imgctl {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
  min-width: 0;
}
.imgctl .hint { margin: 0; }
/* What the file has to be, as figures rather than prose. Set in the mono
   the rest of the app uses for numbers so it reads as a spec and not as
   another sentence of advice. */
.imgspec {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--text-faint);
  letter-spacing: .01em;
}

.btn-file { cursor: pointer; }

/* The input is .sr-only, so the ring has to be put on the label by hand
   or a keyboard user tabs onto something with no visible focus at all.
   This is why the input sits before the label in the markup. */
.sr-only:focus-visible + .btn-file {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.imgdrop {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 12.5px;
  color: var(--text-muted);
  cursor: pointer;
}
.imgdrop input { margin: 0; }

/* A hint carrying a refusal the browser made on its own, before the file
   was ever sent. Same slot as the ordinary hint so nothing moves, and the
   same red as .field-error and .err — see the note above about --danger,
   a token this palette has never had. */
.hint-bad { color: var(--win-ink); font-weight: 600; }

/* An uploaded photo in a disc that was built for two letters: the image
   fills it and is clipped to the same circle. */
.avatar-img { overflow: hidden; background: var(--surface-alt); }
.avatar-img img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* A switch that cannot apply right now. Dimmed rather than hidden, so the
   option is still discoverable and the reason beneath it is readable. */
.check.is-off { opacity: .6; }
.check.is-off strong { color: var(--text-muted); }

/* A consent checkbox and the sentence it belongs to. The box must sit on the
   first line of the text, not float in the middle of a wrapped paragraph. */
.agree { display: flex; align-items: flex-start; gap: 10px; cursor: pointer; font-size: 14px; }
.agree input { flex: none; width: 18px; height: 18px; margin-top: 1px; accent-color: var(--accent); }
.agree span { color: var(--text-muted); }
.agree a { font-weight: 600; }

/* The legal documents. A measure you can actually read a clause in. */
.legal { max-width: 68ch; }
.legal h2 { font-size: 19px; margin: 30px 0 10px; }
.legal h2:first-of-type { margin-top: 0; }
.legal p { color: var(--text-muted); margin-bottom: 12px; }
.legal-note {
  margin-bottom: 30px; padding: 16px 18px;
  border: 1px solid var(--border); border-left: 3px solid var(--win);
  border-radius: var(--r-md); background: var(--surface);
  font-size: 14px; color: var(--text-muted);
}
.legal-note strong { color: var(--text); }

/* Terms and privacy in the footer: reachable from every public page, which is
   the minimum for a document somebody is asked to agree to. */
.footlinks { display: inline-flex; gap: 16px; }
.footlinks a { color: inherit; opacity: .85; text-decoration: none; border-bottom: 1px solid currentColor; }
.footlinks a:hover { opacity: 1; }

/* ---- rich text ------------------------------------------------------- */
/* The two fields an organiser writes prose in. A toolbar over a contenteditable
   div, sharing the .input border so the pair reads as one control rather than
   as a widget that landed on the form. See richtext.js -- the plain textarea
   underneath still submits when the script does not run. */
.rich {
  border: 1.5px solid var(--border-strong);
  border-radius: var(--r-md);
  background: var(--surface);
  overflow: hidden;
}
.rich:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }

.rich-bar {
  display: flex; align-items: center; flex-wrap: wrap; gap: 2px;
  padding: 6px 7px;
  background: var(--surface-alt);
  border-bottom: 1px solid var(--border);
}

.rich-btn {
  display: grid; place-items: center;
  width: 30px; height: 30px; padding: 0;
  border: 0; border-radius: var(--r-sm);
  background: transparent; color: var(--text-muted);
  cursor: pointer; line-height: 1;
}
.rich-btn:hover { background: var(--surface); color: var(--text); }
.rich-btn.is-on { background: var(--accent-soft); color: var(--accent); }
.rich-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* B, I and U say what they do by how they are drawn. */
.rich-letter { font-size: 14px; font-weight: 700; }
.rich-letter-italic { font-style: italic; font-family: var(--display); }
.rich-letter-underline { text-decoration: underline; }

.rich-sep { width: 1px; height: 18px; margin: 0 4px; background: var(--border); }

.rich-body {
  border: 0; border-radius: 0; box-shadow: none;
  min-height: 110px; max-height: 340px; overflow-y: auto;
  padding: 12px 13px;
  font-size: 15px; line-height: 1.55;
}
.rich-body:focus { outline: none; box-shadow: none; }

/* contenteditable has no placeholder of its own. */
.rich-body.is-empty::before {
  content: attr(data-placeholder);
  color: var(--text-faint);
  pointer-events: none;
}

/* The tags the server keeps, and nothing else, so what is typed is what is
   published -- see App\Support\Html::ALLOWED. */
.rich-body p { margin: 0 0 8px; }
.rich-body p:last-child { margin-bottom: 0; }
.rich-body ul, .rich-body ol { margin: 0 0 8px; padding-left: 22px; }
.rich-body li { margin-bottom: 3px; }
.rich-body a { color: var(--accent); text-decoration: underline; }

/* ---- the same prose, once published --------------------------------- */
.prose > *:last-child { margin-bottom: 0; }
.prose p { margin: 0 0 8px; }
.prose ul, .prose ol { margin: 0 0 8px; padding-left: 20px; }
.prose li { margin-bottom: 3px; }
.prose a { color: var(--accent); text-decoration: underline; }
.prose strong { font-weight: 700; }

/* ---- the "?" beside a setting ------------------------------------------
   A button beside the label (help.js) that opens its explanation in place,
   pushing the form down. It floated over the field at first, and on a
   one-column phone form the card covered the next field's "?". */
.label-row { display: flex; align-items: center; gap: 7px; }
.help-q {
  position: relative; flex: none;
  display: inline-grid; place-items: center;
  width: 18px; height: 18px; padding: 0; border-radius: 50%;
  background: transparent;
  border: 1.5px solid var(--border-strong); color: var(--text-muted);
  font: 800 11px/1 var(--body);
  cursor: pointer;
}
.help-q[hidden] { display: none; }
/* 18px to look at, 34px to hit: it is tapped with a thumb. */
.help-q::after { content: ""; position: absolute; inset: -8px; }
.help-q:hover { border-color: var(--accent); color: var(--accent); }
.help-q:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.help-q[aria-expanded="true"] { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.help-body {
  background: var(--accent-soft); color: var(--text);
  border-radius: var(--r-sm);
  padding: 11px 13px; font-size: 13.5px; line-height: 1.5;
}
.help-body p { margin: 0; }
.help-body p + p { margin-top: 7px; }
.help-body .help-foot { color: var(--text-muted); }
/* On the mixed-doubles card the "?" sits in the corner, outside the label
   so that opening it does not also tick the box. */
.check-help { position: relative; }
.help-corner { position: absolute; top: 15px; right: 15px; z-index: 1; }
.check-help > .check { padding-right: 46px; }
.check-help > .help-body { margin-top: 8px; }
